Skip to content

Commit 17d8f7b

Browse files
committed
Add claim users field to approvals
1 parent 1b178c3 commit 17d8f7b

11 files changed

Lines changed: 255 additions & 13 deletions

File tree

backend/internal/orders/forms/admin.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ type AdminGetApprovalFlowsForm struct {
1919
type AdminUpdateApprovalFlowsForm struct {
2020
Definition datatypes.JSON `form:"definition" json:"definition"`
2121
Name string `form:"name" json:"name" binding:"required,min=3,max=256"`
22+
ClaimUsers []string `form:"claim_users" json:"claim_users" binding:"required,min=1,dive,required"`
2223
}
2324

2425
type AdminCreateApprovalFlowsForm struct {
2526
Definition datatypes.JSON `form:"definition" json:"definition"`
2627
Name string `form:"name" json:"name" binding:"required,min=3,max=256"`
28+
ClaimUsers []string `form:"claim_users" json:"claim_users" binding:"required,min=1,dive,required"`
2729
}
2830

2931
type AdminBindUsersToApprovalFlowForm struct {

backend/internal/orders/models/models.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type InsightOrderRecords struct {
3333
Applicant string `gorm:"type:varchar(32);not null;default:'';comment:申请人;index" json:"applicant"`
3434
Organization string `gorm:"type:varchar(256);not null;default:'';index;comment:组织" json:"organization"`
3535
Claimer string `gorm:"type:varchar(32);not null;default:'';comment:认领人;index" json:"claimer"`
36+
ClaimUsers datatypes.JSON `gorm:"type:json;null;default:null;comment:可认领人员列表" json:"claim_users"`
3637
Executor string `gorm:"type:varchar(32);not null;default:'';comment:工单执行人;index" json:"executor"`
3738
Approver datatypes.JSON `gorm:"type:json;null;default:null;comment:工单审核人" json:"approver"`
3839
Reviewer datatypes.JSON `gorm:"type:json;null;default:null;comment:工单复核人" json:"reviewer"`
@@ -55,6 +56,7 @@ type InsightApprovalFlows struct {
5556
*models.Model
5657
ApprovalID uuid.UUID `gorm:"type:char(36);comment:审批流ID;uniqueIndex:uniq_approval_id" json:"approval_id"`
5758
Name string `gorm:"type:varchar(64);not null;default:'';comment:审批流名称" json:"name"`
59+
ClaimUsers datatypes.JSON `gorm:"type:json;null;default:null;comment:可认领人员列表" json:"claim_users"`
5860
Definition datatypes.JSON `json:"definition"` // [{"stage":1, "approvers":["zhangsan","lisi"], "type":"AND", "stage_name": '部门审批'}]
5961
}
6062

backend/internal/orders/services/admin.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (s *AdminGetApprovalFlowsService) Run() (responseData any, total int64, err
5757
}
5858

5959
// 固定字段 + 去重
60-
base = base.Select("a.id, a.name, a.definition, a.approval_id, a.created_at, a.updated_at").
60+
base = base.Select("a.id, a.name, a.definition, a.claim_users, a.approval_id, a.created_at, a.updated_at").
6161
Group("a.id")
6262

6363
total = pagination.Pager(&s.PaginationQ, base, &records)
@@ -71,10 +71,15 @@ type AdminUpdateApprovalFlowsService struct {
7171
}
7272

7373
func (s *AdminUpdateApprovalFlowsService) Run() (responseData any, total int64, err error) {
74+
claimUsers, err := marshalClaimUsers(s.ClaimUsers)
75+
if err != nil {
76+
return nil, 0, err
77+
}
7478
// 更新记录
7579
result := global.App.DB.Model(&models.InsightApprovalFlows{}).Where("id=?", s.ID).Updates(map[string]any{
76-
"definition": s.Definition,
77-
"name": s.Name,
80+
"definition": s.Definition,
81+
"name": s.Name,
82+
"claim_users": claimUsers,
7883
})
7984

8085
if result.Error != nil {
@@ -89,8 +94,13 @@ type AdminCreateApprovalFlowsService struct {
8994
}
9095

9196
func (s *AdminCreateApprovalFlowsService) Run() error {
97+
claimUsers, err := marshalClaimUsers(s.ClaimUsers)
98+
if err != nil {
99+
return err
100+
}
92101
flow := models.InsightApprovalFlows{
93102
Definition: s.Definition,
103+
ClaimUsers: claimUsers,
94104
Name: s.Name,
95105
ApprovalID: uuid.New(),
96106
}
@@ -162,7 +172,7 @@ type AdminGetApprovalFlowUsersService struct {
162172

163173
func (s *AdminGetApprovalFlowUsersService) Run() (responseData any, total int64, err error) {
164174
var records []models.InsightApprovalFlowUsers
165-
tx := global.App.DB.Table("insight_approval_flow_users").Where("approval_id=?", s.ApprovalID).Scan(&records)
175+
tx := global.App.DB.Table("insight_approval_flow_users").Where("approval_id=?", s.ApprovalID)
166176

167177
// 搜索
168178
if s.Search != "" {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package services
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/lazzyfu/goinsight/pkg/utils"
9+
"gorm.io/datatypes"
10+
)
11+
12+
func normalizeClaimUsers(users []string) ([]string, error) {
13+
if len(users) == 0 {
14+
return nil, fmt.Errorf("可领取人不能为空")
15+
}
16+
17+
seen := make(map[string]struct{}, len(users))
18+
result := make([]string, 0, len(users))
19+
for _, user := range users {
20+
u := strings.TrimSpace(user)
21+
if u == "" {
22+
continue
23+
}
24+
if _, ok := seen[u]; ok {
25+
continue
26+
}
27+
seen[u] = struct{}{}
28+
result = append(result, u)
29+
}
30+
31+
if len(result) == 0 {
32+
return nil, fmt.Errorf("可领取人不能为空")
33+
}
34+
return result, nil
35+
}
36+
37+
func marshalClaimUsers(users []string) (datatypes.JSON, error) {
38+
normalized, err := normalizeClaimUsers(users)
39+
if err != nil {
40+
return nil, err
41+
}
42+
data, err := json.Marshal(normalized)
43+
if err != nil {
44+
return nil, fmt.Errorf("序列化可领取人失败: %w", err)
45+
}
46+
return datatypes.JSON(data), nil
47+
}
48+
49+
func parseClaimUsers(raw datatypes.JSON) ([]string, error) {
50+
if len(raw) == 0 {
51+
return nil, fmt.Errorf("审批流未配置可领取人")
52+
}
53+
54+
var users []string
55+
if err := json.Unmarshal(raw, &users); err != nil {
56+
return nil, fmt.Errorf("解析可领取人失败: %w", err)
57+
}
58+
59+
normalized, err := normalizeClaimUsers(users)
60+
if err != nil {
61+
return nil, err
62+
}
63+
return normalized, nil
64+
}
65+
66+
func canUserClaim(raw datatypes.JSON, username string) (bool, error) {
67+
claimUsers, err := parseClaimUsers(raw)
68+
if err != nil {
69+
return false, err
70+
}
71+
return utils.IsContain(claimUsers, username), nil
72+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package services
2+
3+
import (
4+
"testing"
5+
6+
"gorm.io/datatypes"
7+
)
8+
9+
func TestNormalizeClaimUsers(t *testing.T) {
10+
t.Run("dedupe and trim", func(t *testing.T) {
11+
users, err := normalizeClaimUsers([]string{" alice ", "bob", "alice", ""})
12+
if err != nil {
13+
t.Fatalf("unexpected error: %v", err)
14+
}
15+
if len(users) != 2 || users[0] != "alice" || users[1] != "bob" {
16+
t.Fatalf("unexpected users: %#v", users)
17+
}
18+
})
19+
20+
t.Run("empty should fail", func(t *testing.T) {
21+
if _, err := normalizeClaimUsers([]string{"", " "}); err == nil {
22+
t.Fatal("expected error for empty claim users")
23+
}
24+
})
25+
}
26+
27+
func TestCanUserClaim(t *testing.T) {
28+
raw := datatypes.JSON([]byte(`["alice","bob"]`))
29+
30+
ok, err := canUserClaim(raw, "alice")
31+
if err != nil {
32+
t.Fatalf("unexpected error: %v", err)
33+
}
34+
if !ok {
35+
t.Fatal("alice should be allowed to claim")
36+
}
37+
38+
ok, err = canUserClaim(raw, "charlie")
39+
if err != nil {
40+
t.Fatalf("unexpected error: %v", err)
41+
}
42+
if ok {
43+
t.Fatal("charlie should not be allowed to claim")
44+
}
45+
}
46+
47+
func TestCanUserClaimInvalidJSON(t *testing.T) {
48+
if _, err := canUserClaim(datatypes.JSON([]byte(`{}`)), "alice"); err == nil {
49+
t.Fatal("expected error for invalid claim users json")
50+
}
51+
}

backend/internal/orders/services/commit.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ type CreateOrderService struct {
102102
Audit *parser.TiStmt
103103
}
104104

105-
func (s *CreateOrderService) generateApprovalRecords(tx *gorm.DB, orderID uuid.UUID) error {
105+
func (s *CreateOrderService) generateApprovalRecords(tx *gorm.DB, orderID uuid.UUID) (datatypes.JSON, error) {
106106
type FlowStage struct {
107107
Type string `json:"type"`
108108
Stage int `json:"stage"`
@@ -111,16 +111,25 @@ func (s *CreateOrderService) generateApprovalRecords(tx *gorm.DB, orderID uuid.U
111111
}
112112

113113
var record models.InsightApprovalFlows
114-
flow := tx.Table("insight_approval_flow_users a").Select(`b.definition`).
114+
flow := tx.Table("insight_approval_flow_users a").Select(`b.definition, b.claim_users`).
115115
Joins("inner join insight_approval_flow b on a.approval_id = b.approval_id").
116116
Where("a.username = ?", s.Username).Take(&record)
117117
if flow.Error != nil || flow.RowsAffected == 0 {
118-
return fmt.Errorf("未找到您的审批流配置,请联系管理员设置")
118+
return nil, fmt.Errorf("未找到您的审批流配置,请联系管理员设置")
119+
}
120+
121+
claimUsers, err := parseClaimUsers(record.ClaimUsers)
122+
if err != nil {
123+
return nil, err
124+
}
125+
claimUsersJSON, err := marshalClaimUsers(claimUsers)
126+
if err != nil {
127+
return nil, err
119128
}
120129
// [{"type": "OR", "stage": 1, "approvers": ["zhangsan", "lisi"], "stage_name": "安全组审批"}, {"type": "AND", "stage": 1, "approvers": ["admin"], "stage_name": "部门负责人审批"}]
121130
var stages []FlowStage
122-
if err := json.Unmarshal(record.Definition, &stages); err != nil {
123-
return fmt.Errorf("解析审批流JSON失败: %w", err)
131+
if err = json.Unmarshal(record.Definition, &stages); err != nil {
132+
return nil, fmt.Errorf("解析审批流JSON失败: %w", err)
124133
}
125134

126135
for _, s := range stages {
@@ -134,11 +143,11 @@ func (s *CreateOrderService) generateApprovalRecords(tx *gorm.DB, orderID uuid.U
134143
ApprovalType: commonModels.EnumType(s.Type),
135144
}
136145
if err := tx.Create(&audit).Error; err != nil {
137-
return fmt.Errorf("创建审批记录失败: %w", err)
146+
return nil, fmt.Errorf("创建审批记录失败: %w", err)
138147
}
139148
}
140149
}
141-
return nil
150+
return claimUsersJSON, nil
142151
}
143152

144153
// 审核SQL
@@ -299,10 +308,15 @@ func (s *CreateOrderService) Run() error {
299308
return err
300309
}
301310
// 生成审批流
302-
err = s.generateApprovalRecords(tx, orderID)
311+
claimUsers, err := s.generateApprovalRecords(tx, orderID)
303312
if err != nil {
304313
return err
305314
}
315+
if err := tx.Model(&models.InsightOrderRecords{}).
316+
Where("order_id=?", orderID).
317+
Update("claim_users", claimUsers).Error; err != nil {
318+
return err
319+
}
306320
// 记录操作日志
307321
if err := WriteOrderLog(tx, orderID.String(), s.Username, fmt.Sprintf("用户%s提交了工单", s.Username)); err != nil {
308322
global.App.Log.Error("CreateOrderService.Run error:", err.Error())

backend/internal/orders/services/operate.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ func (s *ClaimOrderService) Run() (err error) {
188188
if !utils.IsContain([]string{"APPROVED"}, string(record.Progress)) {
189189
return fmt.Errorf("当前工单没有审批通过,无法认领")
190190
}
191+
allowed, err := canUserClaim(record.ClaimUsers, s.Username)
192+
if err != nil {
193+
return err
194+
}
195+
if !allowed {
196+
return fmt.Errorf("您不在可领取人员列表中,无法认领工单")
197+
}
191198
// 认领操作
192199
txErr := global.App.DB.Transaction(func(tx *gorm.DB) error {
193200
// 更新工单认领人
@@ -249,6 +256,13 @@ func (s *TransferOrderService) Run() (err error) {
249256
if record.Claimer != s.Username {
250257
return fmt.Errorf("只有工单认领人才能转交工单")
251258
}
259+
allowed, err := canUserClaim(record.ClaimUsers, s.NewClaimer)
260+
if err != nil {
261+
return err
262+
}
263+
if !allowed {
264+
return fmt.Errorf("转交目标用户不在可领取人员列表中")
265+
}
252266
// 转交操作
253267
txErr := global.App.DB.Transaction(func(tx *gorm.DB) error {
254268
// 更新工单执行人

www/src/views/admin/perms/flows/ApprovalFlowFormModal.vue

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,30 @@
1919
<a-input v-model:value="formData.name" placeholder="请输入审批流名称" allow-clear />
2020
</a-form-item>
2121

22+
<a-form-item
23+
label="可领取人"
24+
name="claim_users"
25+
:rules="[
26+
{
27+
required: true,
28+
message: '请选择可领取人',
29+
}
30+
]"
31+
has-feedback
32+
>
33+
<a-select
34+
v-model:value="formData.claim_users"
35+
mode="multiple"
36+
show-search
37+
:filter-option="filterUserOption"
38+
style="width: 100%"
39+
placeholder="请选择可领取人(谁领取谁执行)"
40+
:options="props.userOptions"
41+
option-label-prop="label"
42+
:max-tag-count="4"
43+
/>
44+
</a-form-item>
45+
2246
<div class="divider-section">
2347
<div class="divider-title">
2448
<span class="divider-icon">📋</span>
@@ -184,6 +208,10 @@ const handleCancel = () => {
184208
185209
// 自定义校验(对动态数组字段使用自定义校验更可控)
186210
const validateDefinition = async () => {
211+
const claimUsers = formData.value.claim_users || []
212+
if (claimUsers.length === 0) {
213+
return Promise.reject('请至少选择一个可领取人。')
214+
}
187215
const definition = formData.value.definition
188216
if (!definition || definition.length === 0) {
189217
return Promise.reject('请至少配置一个审批阶段。')

www/src/views/admin/perms/flows/ApprovalFlowList.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
<template v-if="column.key === 'flow'">
2424
<a-tag color="blue">{{ record.definition.length }} 个阶段</a-tag>
2525
</template>
26+
<template v-if="column.key === 'claim_users'">
27+
<a-tag color="cyan">{{ record.claim_users.length }} 人</a-tag>
28+
</template>
2629

2730
<template v-if="column.key === 'action'">
2831
<a-space>
@@ -108,6 +111,7 @@ const uiData = reactive({
108111
tableColumns: [
109112
{ title: '审批流名称', dataIndex: 'name', key: 'name', width: 150 },
110113
{ title: '流程阶段数', dataIndex: 'definition', key: 'flow', width: 120 },
114+
{ title: '可领取人', dataIndex: 'claim_users', key: 'claim_users', width: 120 },
111115
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
112116
{ title: '更新时间', dataIndex: 'updated_at', key: 'updated_at' },
113117
{ title: '操作', dataIndex: 'action', key: 'action', fixed: 'right', width: 250 },
@@ -117,6 +121,7 @@ const uiData = reactive({
117121
// form表单
118122
const defaultForm = {
119123
name: '',
124+
claim_users: [],
120125
definition: [
121126
{
122127
stage: 1,
@@ -168,6 +173,11 @@ const fetchData = async () => {
168173
: item.definition
169174
? JSON.parse(item.definition)
170175
: [],
176+
claim_users: Array.isArray(item.claim_users)
177+
? item.claim_users
178+
: item.claim_users
179+
? JSON.parse(item.claim_users)
180+
: [],
171181
}))
172182
173183
uiData.flows = res.data.map((flow) => ({
@@ -219,6 +229,11 @@ const handleEdit = (record) => {
219229
220230
formState.value = {
221231
...record,
232+
claim_users: Array.isArray(record.claim_users)
233+
? record.claim_users
234+
: typeof record.claim_users === 'string'
235+
? JSON.parse(record.claim_users)
236+
: [],
222237
definition: definition.length > 0 ? definition : defaultForm.definition,
223238
}
224239
uiState.isModalOpen = true

0 commit comments

Comments
 (0)