Procházet zdrojové kódy

feat(wm-bpm): #32 流程引擎核心完整实现

- ProcessEngine: 增强为数据库持久化引擎,支持启动/审批/驳回/转办/会签/待办/已办
- ProcessDefinitionService: 流程定义CRUD/发布/停用/版本管理
- ProcessEngineController: /api/bpm/engine/* REST API
- ProcessDefinitionController: /api/bpm/definition/* REST API
- BpmProcessInstanceMapper/BpmTodoTaskMapper: 新增查询方法
- V_bpm_core.sql: 完整表结构DDL + 示例数据
- ProcessEngineTest/ProcessDefinitionServiceTest: 单元测试覆盖核心场景
- 保留原有 ProcessController 向后兼容
bot_dev2 před 5 dny
rodič
revize
689ddfac57

+ 122
- 0
wm-bpm/src/main/java/com/water/bpm/controller/ProcessDefinitionController.java Zobrazit soubor

1
+package com.water.bpm.controller;
2
+
3
+import com.water.bpm.entity.BpmProcessDefinition;
4
+import com.water.bpm.entity.BpmProcessNode;
5
+import com.water.bpm.service.ProcessDefinitionService;
6
+import com.water.common.core.result.R;
7
+import io.swagger.v3.oas.annotations.Operation;
8
+import io.swagger.v3.oas.annotations.tags.Tag;
9
+import lombok.RequiredArgsConstructor;
10
+import org.springframework.web.bind.annotation.*;
11
+
12
+import java.util.List;
13
+
14
+/**
15
+ * 流程定义管理 REST API
16
+ * 提供流程定义的 CRUD、发布、停用、版本管理等接口
17
+ */
18
+@Tag(name = "流程定义管理")
19
+@RestController
20
+@RequestMapping("/api/bpm/definition")
21
+@RequiredArgsConstructor
22
+public class ProcessDefinitionController {
23
+
24
+    private final ProcessDefinitionService definitionService;
25
+
26
+    // ==================== CRUD ====================
27
+
28
+    @Operation(summary = "创建流程定义")
29
+    @PostMapping
30
+    public R<BpmProcessDefinition> create(@RequestBody BpmProcessDefinition definition) {
31
+        return R.ok(definitionService.create(definition));
32
+    }
33
+
34
+    @Operation(summary = "更新流程定义")
35
+    @PutMapping("/{id}")
36
+    public R<BpmProcessDefinition> update(@PathVariable Long id,
37
+                                           @RequestBody BpmProcessDefinition definition) {
38
+        return R.ok(definitionService.update(id, definition));
39
+    }
40
+
41
+    @Operation(summary = "删除流程定义")
42
+    @DeleteMapping("/{id}")
43
+    public R<Void> delete(@PathVariable Long id) {
44
+        definitionService.delete(id);
45
+        return R.ok();
46
+    }
47
+
48
+    @Operation(summary = "获取流程定义详情")
49
+    @GetMapping("/{id}")
50
+    public R<BpmProcessDefinition> detail(@PathVariable Long id) {
51
+        return R.ok(definitionService.getById(id));
52
+    }
53
+
54
+    @Operation(summary = "查询流程定义列表")
55
+    @GetMapping("/list")
56
+    public R<List<BpmProcessDefinition>> list(
57
+            @RequestParam(required = false) String category,
58
+            @RequestParam(required = false) Integer status) {
59
+        return R.ok(definitionService.list(category, status));
60
+    }
61
+
62
+    @Operation(summary = "查询已发布的流程定义")
63
+    @GetMapping("/published")
64
+    public R<List<BpmProcessDefinition>> published(@RequestParam(required = false) String category) {
65
+        return R.ok(definitionService.listPublished(category));
66
+    }
67
+
68
+    @Operation(summary = "根据 processKey 查询")
69
+    @GetMapping("/key/{processKey}")
70
+    public R<BpmProcessDefinition> getByKey(@PathVariable String processKey) {
71
+        return R.ok(definitionService.getByProcessKey(processKey));
72
+    }
73
+
74
+    @Operation(summary = "查询所有分类")
75
+    @GetMapping("/categories")
76
+    public R<List<String>> categories() {
77
+        return R.ok(definitionService.listCategories());
78
+    }
79
+
80
+    // ==================== 发布/停用 ====================
81
+
82
+    @Operation(summary = "发布流程定义")
83
+    @PostMapping("/{id}/publish")
84
+    public R<BpmProcessDefinition> publish(@PathVariable Long id) {
85
+        return R.ok(definitionService.publish(id));
86
+    }
87
+
88
+    @Operation(summary = "停用流程定义")
89
+    @PostMapping("/{id}/deactivate")
90
+    public R<BpmProcessDefinition> deactivate(@PathVariable Long id) {
91
+        return R.ok(definitionService.deactivate(id));
92
+    }
93
+
94
+    // ==================== 版本管理 ====================
95
+
96
+    @Operation(summary = "创建新版本")
97
+    @PostMapping("/{id}/new-version")
98
+    public R<BpmProcessDefinition> newVersion(@PathVariable Long id) {
99
+        return R.ok(definitionService.createNewVersion(id));
100
+    }
101
+
102
+    // ==================== 节点管理 ====================
103
+
104
+    @Operation(summary = "获取流程节点列表")
105
+    @GetMapping("/{id}/nodes")
106
+    public R<List<BpmProcessNode>> nodes(@PathVariable Long id) {
107
+        return R.ok(definitionService.getNodes(id));
108
+    }
109
+
110
+    @Operation(summary = "保存流程节点")
111
+    @PostMapping("/node")
112
+    public R<BpmProcessNode> saveNode(@RequestBody BpmProcessNode node) {
113
+        return R.ok(definitionService.saveNode(node));
114
+    }
115
+
116
+    @Operation(summary = "删除流程节点")
117
+    @DeleteMapping("/node/{nodeId}")
118
+    public R<Void> deleteNode(@PathVariable Long nodeId) {
119
+        definitionService.deleteNode(nodeId);
120
+        return R.ok();
121
+    }
122
+}

+ 200
- 0
wm-bpm/src/main/java/com/water/bpm/controller/ProcessEngineController.java Zobrazit soubor

1
+package com.water.bpm.controller;
2
+
3
+import com.water.bpm.entity.BpmApprovalRecord;
4
+import com.water.bpm.entity.BpmProcessInstance;
5
+import com.water.bpm.entity.BpmTodoTask;
6
+import com.water.bpm.entity.dto.ApprovalRequest;
7
+import com.water.bpm.entity.dto.ProcessInstanceQuery;
8
+import com.water.bpm.entity.dto.ProcessStartRequest;
9
+import com.water.bpm.service.ProcessEngine;
10
+import com.water.common.core.result.R;
11
+import io.swagger.v3.oas.annotations.Operation;
12
+import io.swagger.v3.oas.annotations.tags.Tag;
13
+import lombok.RequiredArgsConstructor;
14
+import org.springframework.web.bind.annotation.*;
15
+
16
+import java.util.List;
17
+import java.util.Map;
18
+
19
+/**
20
+ * 流程引擎 REST API
21
+ * 提供流程启动、审批、驳回、转办、会签、待办/已办查询等接口
22
+ */
23
+@Tag(name = "流程引擎")
24
+@RestController
25
+@RequestMapping("/api/bpm/engine")
26
+@RequiredArgsConstructor
27
+public class ProcessEngineController {
28
+
29
+    private final ProcessEngine processEngine;
30
+
31
+    // ==================== 启动流程 ====================
32
+
33
+    @Operation(summary = "启动流程")
34
+    @PostMapping("/start")
35
+    public R<BpmProcessInstance> start(@RequestBody ProcessStartRequest request) {
36
+        // TODO: 从 Sa-Token 获取当前用户信息
37
+        Long userId = 1L;
38
+        String userName = "当前用户";
39
+
40
+        BpmProcessInstance instance = processEngine.startProcess(
41
+                request.getDefinitionId(),
42
+                userId, userName,
43
+                request.getBusinessKey(),
44
+                request.getBusinessType(),
45
+                request.getTitle(),
46
+                request.getFormData(),
47
+                request.getVariables(),
48
+                request.getPriority()
49
+        );
50
+        return R.ok(instance);
51
+    }
52
+
53
+    // ==================== 审批通过 ====================
54
+
55
+    @Operation(summary = "审批通过")
56
+    @PostMapping("/approve")
57
+    public R<BpmProcessInstance> approve(@RequestBody ApprovalRequest request) {
58
+        Long userId = 1L;
59
+        String userName = "当前用户";
60
+
61
+        BpmProcessInstance instance = processEngine.approveTask(
62
+                request.getInstanceUuid(),
63
+                userId, userName,
64
+                request.getNodeId(),
65
+                request.getComment()
66
+        );
67
+        return R.ok(instance);
68
+    }
69
+
70
+    // ==================== 驳回 ====================
71
+
72
+    @Operation(summary = "驳回")
73
+    @PostMapping("/reject")
74
+    public R<BpmProcessInstance> reject(@RequestBody ApprovalRequest request) {
75
+        Long userId = 1L;
76
+        String userName = "当前用户";
77
+
78
+        BpmProcessInstance instance = processEngine.rejectTask(
79
+                request.getInstanceUuid(),
80
+                userId, userName,
81
+                request.getNodeId(),
82
+                request.getComment()
83
+        );
84
+        return R.ok(instance);
85
+    }
86
+
87
+    // ==================== 转办 ====================
88
+
89
+    @Operation(summary = "转办")
90
+    @PostMapping("/transfer")
91
+    public R<BpmProcessInstance> transfer(@RequestBody ApprovalRequest request) {
92
+        Long userId = 1L;
93
+        String userName = "当前用户";
94
+
95
+        BpmProcessInstance instance = processEngine.transferTask(
96
+                request.getInstanceUuid(),
97
+                userId, userName,
98
+                request.getTargetAssigneeId(),
99
+                request.getTargetAssigneeName(),
100
+                request.getNodeId(),
101
+                request.getComment()
102
+        );
103
+        return R.ok(instance);
104
+    }
105
+
106
+    // ==================== 会签 ====================
107
+
108
+    @Operation(summary = "发起会签")
109
+    @PostMapping("/countersign/initiate")
110
+    public R<BpmProcessInstance> initiateCountersign(@RequestBody Map<String, Object> request) {
111
+        Long userId = 1L;
112
+
113
+        @SuppressWarnings("unchecked")
114
+        List<Number> signerIdNums = (List<Number>) request.get("signerIds");
115
+        @SuppressWarnings("unchecked")
116
+        List<String> signerNames = (List<String>) request.get("signerNames");
117
+        String instanceUuid = (String) request.get("instanceUuid");
118
+        String nodeId = (String) request.get("nodeId");
119
+        String comment = (String) request.get("comment");
120
+
121
+        List<Long> signerIds = signerIdNums.stream().map(Number::longValue).toList();
122
+
123
+        BpmProcessInstance instance = processEngine.initiateCountersign(
124
+                instanceUuid, userId, signerIds, signerNames, nodeId, comment
125
+        );
126
+        return R.ok(instance);
127
+    }
128
+
129
+    @Operation(summary = "会签审批")
130
+    @PostMapping("/countersign/approve")
131
+    public R<BpmProcessInstance> countersignApprove(@RequestBody ApprovalRequest request) {
132
+        Long userId = 1L;
133
+        String userName = "当前用户";
134
+
135
+        BpmProcessInstance instance = processEngine.countersignApprove(
136
+                request.getInstanceUuid(),
137
+                userId, userName,
138
+                request.getNodeId(),
139
+                request.getComment()
140
+        );
141
+        return R.ok(instance);
142
+    }
143
+
144
+    // ==================== 待办/已办查询 ====================
145
+
146
+    @Operation(summary = "待办列表")
147
+    @GetMapping("/todo/list")
148
+    public R<List<BpmTodoTask>> todoList() {
149
+        Long userId = 1L;
150
+        return R.ok(processEngine.getTodoTasks(userId));
151
+    }
152
+
153
+    @Operation(summary = "待办数量")
154
+    @GetMapping("/todo/count")
155
+    public R<Integer> todoCount() {
156
+        Long userId = 1L;
157
+        return R.ok(processEngine.getTodoCount(userId));
158
+    }
159
+
160
+    @Operation(summary = "已办列表")
161
+    @GetMapping("/done/list")
162
+    public R<List<BpmTodoTask>> doneList() {
163
+        Long userId = 1L;
164
+        return R.ok(processEngine.getDoneTasks(userId));
165
+    }
166
+
167
+    // ==================== 流程实例查询 ====================
168
+
169
+    @Operation(summary = "查询流程实例详情")
170
+    @GetMapping("/instance/{instanceUuid}")
171
+    public R<BpmProcessInstance> instanceDetail(@PathVariable String instanceUuid) {
172
+        return R.ok(processEngine.getInstance(instanceUuid));
173
+    }
174
+
175
+    @Operation(summary = "查询我发起的流程")
176
+    @GetMapping("/instance/my-initiated")
177
+    public R<List<BpmProcessInstance>> myInitiated() {
178
+        Long userId = 1L;
179
+        return R.ok(processEngine.getMyInitiated(userId));
180
+    }
181
+
182
+    @Operation(summary = "条件查询流程实例")
183
+    @GetMapping("/instance/list")
184
+    public R<List<BpmProcessInstance>> instanceList(ProcessInstanceQuery query) {
185
+        List<BpmProcessInstance> list = processEngine.queryInstances(
186
+                query.getProcessKey(), query.getStatus(),
187
+                query.getInitiatorId(), query.getBusinessKey(),
188
+                query.getPageNum(), query.getPageSize()
189
+        );
190
+        return R.ok(list);
191
+    }
192
+
193
+    // ==================== 审批记录 ====================
194
+
195
+    @Operation(summary = "查询审批记录")
196
+    @GetMapping("/records/{instanceUuid}")
197
+    public R<List<BpmApprovalRecord>> approvalRecords(@PathVariable String instanceUuid) {
198
+        return R.ok(processEngine.getApprovalRecordsByUuid(instanceUuid));
199
+    }
200
+}

+ 12
- 0
wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessInstanceMapper.java Zobrazit soubor

40
      */
40
      */
41
     @Select("SELECT * FROM bpm_process_instance WHERE initiator_id = #{userId} AND deleted = 0 ORDER BY created_at DESC")
41
     @Select("SELECT * FROM bpm_process_instance WHERE initiator_id = #{userId} AND deleted = 0 ORDER BY created_at DESC")
42
     List<BpmProcessInstance> selectMyInitiated(@Param("userId") Long userId);
42
     List<BpmProcessInstance> selectMyInitiated(@Param("userId") Long userId);
43
+
44
+    /**
45
+     * 根据 UUID 查询流程实例
46
+     */
47
+    @Select("SELECT * FROM bpm_process_instance WHERE instance_id = #{instanceUuid} AND deleted = 0")
48
+    BpmProcessInstance selectByInstanceUuid(@Param("instanceUuid") String instanceUuid);
49
+
50
+    /**
51
+     * 查询当前处理人的运行中流程
52
+     */
53
+    @Select("SELECT * FROM bpm_process_instance WHERE current_assignee_id = #{userId} AND status = 'running' AND deleted = 0 ORDER BY created_at DESC")
54
+    List<BpmProcessInstance> selectByCurrentAssignee(@Param("userId") Long userId);
43
 }
55
 }

+ 12
- 0
wm-bpm/src/main/java/com/water/bpm/mapper/BpmTodoTaskMapper.java Zobrazit soubor

39
      */
39
      */
40
     @Select("SELECT * FROM bpm_todo_task WHERE status = 'pending' AND deadline_at < NOW() AND deleted = 0")
40
     @Select("SELECT * FROM bpm_todo_task WHERE status = 'pending' AND deadline_at < NOW() AND deleted = 0")
41
     List<BpmTodoTask> selectTimeoutTasks();
41
     List<BpmTodoTask> selectTimeoutTasks();
42
+
43
+    /**
44
+     * 根据流程实例ID查询待办
45
+     */
46
+    @Select("SELECT * FROM bpm_todo_task WHERE instance_id = #{instanceId} AND status = 'pending' AND deleted = 0")
47
+    List<BpmTodoTask> selectPendingByInstanceId(@Param("instanceId") Long instanceId);
48
+
49
+    /**
50
+     * 根据流程实例UUID查询待办
51
+     */
52
+    @Select("SELECT * FROM bpm_todo_task WHERE instance_uuid = #{instanceUuid} AND status = 'pending' AND deleted = 0")
53
+    List<BpmTodoTask> selectPendingByInstanceUuid(@Param("instanceUuid") String instanceUuid);
42
 }
54
 }

+ 268
- 0
wm-bpm/src/main/java/com/water/bpm/service/ProcessDefinitionService.java Zobrazit soubor

1
+package com.water.bpm.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.bpm.entity.BpmProcessDefinition;
5
+import com.water.bpm.entity.BpmProcessNode;
6
+import com.water.bpm.mapper.BpmProcessDefinitionMapper;
7
+import com.water.bpm.mapper.BpmProcessNodeMapper;
8
+import com.water.common.core.exception.BusinessException;
9
+import lombok.RequiredArgsConstructor;
10
+import lombok.extern.slf4j.Slf4j;
11
+import org.springframework.stereotype.Service;
12
+import org.springframework.transaction.annotation.Transactional;
13
+
14
+import java.time.LocalDateTime;
15
+import java.util.List;
16
+
17
+/**
18
+ * 流程定义管理服务
19
+ * 支持:CRUD、发布、停用、版本管理
20
+ */
21
+@Slf4j
22
+@Service
23
+@RequiredArgsConstructor
24
+public class ProcessDefinitionService {
25
+
26
+    private final BpmProcessDefinitionMapper definitionMapper;
27
+    private final BpmProcessNodeMapper processNodeMapper;
28
+
29
+    /**
30
+     * 创建流程定义(草稿)
31
+     */
32
+    @Transactional
33
+    public BpmProcessDefinition create(BpmProcessDefinition definition) {
34
+        // 校验 processKey 唯一性
35
+        BpmProcessDefinition existing = definitionMapper.selectByProcessKey(definition.getProcessKey());
36
+        if (existing != null) {
37
+            throw new BusinessException("流程标识 " + definition.getProcessKey() + " 已存在");
38
+        }
39
+
40
+        definition.setStatus(0); // 草稿
41
+        definition.setVersion(1);
42
+        definition.setCreatedAt(LocalDateTime.now());
43
+        definition.setUpdatedAt(LocalDateTime.now());
44
+        definitionMapper.insert(definition);
45
+
46
+        log.info("流程定义已创建: key={}, name={}", definition.getProcessKey(), definition.getProcessName());
47
+        return definition;
48
+    }
49
+
50
+    /**
51
+     * 更新流程定义(仅草稿状态可修改)
52
+     */
53
+    @Transactional
54
+    public BpmProcessDefinition update(Long id, BpmProcessDefinition definition) {
55
+        BpmProcessDefinition existing = definitionMapper.selectById(id);
56
+        if (existing == null) {
57
+            throw new BusinessException("流程定义不存在");
58
+        }
59
+        if (existing.getStatus() != 0) {
60
+            throw new BusinessException("已发布的流程定义不可修改,请先停用后创建新版本");
61
+        }
62
+
63
+        definition.setId(id);
64
+        definition.setUpdatedAt(LocalDateTime.now());
65
+        definitionMapper.updateById(definition);
66
+
67
+        log.info("流程定义已更新: id={}, name={}", id, definition.getProcessName());
68
+        return definitionMapper.selectById(id);
69
+    }
70
+
71
+    /**
72
+     * 发布流程定义
73
+     */
74
+    @Transactional
75
+    public BpmProcessDefinition publish(Long id) {
76
+        BpmProcessDefinition existing = definitionMapper.selectById(id);
77
+        if (existing == null) {
78
+            throw new BusinessException("流程定义不存在");
79
+        }
80
+        if (existing.getStatus() == 1) {
81
+            throw new BusinessException("流程定义已发布");
82
+        }
83
+
84
+        existing.setStatus(1);
85
+        existing.setPublishedAt(LocalDateTime.now());
86
+        existing.setUpdatedAt(LocalDateTime.now());
87
+        definitionMapper.updateById(existing);
88
+
89
+        log.info("流程定义已发布: id={}, key={}, version={}",
90
+                id, existing.getProcessKey(), existing.getVersion());
91
+        return existing;
92
+    }
93
+
94
+    /**
95
+     * 停用流程定义
96
+     */
97
+    @Transactional
98
+    public BpmProcessDefinition deactivate(Long id) {
99
+        BpmProcessDefinition existing = definitionMapper.selectById(id);
100
+        if (existing == null) {
101
+            throw new BusinessException("流程定义不存在");
102
+        }
103
+        if (existing.getStatus() != 1) {
104
+            throw new BusinessException("只有已发布的流程定义可以停用");
105
+        }
106
+
107
+        existing.setStatus(2);
108
+        existing.setUpdatedAt(LocalDateTime.now());
109
+        definitionMapper.updateById(existing);
110
+
111
+        log.info("流程定义已停用: id={}, key={}", id, existing.getProcessKey());
112
+        return existing;
113
+    }
114
+
115
+    /**
116
+     * 创建新版本(基于现有定义)
117
+     */
118
+    @Transactional
119
+    public BpmProcessDefinition createNewVersion(Long id) {
120
+        BpmProcessDefinition existing = definitionMapper.selectById(id);
121
+        if (existing == null) {
122
+            throw new BusinessException("流程定义不存在");
123
+        }
124
+
125
+        // 停用旧版本(如果还在使用中)
126
+        if (existing.getStatus() == 1) {
127
+            existing.setStatus(2);
128
+            existing.setUpdatedAt(LocalDateTime.now());
129
+            definitionMapper.updateById(existing);
130
+        }
131
+
132
+        // 创建新版本
133
+        BpmProcessDefinition newVersion = new BpmProcessDefinition();
134
+        newVersion.setProcessKey(existing.getProcessKey());
135
+        newVersion.setProcessName(existing.getProcessName());
136
+        newVersion.setDescription(existing.getDescription());
137
+        newVersion.setBpmnXml(existing.getBpmnXml());
138
+        newVersion.setFormSchema(existing.getFormSchema());
139
+        newVersion.setCategory(existing.getCategory());
140
+        newVersion.setVersion(existing.getVersion() + 1);
141
+        newVersion.setStatus(0); // 草稿
142
+        newVersion.setCreatedBy(existing.getCreatedBy());
143
+        newVersion.setIcon(existing.getIcon());
144
+        newVersion.setSortOrder(existing.getSortOrder());
145
+        newVersion.setTenantId(existing.getTenantId());
146
+        newVersion.setCreatedAt(LocalDateTime.now());
147
+        newVersion.setUpdatedAt(LocalDateTime.now());
148
+        definitionMapper.insert(newVersion);
149
+
150
+        // 复制节点配置
151
+        List<BpmProcessNode> nodes = processNodeMapper.selectByDefinitionId(id);
152
+        for (BpmProcessNode node : nodes) {
153
+            BpmProcessNode newNode = new BpmProcessNode();
154
+            newNode.setDefinitionId(newVersion.getId());
155
+            newNode.setNodeId(node.getNodeId());
156
+            newNode.setNodeName(node.getNodeName());
157
+            newNode.setNodeType(node.getNodeType());
158
+            newNode.setAssigneeType(node.getAssigneeType());
159
+            newNode.setAssigneeValue(node.getAssigneeValue());
160
+            newNode.setAssigneeName(node.getAssigneeName());
161
+            newNode.setMultiInstanceType(node.getMultiInstanceType());
162
+            newNode.setCountersignRate(node.getCountersignRate());
163
+            newNode.setTimeoutHours(node.getTimeoutHours());
164
+            newNode.setTimeoutAction(node.getTimeoutAction());
165
+            newNode.setFormPermission(node.getFormPermission());
166
+            newNode.setConditionExpression(node.getConditionExpression());
167
+            newNode.setSortOrder(node.getSortOrder());
168
+            newNode.setTenantId(node.getTenantId());
169
+            newNode.setCreatedAt(LocalDateTime.now());
170
+            newNode.setUpdatedAt(LocalDateTime.now());
171
+            processNodeMapper.insert(newNode);
172
+        }
173
+
174
+        log.info("新版本已创建: key={}, oldVersion={}, newVersion={}",
175
+                existing.getProcessKey(), existing.getVersion(), newVersion.getVersion());
176
+        return newVersion;
177
+    }
178
+
179
+    /**
180
+     * 根据ID获取流程定义
181
+     */
182
+    public BpmProcessDefinition getById(Long id) {
183
+        BpmProcessDefinition definition = definitionMapper.selectById(id);
184
+        if (definition == null) {
185
+            throw new BusinessException("流程定义不存在");
186
+        }
187
+        return definition;
188
+    }
189
+
190
+    /**
191
+     * 根据 processKey 获取最新版本
192
+     */
193
+    public BpmProcessDefinition getByProcessKey(String processKey) {
194
+        return definitionMapper.selectByProcessKey(processKey);
195
+    }
196
+
197
+    /**
198
+     * 查询所有流程定义
199
+     */
200
+    public List<BpmProcessDefinition> list(String category, Integer status) {
201
+        LambdaQueryWrapper<BpmProcessDefinition> wrapper = new LambdaQueryWrapper<>();
202
+        wrapper.eq(category != null && !category.isEmpty(), BpmProcessDefinition::getCategory, category)
203
+                .eq(status != null, BpmProcessDefinition::getStatus, status)
204
+                .orderByDesc(BpmProcessDefinition::getCreatedAt);
205
+        return definitionMapper.selectList(wrapper);
206
+    }
207
+
208
+    /**
209
+     * 查询已发布的流程定义
210
+     */
211
+    public List<BpmProcessDefinition> listPublished(String category) {
212
+        return definitionMapper.selectByCategory(category);
213
+    }
214
+
215
+    /**
216
+     * 查询所有分类
217
+     */
218
+    public List<String> listCategories() {
219
+        return definitionMapper.selectAllCategories();
220
+    }
221
+
222
+    /**
223
+     * 获取流程定义的节点列表
224
+     */
225
+    public List<BpmProcessNode> getNodes(Long definitionId) {
226
+        return processNodeMapper.selectByDefinitionId(definitionId);
227
+    }
228
+
229
+    /**
230
+     * 保存/更新流程节点
231
+     */
232
+    @Transactional
233
+    public BpmProcessNode saveNode(BpmProcessNode node) {
234
+        if (node.getId() != null) {
235
+            node.setUpdatedAt(LocalDateTime.now());
236
+            processNodeMapper.updateById(node);
237
+        } else {
238
+            node.setCreatedAt(LocalDateTime.now());
239
+            node.setUpdatedAt(LocalDateTime.now());
240
+            processNodeMapper.insert(node);
241
+        }
242
+        return node;
243
+    }
244
+
245
+    /**
246
+     * 删除流程节点
247
+     */
248
+    @Transactional
249
+    public void deleteNode(Long nodeId) {
250
+        processNodeMapper.deleteById(nodeId);
251
+    }
252
+
253
+    /**
254
+     * 删除流程定义(逻辑删除)
255
+     */
256
+    @Transactional
257
+    public void delete(Long id) {
258
+        BpmProcessDefinition existing = definitionMapper.selectById(id);
259
+        if (existing == null) {
260
+            throw new BusinessException("流程定义不存在");
261
+        }
262
+        if (existing.getStatus() == 1) {
263
+            throw new BusinessException("已发布的流程定义不可删除,请先停用");
264
+        }
265
+        definitionMapper.deleteById(id);
266
+        log.info("流程定义已删除: id={}, key={}", id, existing.getProcessKey());
267
+    }
268
+}

+ 642
- 50
wm-bpm/src/main/java/com/water/bpm/service/ProcessEngine.java Zobrazit soubor

1
 package com.water.bpm.service;
1
 package com.water.bpm.service;
2
 
2
 
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
5
+import com.fasterxml.jackson.core.JsonProcessingException;
6
+import com.fasterxml.jackson.core.type.TypeReference;
7
+import com.fasterxml.jackson.databind.ObjectMapper;
3
 import com.water.bpm.entity.*;
8
 import com.water.bpm.entity.*;
9
+import com.water.bpm.mapper.*;
10
+import com.water.common.core.exception.BusinessException;
4
 import lombok.RequiredArgsConstructor;
11
 import lombok.RequiredArgsConstructor;
5
 import lombok.extern.slf4j.Slf4j;
12
 import lombok.extern.slf4j.Slf4j;
6
 import org.springframework.stereotype.Service;
13
 import org.springframework.stereotype.Service;
7
 import org.springframework.transaction.annotation.Transactional;
14
 import org.springframework.transaction.annotation.Transactional;
8
 
15
 
16
+import java.time.LocalDateTime;
17
+import java.time.temporal.ChronoUnit;
9
 import java.util.*;
18
 import java.util.*;
10
-import java.util.concurrent.ConcurrentHashMap;
19
+import java.util.stream.Collectors;
11
 
20
 
21
+/**
22
+ * 流程引擎核心服务
23
+ * 支持:启动流程、审批通过、驳回、转办、会签、待办/已办查询、流程状态流转
24
+ */
12
 @Slf4j
25
 @Slf4j
13
 @Service
26
 @Service
14
 @RequiredArgsConstructor
27
 @RequiredArgsConstructor
15
 public class ProcessEngine {
28
 public class ProcessEngine {
16
 
29
 
17
-    // 简化流程引擎:模拟 Camunda/Flowable 核心功能
18
-    private final Map<String, BpmProcessInstance> instances = new ConcurrentHashMap<>();
19
-    private final List<BpmApprovalRecord> approvalRecords = new ArrayList<>();
30
+    private final BpmProcessDefinitionMapper definitionMapper;
31
+    private final BpmProcessInstanceMapper instanceMapper;
32
+    private final BpmApprovalRecordMapper approvalRecordMapper;
33
+    private final BpmTodoTaskMapper todoTaskMapper;
34
+    private final BpmProcessNodeMapper processNodeMapper;
35
+    private final ObjectMapper objectMapper;
20
 
36
 
21
-    /** 创建流程实例 */
37
+    // ==================== 常量 ====================
38
+
39
+    public static final String STATUS_DRAFT = "draft";
40
+    public static final String STATUS_RUNNING = "running";
41
+    public static final String STATUS_COMPLETED = "completed";
42
+    public static final String STATUS_REJECTED = "rejected";
43
+    public static final String STATUS_TERMINATED = "terminated";
44
+    public static final String STATUS_SUSPENDED = "suspended";
45
+
46
+    public static final String ACTION_APPROVE = "approve";
47
+    public static final String ACTION_REJECT = "reject";
48
+    public static final String ACTION_TRANSFER = "transfer";
49
+    public static final String ACTION_DELEGATE = "delegate";
50
+    public static final String ACTION_COUNTERSIGN = "countersign";
51
+    public static final String ACTION_BACK = "back";
52
+
53
+    public static final String TODO_PENDING = "pending";
54
+    public static final String TODO_COMPLETED = "completed";
55
+    public static final String TODO_TRANSFERRED = "transferred";
56
+    public static final String TODO_CANCELLED = "cancelled";
57
+
58
+    // ==================== 启动流程 ====================
59
+
60
+    /**
61
+     * 启动流程(向后兼容旧接口)
62
+     */
22
     @Transactional
63
     @Transactional
23
     public BpmProcessInstance startProcess(BpmProcessDefinition definition, Long initiatorId,
64
     public BpmProcessInstance startProcess(BpmProcessDefinition definition, Long initiatorId,
24
                                             String initiatorName, String businessKey,
65
                                             String initiatorName, String businessKey,
25
                                             Map<String, Object> formData) {
66
                                             Map<String, Object> formData) {
67
+        return startProcess(definition.getId(), initiatorId, initiatorName,
68
+                businessKey, null, null, formData, null, 0);
69
+    }
70
+
71
+    /**
72
+     * 启动流程(完整版)
73
+     *
74
+     * @param definitionId   流程定义ID
75
+     * @param initiatorId    发起人ID
76
+     * @param initiatorName  发起人姓名
77
+     * @param businessKey    业务主键
78
+     * @param businessType   业务类型
79
+     * @param title          流程标题
80
+     * @param formData       表单数据
81
+     * @param variables      流程变量
82
+     * @param priority       优先级
83
+     * @return 流程实例
84
+     */
85
+    @Transactional
86
+    public BpmProcessInstance startProcess(Long definitionId, Long initiatorId, String initiatorName,
87
+                                            String businessKey, String businessType, String title,
88
+                                            Map<String, Object> formData, Map<String, Object> variables,
89
+                                            Integer priority) {
90
+        // 1. 加载并校验流程定义
91
+        BpmProcessDefinition definition = definitionMapper.selectById(definitionId);
92
+        if (definition == null || definition.getStatus() != 1) {
93
+            throw new BusinessException("流程定义不存在或未发布");
94
+        }
95
+
96
+        // 2. 加载流程节点
97
+        List<BpmProcessNode> nodes = processNodeMapper.selectByDefinitionId(definitionId);
98
+        if (nodes == null || nodes.isEmpty()) {
99
+            throw new BusinessException("流程定义无节点配置");
100
+        }
101
+
102
+        // 3. 找到起始节点
103
+        BpmProcessNode startNode = findStartNode(nodes);
104
+
105
+        // 4. 创建流程实例
26
         BpmProcessInstance instance = new BpmProcessInstance();
106
         BpmProcessInstance instance = new BpmProcessInstance();
27
-        instance.setId(System.currentTimeMillis());
28
         instance.setInstanceId(UUID.randomUUID().toString());
107
         instance.setInstanceId(UUID.randomUUID().toString());
29
-        instance.setDefinitionId(definition.getId());
108
+        instance.setDefinitionId(definitionId);
30
         instance.setProcessKey(definition.getProcessKey());
109
         instance.setProcessKey(definition.getProcessKey());
31
-        instance.setTitle(definition.getProcessName());
110
+        instance.setProcessName(definition.getProcessName());
32
         instance.setBusinessKey(businessKey);
111
         instance.setBusinessKey(businessKey);
112
+        instance.setBusinessType(businessType);
113
+        instance.setTitle(title != null ? title : definition.getProcessName() + " - " + initiatorName);
33
         instance.setInitiatorId(initiatorId);
114
         instance.setInitiatorId(initiatorId);
34
         instance.setInitiatorName(initiatorName);
115
         instance.setInitiatorName(initiatorName);
35
-        instance.setStatus("running");
36
-        instance.setCurrentNode("START");
37
-        instance.setFormData(formData);
38
-        instance.setStartedAt(java.time.LocalDateTime.now());
39
-        instance.setCreatedAt(java.time.LocalDateTime.now());
40
-        instances.put(instance.getInstanceId(), instance);
41
-        log.info("Process started: {} - {}", instance.getProcessKey(), instance.getTitle());
116
+        instance.setStatus(STATUS_RUNNING);
117
+        instance.setCurrentNodeId(startNode.getNodeId());
118
+        instance.setCurrentNodeName(startNode.getNodeName());
119
+        instance.setPriority(priority != null ? priority : 0);
120
+        instance.setFormData(toJson(formData));
121
+        instance.setVariables(toJson(variables));
122
+        instance.setStartedAt(LocalDateTime.now());
123
+        instance.setCreatedAt(LocalDateTime.now());
124
+        instance.setUpdatedAt(LocalDateTime.now());
125
+        instanceMapper.insert(instance);
126
+
127
+        // 5. 确定起始节点审批人并创建待办
128
+        Long assigneeId = resolveAssignee(startNode, initiatorId);
129
+        String assigneeName = startNode.getAssigneeName();
130
+        createTodoTask(instance, startNode, assigneeId, assigneeName);
131
+
132
+        // 6. 更新实例当前处理人
133
+        instance.setCurrentAssigneeId(assigneeId);
134
+        instance.setCurrentAssigneeName(assigneeName);
135
+        instanceMapper.updateById(instance);
136
+
137
+        // 7. 记录启动审批记录
138
+        BpmApprovalRecord startRecord = new BpmApprovalRecord();
139
+        startRecord.setInstanceId(instance.getId());
140
+        startRecord.setInstanceUuid(instance.getInstanceId());
141
+        startRecord.setNodeId(startNode.getNodeId());
142
+        startRecord.setNodeName(startNode.getNodeName());
143
+        startRecord.setApproverId(initiatorId);
144
+        startRecord.setApproverName(initiatorName);
145
+        startRecord.setAction("start");
146
+        startRecord.setComment("发起流程");
147
+        startRecord.setApprovedAt(LocalDateTime.now());
148
+        startRecord.setCreatedAt(LocalDateTime.now());
149
+        approvalRecordMapper.insert(startRecord);
150
+
151
+        log.info("流程已启动: key={}, instanceId={}, initiator={}",
152
+                definition.getProcessKey(), instance.getInstanceId(), initiatorName);
42
         return instance;
153
         return instance;
43
     }
154
     }
44
 
155
 
45
-    /** 审批节点 */
156
+    // ==================== 审批通过 ====================
157
+
158
+    /**
159
+     * 审批(向后兼容旧接口)
160
+     */
161
+    @Transactional
162
+    public BpmProcessInstance approve(String instanceUuid, Long approverId, String approverName,
163
+                                       String nodeId, String nodeName,
164
+                                       String action, String comment) {
165
+        switch (action) {
166
+            case ACTION_APPROVE:
167
+                return approveTask(instanceUuid, approverId, approverName, nodeId, comment);
168
+            case ACTION_REJECT:
169
+                return rejectTask(instanceUuid, approverId, approverName, nodeId, comment);
170
+            case ACTION_TRANSFER:
171
+                // 旧接口不支持转办目标人,使用默认
172
+                return approveTask(instanceUuid, approverId, approverName, nodeId, comment);
173
+            default:
174
+                return approveTask(instanceUuid, approverId, approverName, nodeId, comment);
175
+        }
176
+    }
177
+
178
+    /**
179
+     * 审批通过
180
+     */
46
     @Transactional
181
     @Transactional
47
-    public BpmProcessInstance approve(String instanceId, Long approverId, String approverName,
48
-                                        String nodeId, String nodeName,
49
-                                        String action, String comment) {
50
-        BpmProcessInstance instance = instances.get(instanceId);
51
-        if (instance == null) throw new RuntimeException("流程实例不存在");
182
+    public BpmProcessInstance approveTask(String instanceUuid, Long approverId, String approverName,
183
+                                           String nodeId, String comment) {
184
+        BpmProcessInstance instance = getInstanceByUuid(instanceUuid);
185
+        validateRunning(instance);
186
+        validateAssignee(instance, approverId);
52
 
187
 
53
-        BpmApprovalRecord record = new BpmApprovalRecord();
54
-        record.setInstanceId(instance.getId());
55
-        record.setNodeId(nodeId);
56
-        record.setNodeName(nodeName);
57
-        record.setApproverId(approverId);
58
-        record.setApproverName(approverName);
59
-        record.setAction(action);
60
-        record.setComment(comment);
61
-        record.setApprovedAt(java.time.LocalDateTime.now());
62
-        approvalRecords.add(record);
188
+        // 记录审批
189
+        BpmApprovalRecord record = buildApprovalRecord(instance, nodeId, approverId, approverName,
190
+                ACTION_APPROVE, comment);
191
+        approvalRecordMapper.insert(record);
63
 
192
 
64
-        instance.setCurrentNode(nodeName);
65
-        switch (action) {
66
-            case "approve": instance.setStatus("running"); break;
67
-            case "reject": instance.setStatus("rejected"); instance.setCompletedAt(java.time.LocalDateTime.now()); break;
68
-            default: instance.setStatus("running");
193
+        // 完成当前待办
194
+        completeTodoTasks(instance.getId(), approverId);
195
+
196
+        // 流转到下一节点
197
+        List<BpmProcessNode> nodes = processNodeMapper.selectByDefinitionId(instance.getDefinitionId());
198
+        BpmProcessNode currentNode = findNodeById(nodes, nodeId != null ? nodeId : instance.getCurrentNodeId());
199
+        BpmProcessNode nextNode = findNextNode(nodes, currentNode);
200
+
201
+        if (nextNode == null || "end".equals(nextNode.getNodeType())) {
202
+            // 流程结束
203
+            completeInstance(instance);
204
+        } else {
205
+            // 流转到下一节点
206
+            Long nextAssigneeId = resolveAssignee(nextNode, instance.getInitiatorId());
207
+            String nextAssigneeName = nextNode.getAssigneeName();
208
+
209
+            instance.setCurrentNodeId(nextNode.getNodeId());
210
+            instance.setCurrentNodeName(nextNode.getNodeName());
211
+            instance.setCurrentAssigneeId(nextAssigneeId);
212
+            instance.setCurrentAssigneeName(nextAssigneeName);
213
+            instance.setUpdatedAt(LocalDateTime.now());
214
+            instanceMapper.updateById(instance);
215
+
216
+            createTodoTask(instance, nextNode, nextAssigneeId, nextAssigneeName);
217
+        }
218
+
219
+        log.info("审批通过: instanceUuid={}, approver={}, nodeId={}", instanceUuid, approverName, nodeId);
220
+        return instance;
221
+    }
222
+
223
+    // ==================== 驳回 ====================
224
+
225
+    /**
226
+     * 驳回
227
+     */
228
+    @Transactional
229
+    public BpmProcessInstance rejectTask(String instanceUuid, Long approverId, String approverName,
230
+                                          String nodeId, String comment) {
231
+        BpmProcessInstance instance = getInstanceByUuid(instanceUuid);
232
+        validateRunning(instance);
233
+        validateAssignee(instance, approverId);
234
+
235
+        // 记录驳回
236
+        BpmApprovalRecord record = buildApprovalRecord(instance, nodeId, approverId, approverName,
237
+                ACTION_REJECT, comment);
238
+        approvalRecordMapper.insert(record);
239
+
240
+        // 取消所有待办
241
+        cancelTodoTasks(instance.getId());
242
+
243
+        // 更新实例状态
244
+        instance.setStatus(STATUS_REJECTED);
245
+        instance.setCompletedAt(LocalDateTime.now());
246
+        instance.setDurationSeconds(ChronoUnit.SECONDS.between(instance.getStartedAt(), LocalDateTime.now()));
247
+        instance.setUpdatedAt(LocalDateTime.now());
248
+        instanceMapper.updateById(instance);
249
+
250
+        log.info("审批驳回: instanceUuid={}, approver={}, reason={}", instanceUuid, approverName, comment);
251
+        return instance;
252
+    }
253
+
254
+    // ==================== 转办 ====================
255
+
256
+    /**
257
+     * 转办:将审批权转给其他人,原审批人不再审批
258
+     */
259
+    @Transactional
260
+    public BpmProcessInstance transferTask(String instanceUuid, Long currentApproverId, String currentApproverName,
261
+                                            Long targetAssigneeId, String targetAssigneeName,
262
+                                            String nodeId, String comment) {
263
+        BpmProcessInstance instance = getInstanceByUuid(instanceUuid);
264
+        validateRunning(instance);
265
+        validateAssignee(instance, currentApproverId);
266
+
267
+        if (targetAssigneeId == null || targetAssigneeId.equals(currentApproverId)) {
268
+            throw new BusinessException("转办目标人不能为空或与当前审批人相同");
269
+        }
270
+
271
+        // 记录转办
272
+        BpmApprovalRecord record = buildApprovalRecord(instance, nodeId, currentApproverId, currentApproverName,
273
+                ACTION_TRANSFER, comment);
274
+        record.setTargetAssigneeId(targetAssigneeId);
275
+        record.setTargetAssigneeName(targetAssigneeName);
276
+        approvalRecordMapper.insert(record);
277
+
278
+        // 关闭当前待办(标记为转办)
279
+        List<BpmTodoTask> currentTodos = todoTaskMapper.selectPendingByUserId(currentApproverId);
280
+        for (BpmTodoTask todo : currentTodos) {
281
+            if (todo.getInstanceId().equals(instance.getId())) {
282
+                todo.setStatus(TODO_TRANSFERRED);
283
+                todo.setCompletedAt(LocalDateTime.now());
284
+                todo.setUpdatedAt(LocalDateTime.now());
285
+                todoTaskMapper.updateById(todo);
286
+            }
287
+        }
288
+
289
+        // 创建新的待办给目标人
290
+        List<BpmProcessNode> nodes = processNodeMapper.selectByDefinitionId(instance.getDefinitionId());
291
+        BpmProcessNode currentNode = findNodeById(nodes, nodeId != null ? nodeId : instance.getCurrentNodeId());
292
+        createTodoTask(instance, currentNode, targetAssigneeId, targetAssigneeName);
293
+
294
+        // 更新实例当前处理人
295
+        instance.setCurrentAssigneeId(targetAssigneeId);
296
+        instance.setCurrentAssigneeName(targetAssigneeName);
297
+        instance.setUpdatedAt(LocalDateTime.now());
298
+        instanceMapper.updateById(instance);
299
+
300
+        log.info("任务转办: instanceUuid={}, from={} to={}",
301
+                instanceUuid, currentApproverName, targetAssigneeName);
302
+        return instance;
303
+    }
304
+
305
+    // ==================== 会签 ====================
306
+
307
+    /**
308
+     * 发起会签:多人审批,全部通过才继续流转
309
+     */
310
+    @Transactional
311
+    public BpmProcessInstance initiateCountersign(String instanceUuid, Long initiatorId,
312
+                                                    List<Long> signerIds, List<String> signerNames,
313
+                                                    String nodeId, String comment) {
314
+        BpmProcessInstance instance = getInstanceByUuid(instanceUuid);
315
+        validateRunning(instance);
316
+
317
+        if (signerIds == null || signerIds.size() < 2) {
318
+            throw new BusinessException("会签至少需要2个审批人");
319
+        }
320
+
321
+        // 记录会签发起
322
+        BpmApprovalRecord record = buildApprovalRecord(instance, nodeId, initiatorId,
323
+                null, ACTION_COUNTERSIGN, comment);
324
+        record.setCountersignTotal(signerIds.size());
325
+        record.setCountersignApproved(0);
326
+        record.setCountersignResult("all");
327
+        approvalRecordMapper.insert(record);
328
+
329
+        // 关闭当前待办
330
+        cancelTodoTasks(instance.getId());
331
+
332
+        // 为每个会签人创建待办
333
+        List<BpmProcessNode> nodes = processNodeMapper.selectByDefinitionId(instance.getDefinitionId());
334
+        BpmProcessNode currentNode = findNodeById(nodes, nodeId != null ? nodeId : instance.getCurrentNodeId());
335
+
336
+        for (int i = 0; i < signerIds.size(); i++) {
337
+            Long signerId = signerIds.get(i);
338
+            String signerName = (signerNames != null && i < signerNames.size()) ? signerNames.get(i) : "审批人" + (i + 1);
339
+            createTodoTask(instance, currentNode, signerId, signerName);
340
+        }
341
+
342
+        // 更新实例(当前处理人设为第一个会签人作为代表)
343
+        instance.setCurrentAssigneeId(signerIds.get(0));
344
+        instance.setCurrentAssigneeName(signerNames != null && !signerNames.isEmpty() ? signerNames.get(0) : "会签中");
345
+        instance.setUpdatedAt(LocalDateTime.now());
346
+        instanceMapper.updateById(instance);
347
+
348
+        log.info("会签发起: instanceUuid={}, signers={}", instanceUuid, signerNames);
349
+        return instance;
350
+    }
351
+
352
+    /**
353
+     * 会签审批(单个会签人审批通过)
354
+     */
355
+    @Transactional
356
+    public BpmProcessInstance countersignApprove(String instanceUuid, Long signerId, String signerName,
357
+                                                   String nodeId, String comment) {
358
+        BpmProcessInstance instance = getInstanceByUuid(instanceUuid);
359
+        validateRunning(instance);
360
+
361
+        // 校验该会签人是否有待办
362
+        List<BpmTodoTask> signerTodos = todoTaskMapper.selectPendingByUserId(signerId);
363
+        boolean hasTodo = signerTodos.stream()
364
+                .anyMatch(t -> t.getInstanceId().equals(instance.getId()));
365
+        if (!hasTodo) {
366
+            throw new BusinessException("该审批人无此任务的审批权限");
367
+        }
368
+
369
+        // 记录会签审批
370
+        BpmApprovalRecord record = buildApprovalRecord(instance, nodeId, signerId, signerName,
371
+                ACTION_APPROVE, comment);
372
+        approvalRecordMapper.insert(record);
373
+
374
+        // 完成该会签人的待办
375
+        completeTodoTasks(instance.getId(), signerId);
376
+
377
+        // 检查是否所有会签人都已审批
378
+        List<BpmTodoTask> remainingTodos = todoTaskMapper.selectPendingByInstanceId(instance.getId());
379
+        if (remainingTodos.isEmpty()) {
380
+            // 所有会签人完成,流转到下一节点
381
+            List<BpmProcessNode> nodes = processNodeMapper.selectByDefinitionId(instance.getDefinitionId());
382
+            BpmProcessNode currentNode = findNodeById(nodes, nodeId != null ? nodeId : instance.getCurrentNodeId());
383
+            BpmProcessNode nextNode = findNextNode(nodes, currentNode);
384
+
385
+            if (nextNode == null || "end".equals(nextNode.getNodeType())) {
386
+                completeInstance(instance);
387
+            } else {
388
+                Long nextAssigneeId = resolveAssignee(nextNode, instance.getInitiatorId());
389
+                String nextAssigneeName = nextNode.getAssigneeName();
390
+
391
+                instance.setCurrentNodeId(nextNode.getNodeId());
392
+                instance.setCurrentNodeName(nextNode.getNodeName());
393
+                instance.setCurrentAssigneeId(nextAssigneeId);
394
+                instance.setCurrentAssigneeName(nextAssigneeName);
395
+                instance.setUpdatedAt(LocalDateTime.now());
396
+                instanceMapper.updateById(instance);
397
+
398
+                createTodoTask(instance, nextNode, nextAssigneeId, nextAssigneeName);
399
+            }
400
+
401
+            log.info("会签完成: instanceUuid={}", instanceUuid);
402
+        } else {
403
+            log.info("会签进行中: instanceUuid={}, remaining={}", instanceUuid, remainingTodos.size());
69
         }
404
         }
70
-        log.info("Approval: {} - {}: {}", instanceId, action, comment);
405
+
71
         return instance;
406
         return instance;
72
     }
407
     }
73
 
408
 
74
-    /** 完成流程 */
409
+    // ==================== 完成流程(向后兼容) ====================
410
+
411
+    /**
412
+     * 完成流程
413
+     */
75
     @Transactional
414
     @Transactional
76
-    public void completeProcess(String instanceId) {
77
-        BpmProcessInstance instance = instances.get(instanceId);
415
+    public void completeProcess(String instanceUuid) {
416
+        BpmProcessInstance instance = getInstanceByUuid(instanceUuid);
78
         if (instance != null) {
417
         if (instance != null) {
79
-            instance.setStatus("completed");
80
-            instance.setCompletedAt(java.time.LocalDateTime.now());
418
+            completeInstance(instance);
81
         }
419
         }
82
     }
420
     }
83
 
421
 
84
-    /** 查询待办 */
422
+    // ==================== 待办/已办查询 ====================
423
+
424
+    /**
425
+     * 查询待办(向后兼容旧接口)
426
+     */
85
     public List<BpmProcessInstance> getTodoList(Long userId) {
427
     public List<BpmProcessInstance> getTodoList(Long userId) {
86
-        return instances.values().stream()
87
-                .filter(i -> "running".equals(i.getStatus()) && i.getInitiatorId().equals(userId))
88
-                .toList();
428
+        List<BpmTodoTask> todos = todoTaskMapper.selectPendingByUserId(userId);
429
+        if (todos.isEmpty()) {
430
+            return Collections.emptyList();
431
+        }
432
+        List<Long> instanceIds = todos.stream()
433
+                .map(BpmTodoTask::getInstanceId)
434
+                .distinct()
435
+                .collect(Collectors.toList());
436
+        if (instanceIds.isEmpty()) {
437
+            return Collections.emptyList();
438
+        }
439
+        return instanceMapper.selectBatchIds(instanceIds);
440
+    }
441
+
442
+    /**
443
+     * 查询待办任务列表(返回任务详情)
444
+     */
445
+    public List<BpmTodoTask> getTodoTasks(Long userId) {
446
+        return todoTaskMapper.selectPendingByUserId(userId);
447
+    }
448
+
449
+    /**
450
+     * 查询已办任务列表
451
+     */
452
+    public List<BpmTodoTask> getDoneTasks(Long userId) {
453
+        return todoTaskMapper.selectDoneByUserId(userId);
454
+    }
455
+
456
+    /**
457
+     * 查询待办数量
458
+     */
459
+    public Integer getTodoCount(Long userId) {
460
+        return todoTaskMapper.countPendingByUserId(userId);
461
+    }
462
+
463
+    /**
464
+     * 查询流程实例(通过UUID)
465
+     */
466
+    public BpmProcessInstance getInstance(String instanceUuid) {
467
+        return getInstanceByUuid(instanceUuid);
468
+    }
469
+
470
+    /**
471
+     * 查询流程实例(通过数据库ID)
472
+     */
473
+    public BpmProcessInstance getInstanceById(Long id) {
474
+        BpmProcessInstance instance = instanceMapper.selectById(id);
475
+        if (instance == null) {
476
+            throw new BusinessException("流程实例不存在");
477
+        }
478
+        return instance;
479
+    }
480
+
481
+    /**
482
+     * 查询我发起的流程
483
+     */
484
+    public List<BpmProcessInstance> getMyInitiated(Long userId) {
485
+        return instanceMapper.selectMyInitiated(userId);
486
+    }
487
+
488
+    /**
489
+     * 查询流程审批记录
490
+     */
491
+    public List<BpmApprovalRecord> getApprovalRecords(Long instanceId) {
492
+        return approvalRecordMapper.selectByInstanceId(instanceId);
493
+    }
494
+
495
+    /**
496
+     * 查询流程审批记录(通过UUID)
497
+     */
498
+    public List<BpmApprovalRecord> getApprovalRecordsByUuid(String instanceUuid) {
499
+        return approvalRecordMapper.selectByInstanceUuid(instanceUuid);
500
+    }
501
+
502
+    /**
503
+     * 条件查询流程实例
504
+     */
505
+    public List<BpmProcessInstance> queryInstances(String processKey, String status,
506
+                                                     Long initiatorId, String businessKey,
507
+                                                     Integer pageNum, Integer pageSize) {
508
+        LambdaQueryWrapper<BpmProcessInstance> wrapper = new LambdaQueryWrapper<>();
509
+        wrapper.eq(processKey != null, BpmProcessInstance::getProcessKey, processKey)
510
+                .eq(status != null, BpmProcessInstance::getStatus, status)
511
+                .eq(initiatorId != null, BpmProcessInstance::getInitiatorId, initiatorId)
512
+                .eq(businessKey != null, BpmProcessInstance::getBusinessKey, businessKey)
513
+                .orderByDesc(BpmProcessInstance::getCreatedAt);
514
+
515
+        if (pageNum != null && pageSize != null) {
516
+            wrapper.last("LIMIT " + pageSize + " OFFSET " + (pageNum - 1) * pageSize);
517
+        }
518
+        return instanceMapper.selectList(wrapper);
519
+    }
520
+
521
+    // ==================== 内部辅助方法 ====================
522
+
523
+    private BpmProcessInstance getInstanceByUuid(String instanceUuid) {
524
+        LambdaQueryWrapper<BpmProcessInstance> wrapper = new LambdaQueryWrapper<>();
525
+        wrapper.eq(BpmProcessInstance::getInstanceId, instanceUuid);
526
+        BpmProcessInstance instance = instanceMapper.selectOne(wrapper);
527
+        if (instance == null) {
528
+            throw new BusinessException("流程实例不存在");
529
+        }
530
+        return instance;
89
     }
531
     }
90
 
532
 
91
-    /** 查询流程实例 */
92
-    public BpmProcessInstance getInstance(String instanceId) {
93
-        return instances.get(instanceId);
533
+    private void validateRunning(BpmProcessInstance instance) {
534
+        if (!STATUS_RUNNING.equals(instance.getStatus())) {
535
+            throw new BusinessException("流程实例不在运行中,当前状态: " + instance.getStatus());
536
+        }
537
+    }
538
+
539
+    private void validateAssignee(BpmProcessInstance instance, Long approverId) {
540
+        if (instance.getCurrentAssigneeId() != null &&
541
+                !instance.getCurrentAssigneeId().equals(approverId)) {
542
+            throw new BusinessException("当前审批人无权操作此任务");
543
+        }
544
+    }
545
+
546
+    private void completeInstance(BpmProcessInstance instance) {
547
+        instance.setStatus(STATUS_COMPLETED);
548
+        instance.setCompletedAt(LocalDateTime.now());
549
+        if (instance.getStartedAt() != null) {
550
+            instance.setDurationSeconds(ChronoUnit.SECONDS.between(instance.getStartedAt(), LocalDateTime.now()));
551
+        }
552
+        instance.setUpdatedAt(LocalDateTime.now());
553
+        instanceMapper.updateById(instance);
554
+
555
+        // 关闭所有待办
556
+        cancelTodoTasks(instance.getId());
557
+    }
558
+
559
+    private BpmProcessNode findStartNode(List<BpmProcessNode> nodes) {
560
+        return nodes.stream()
561
+                .filter(n -> "start".equals(n.getNodeType()))
562
+                .findFirst()
563
+                .orElse(nodes.stream()
564
+                        .min(Comparator.comparingInt(n -> n.getSortOrder() != null ? n.getSortOrder() : 0))
565
+                        .orElseThrow(() -> new BusinessException("无法找到起始节点")));
566
+    }
567
+
568
+    private BpmProcessNode findNextNode(List<BpmProcessNode> nodes, BpmProcessNode currentNode) {
569
+        if (currentNode == null) return null;
570
+        int currentSort = currentNode.getSortOrder() != null ? currentNode.getSortOrder() : 0;
571
+        return nodes.stream()
572
+                .filter(n -> {
573
+                    int sort = n.getSortOrder() != null ? n.getSortOrder() : 0;
574
+                    return sort > currentSort && !"start".equals(n.getNodeType());
575
+                })
576
+                .min(Comparator.comparingInt(n -> n.getSortOrder() != null ? n.getSortOrder() : 0))
577
+                .orElse(null);
578
+    }
579
+
580
+    private BpmProcessNode findNodeById(List<BpmProcessNode> nodes, String nodeId) {
581
+        if (nodeId == null) return null;
582
+        return nodes.stream()
583
+                .filter(n -> nodeId.equals(n.getNodeId()))
584
+                .findFirst()
585
+                .orElse(null);
586
+    }
587
+
588
+    private Long resolveAssignee(BpmProcessNode node, Long fallbackUserId) {
589
+        if (node == null || node.getAssigneeValue() == null || node.getAssigneeValue().isEmpty()) {
590
+            return fallbackUserId;
591
+        }
592
+        try {
593
+            return Long.parseLong(node.getAssigneeValue());
594
+        } catch (NumberFormatException e) {
595
+            return fallbackUserId;
596
+        }
597
+    }
598
+
599
+    private void createTodoTask(BpmProcessInstance instance, BpmProcessNode node,
600
+                                 Long assigneeId, String assigneeName) {
601
+        BpmTodoTask todo = new BpmTodoTask();
602
+        todo.setInstanceId(instance.getId());
603
+        todo.setInstanceUuid(instance.getInstanceId());
604
+        todo.setTitle(instance.getTitle());
605
+        todo.setProcessKey(instance.getProcessKey());
606
+        todo.setProcessName(instance.getProcessName());
607
+        todo.setNodeId(node != null ? node.getNodeId() : "unknown");
608
+        todo.setNodeName(node != null ? node.getNodeName() : "未知节点");
609
+        todo.setAssigneeId(assigneeId);
610
+        todo.setAssigneeName(assigneeName);
611
+        todo.setInitiatorId(instance.getInitiatorId());
612
+        todo.setInitiatorName(instance.getInitiatorName());
613
+        todo.setBusinessKey(instance.getBusinessKey());
614
+        todo.setStatus(TODO_PENDING);
615
+        todo.setPriority(instance.getPriority() != null ? instance.getPriority() : 0);
616
+        todo.setReceivedAt(LocalDateTime.now());
617
+        todo.setIsRead(false);
618
+        todo.setCreatedAt(LocalDateTime.now());
619
+        todo.setUpdatedAt(LocalDateTime.now());
620
+
621
+        // 设置超时时间
622
+        if (node != null && node.getTimeoutHours() != null && node.getTimeoutHours() > 0) {
623
+            todo.setDeadlineAt(LocalDateTime.now().plusHours(node.getTimeoutHours()));
624
+        }
625
+
626
+        todoTaskMapper.insert(todo);
627
+    }
628
+
629
+    private void completeTodoTasks(Long instanceId, Long userId) {
630
+        LambdaUpdateWrapper<BpmTodoTask> wrapper = new LambdaUpdateWrapper<>();
631
+        wrapper.eq(BpmTodoTask::getInstanceId, instanceId)
632
+                .eq(BpmTodoTask::getAssigneeId, userId)
633
+                .eq(BpmTodoTask::getStatus, TODO_PENDING)
634
+                .set(BpmTodoTask::getStatus, TODO_COMPLETED)
635
+                .set(BpmTodoTask::getCompletedAt, LocalDateTime.now())
636
+                .set(BpmTodoTask::getUpdatedAt, LocalDateTime.now());
637
+        todoTaskMapper.update(null, wrapper);
638
+    }
639
+
640
+    private void cancelTodoTasks(Long instanceId) {
641
+        LambdaUpdateWrapper<BpmTodoTask> wrapper = new LambdaUpdateWrapper<>();
642
+        wrapper.eq(BpmTodoTask::getInstanceId, instanceId)
643
+                .eq(BpmTodoTask::getStatus, TODO_PENDING)
644
+                .set(BpmTodoTask::getStatus, TODO_CANCELLED)
645
+                .set(BpmTodoTask::getCompletedAt, LocalDateTime.now())
646
+                .set(BpmTodoTask::getUpdatedAt, LocalDateTime.now());
647
+        todoTaskMapper.update(null, wrapper);
648
+    }
649
+
650
+    private BpmApprovalRecord buildApprovalRecord(BpmProcessInstance instance, String nodeId,
651
+                                                    Long approverId, String approverName,
652
+                                                    String action, String comment) {
653
+        BpmApprovalRecord record = new BpmApprovalRecord();
654
+        record.setInstanceId(instance.getId());
655
+        record.setInstanceUuid(instance.getInstanceId());
656
+        record.setNodeId(nodeId != null ? nodeId : instance.getCurrentNodeId());
657
+        record.setNodeName(instance.getCurrentNodeName());
658
+        record.setApproverId(approverId);
659
+        record.setApproverName(approverName);
660
+        record.setAction(action);
661
+        record.setComment(comment);
662
+        record.setApprovedAt(LocalDateTime.now());
663
+        record.setCreatedAt(LocalDateTime.now());
664
+        return record;
665
+    }
666
+
667
+    private String toJson(Object obj) {
668
+        if (obj == null) return null;
669
+        try {
670
+            return objectMapper.writeValueAsString(obj);
671
+        } catch (JsonProcessingException e) {
672
+            log.warn("JSON序列化失败: {}", e.getMessage());
673
+            return null;
674
+        }
675
+    }
676
+
677
+    @SuppressWarnings("unchecked")
678
+    private Map<String, Object> fromJson(String json) {
679
+        if (json == null || json.isEmpty()) return Collections.emptyMap();
680
+        try {
681
+            return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
682
+        } catch (JsonProcessingException e) {
683
+            log.warn("JSON反序列化失败: {}", e.getMessage());
684
+            return Collections.emptyMap();
685
+        }
94
     }
686
     }
95
-}
687
+}

+ 279
- 0
wm-bpm/src/main/resources/db/V_bpm_core.sql Zobrazit soubor

1
+-- ==========================================
2
+-- BPM 流程引擎核心表结构
3
+-- 数据库: PostgreSQL
4
+-- 版本: V1.0
5
+-- ==========================================
6
+
7
+-- 1. 流程定义表
8
+CREATE TABLE IF NOT EXISTS bpm_process_definition (
9
+    id              BIGSERIAL PRIMARY KEY,
10
+    process_key     VARCHAR(100) NOT NULL,
11
+    process_name    VARCHAR(200) NOT NULL,
12
+    description     TEXT,
13
+    bpmn_xml        TEXT,
14
+    form_schema     TEXT,
15
+    category        VARCHAR(50),
16
+    version         INTEGER NOT NULL DEFAULT 1,
17
+    status          INTEGER NOT NULL DEFAULT 0,          -- 0-草稿 1-已发布 2-已停用
18
+    created_by      VARCHAR(100),
19
+    icon            VARCHAR(200),
20
+    sort_order      INTEGER DEFAULT 0,
21
+    published_at    TIMESTAMP,
22
+    tenant_id       VARCHAR(50),
23
+    deleted         INTEGER NOT NULL DEFAULT 0,
24
+    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
25
+    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
26
+);
27
+
28
+CREATE INDEX IF NOT EXISTS idx_bpm_def_key ON bpm_process_definition(process_key);
29
+CREATE INDEX IF NOT EXISTS idx_bpm_def_category ON bpm_process_definition(category);
30
+CREATE INDEX IF NOT EXISTS idx_bpm_def_status ON bpm_process_definition(status);
31
+CREATE UNIQUE INDEX IF NOT EXISTS uk_bpm_def_key_version ON bpm_process_definition(process_key, version) WHERE deleted = 0;
32
+
33
+COMMENT ON TABLE bpm_process_definition IS '流程定义表';
34
+COMMENT ON COLUMN bpm_process_definition.process_key IS '流程唯一标识';
35
+COMMENT ON COLUMN bpm_process_definition.status IS '状态: 0-草稿 1-已发布 2-已停用';
36
+
37
+-- 2. 流程节点定义表
38
+CREATE TABLE IF NOT EXISTS bpm_process_node (
39
+    id                      BIGSERIAL PRIMARY KEY,
40
+    definition_id           BIGINT NOT NULL REFERENCES bpm_process_definition(id),
41
+    node_id                 VARCHAR(100) NOT NULL,
42
+    node_name               VARCHAR(200) NOT NULL,
43
+    node_type               VARCHAR(50) NOT NULL,        -- start/end/userTask/serviceTask/gateway/subprocess/timer
44
+    assignee_type           VARCHAR(50),                 -- role/user/department/position/initiator
45
+    assignee_value          VARCHAR(200),
46
+    assignee_name           VARCHAR(100),
47
+    multi_instance_type     VARCHAR(50),                 -- sequential(会签)/parallel(或签)/countersign(比例签)
48
+    countersign_rate        INTEGER,
49
+    timeout_hours           INTEGER,
50
+    timeout_action          VARCHAR(50),                 -- remind/transfer/escalate/auto_approve
51
+    form_permission         TEXT,
52
+    condition_expression    VARCHAR(500),
53
+    sort_order              INTEGER DEFAULT 0,
54
+    tenant_id               VARCHAR(50),
55
+    deleted                 INTEGER NOT NULL DEFAULT 0,
56
+    created_at              TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
57
+    updated_at              TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
58
+);
59
+
60
+CREATE INDEX IF NOT EXISTS idx_bpm_node_def ON bpm_process_node(definition_id);
61
+CREATE INDEX IF NOT EXISTS idx_bpm_node_type ON bpm_process_node(node_type);
62
+
63
+COMMENT ON TABLE bpm_process_node IS '流程节点定义表';
64
+COMMENT ON COLUMN bpm_process_node.node_type IS '节点类型: start/end/userTask/serviceTask/gateway/subprocess/timer';
65
+COMMENT ON COLUMN bpm_process_node.multi_instance_type IS '多人审批方式: sequential(会签)/parallel(或签)/countersign(比例签)';
66
+
67
+-- 3. 流程实例表
68
+CREATE TABLE IF NOT EXISTS bpm_process_instance (
69
+    id                          BIGSERIAL PRIMARY KEY,
70
+    instance_id                 VARCHAR(64) NOT NULL,
71
+    definition_id               BIGINT NOT NULL REFERENCES bpm_process_definition(id),
72
+    process_key                 VARCHAR(100) NOT NULL,
73
+    process_name                VARCHAR(200),
74
+    business_key                VARCHAR(200),
75
+    business_type               VARCHAR(100),
76
+    title                       VARCHAR(500),
77
+    initiator_id                BIGINT NOT NULL,
78
+    initiator_name              VARCHAR(100),
79
+    current_node_id             VARCHAR(100),
80
+    current_node_name           VARCHAR(200),
81
+    current_assignee_id         BIGINT,
82
+    current_assignee_name       VARCHAR(100),
83
+    status                      VARCHAR(50) NOT NULL DEFAULT 'running',  -- running/completed/terminated/rejected/suspended
84
+    priority                    INTEGER DEFAULT 0,                       -- 0-普通 1-紧急 2-特急
85
+    variables                   TEXT,
86
+    form_data                   TEXT,
87
+    started_at                  TIMESTAMP,
88
+    completed_at                TIMESTAMP,
89
+    expected_completion_at      TIMESTAMP,
90
+    duration_seconds            BIGINT,
91
+    tenant_id                   VARCHAR(50),
92
+    deleted                     INTEGER NOT NULL DEFAULT 0,
93
+    created_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
94
+    updated_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
95
+);
96
+
97
+CREATE UNIQUE INDEX IF NOT EXISTS uk_bpm_instance_id ON bpm_process_instance(instance_id) WHERE deleted = 0;
98
+CREATE INDEX IF NOT EXISTS idx_bpm_instance_def ON bpm_process_instance(definition_id);
99
+CREATE INDEX IF NOT EXISTS idx_bpm_instance_status ON bpm_process_instance(status);
100
+CREATE INDEX IF NOT EXISTS idx_bpm_instance_initiator ON bpm_process_instance(initiator_id);
101
+CREATE INDEX IF NOT EXISTS idx_bpm_instance_assignee ON bpm_process_instance(current_assignee_id);
102
+CREATE INDEX IF NOT EXISTS idx_bpm_instance_business ON bpm_process_instance(business_key);
103
+CREATE INDEX IF NOT EXISTS idx_bpm_instance_created ON bpm_process_instance(created_at);
104
+
105
+COMMENT ON TABLE bpm_process_instance IS '流程实例表';
106
+COMMENT ON COLUMN bpm_process_instance.status IS '状态: running-运行中/completed-已完成/terminated-已撤回/rejected-已驳回/suspended-挂起';
107
+COMMENT ON COLUMN bpm_process_instance.priority IS '优先级: 0-普通 1-紧急 2-特急';
108
+
109
+-- 4. 审批记录表
110
+CREATE TABLE IF NOT EXISTS bpm_approval_record (
111
+    id                      BIGSERIAL PRIMARY KEY,
112
+    instance_id             BIGINT NOT NULL REFERENCES bpm_process_instance(id),
113
+    instance_uuid           VARCHAR(64),
114
+    node_id                 VARCHAR(100),
115
+    node_name               VARCHAR(200),
116
+    approver_id             BIGINT NOT NULL,
117
+    approver_name           VARCHAR(100),
118
+    action                  VARCHAR(50) NOT NULL,       -- approve/reject/transfer/delegate/back/countersign/start
119
+    comment                 TEXT,
120
+    target_assignee_id      BIGINT,
121
+    target_assignee_name    VARCHAR(100),
122
+    countersign_result      VARCHAR(50),                -- all/pass_one/veto
123
+    countersign_approved    INTEGER,
124
+    countersign_total       INTEGER,
125
+    approved_at             TIMESTAMP,
126
+    tenant_id               VARCHAR(50),
127
+    deleted                 INTEGER NOT NULL DEFAULT 0,
128
+    created_at              TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
129
+);
130
+
131
+CREATE INDEX IF NOT EXISTS idx_bpm_record_instance ON bpm_approval_record(instance_id);
132
+CREATE INDEX IF NOT EXISTS idx_bpm_record_uuid ON bpm_approval_record(instance_uuid);
133
+CREATE INDEX IF NOT EXISTS idx_bpm_record_approver ON bpm_approval_record(approver_id);
134
+CREATE INDEX IF NOT EXISTS idx_bpm_record_action ON bpm_approval_record(action);
135
+CREATE INDEX IF NOT EXISTS idx_bpm_record_time ON bpm_approval_record(approved_at);
136
+
137
+COMMENT ON TABLE bpm_approval_record IS '审批记录表';
138
+COMMENT ON COLUMN bpm_approval_record.action IS '审批动作: approve-通过/reject-驳回/transfer-转办/delegate-委派/back-退回/countersign-会签/start-发起';
139
+
140
+-- 5. 待办任务表
141
+CREATE TABLE IF NOT EXISTS bpm_todo_task (
142
+    id                  BIGSERIAL PRIMARY KEY,
143
+    instance_id         BIGINT NOT NULL REFERENCES bpm_process_instance(id),
144
+    instance_uuid       VARCHAR(64),
145
+    title               VARCHAR(500),
146
+    process_key         VARCHAR(100),
147
+    process_name        VARCHAR(200),
148
+    node_id             VARCHAR(100),
149
+    node_name           VARCHAR(200),
150
+    assignee_id         BIGINT NOT NULL,
151
+    assignee_name       VARCHAR(100),
152
+    initiator_id        BIGINT,
153
+    initiator_name      VARCHAR(100),
154
+    business_key        VARCHAR(200),
155
+    status              VARCHAR(50) NOT NULL DEFAULT 'pending',   -- pending/completed/transferred/delegated/cancelled
156
+    priority            INTEGER DEFAULT 0,
157
+    received_at         TIMESTAMP,
158
+    completed_at        TIMESTAMP,
159
+    deadline_at         TIMESTAMP,
160
+    is_read             BOOLEAN DEFAULT FALSE,
161
+    tenant_id           VARCHAR(50),
162
+    deleted             INTEGER NOT NULL DEFAULT 0,
163
+    created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
164
+    updated_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
165
+);
166
+
167
+CREATE INDEX IF NOT EXISTS idx_bpm_todo_assignee ON bpm_todo_task(assignee_id);
168
+CREATE INDEX IF NOT EXISTS idx_bpm_todo_status ON bpm_todo_task(status);
169
+CREATE INDEX IF NOT EXISTS idx_bpm_todo_instance ON bpm_todo_task(instance_id);
170
+CREATE INDEX IF NOT EXISTS idx_bpm_todo_uuid ON bpm_todo_task(instance_uuid);
171
+CREATE INDEX IF NOT EXISTS idx_bpm_todo_initiator ON bpm_todo_task(initiator_id);
172
+CREATE INDEX IF NOT EXISTS idx_bpm_todo_priority ON bpm_todo_task(priority DESC, received_at);
173
+CREATE INDEX IF NOT EXISTS idx_bpm_todo_deadline ON bpm_todo_task(deadline_at) WHERE status = 'pending';
174
+
175
+COMMENT ON TABLE bpm_todo_task IS '待办任务表';
176
+COMMENT ON COLUMN bpm_todo_task.status IS '任务状态: pending-待处理/completed-已完成/transferred-已转办/delegated-已委派/cancelled-已取消';
177
+
178
+-- 6. 流程编排表
179
+CREATE TABLE IF NOT EXISTS bpm_orchestration (
180
+    id                          BIGSERIAL PRIMARY KEY,
181
+    orchestration_name          VARCHAR(200) NOT NULL,
182
+    orchestration_code          VARCHAR(100) NOT NULL,
183
+    description                 TEXT,
184
+    process_definition_ids      TEXT,
185
+    orchestration_rules         TEXT,
186
+    trigger_type                VARCHAR(50),            -- manual/scheduled/event
187
+    cron_expression             VARCHAR(100),
188
+    event_name                  VARCHAR(200),
189
+    status                      INTEGER NOT NULL DEFAULT 0,
190
+    created_by                  VARCHAR(100),
191
+    execution_count             INTEGER DEFAULT 0,
192
+    tenant_id                   VARCHAR(50),
193
+    deleted                     INTEGER NOT NULL DEFAULT 0,
194
+    created_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
195
+    updated_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
196
+);
197
+
198
+CREATE INDEX IF NOT EXISTS idx_bpm_orch_code ON bpm_orchestration(orchestration_code);
199
+CREATE INDEX IF NOT EXISTS idx_bpm_orch_status ON bpm_orchestration(status);
200
+
201
+COMMENT ON TABLE bpm_orchestration IS '流程编排表';
202
+
203
+-- 7. 流程表单模板表
204
+CREATE TABLE IF NOT EXISTS bpm_form_template (
205
+    id                  BIGSERIAL PRIMARY KEY,
206
+    template_name       VARCHAR(200) NOT NULL,
207
+    template_code       VARCHAR(100) NOT NULL,
208
+    category            VARCHAR(50),
209
+    form_schema         TEXT,
210
+    bpmn_template       TEXT,
211
+    description         TEXT,
212
+    icon                VARCHAR(200),
213
+    use_count           INTEGER DEFAULT 0,
214
+    status              INTEGER NOT NULL DEFAULT 0,
215
+    created_by          VARCHAR(100),
216
+    tenant_id           VARCHAR(50),
217
+    deleted             INTEGER NOT NULL DEFAULT 0,
218
+    created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
219
+    updated_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
220
+);
221
+
222
+CREATE INDEX IF NOT EXISTS idx_bpm_form_code ON bpm_form_template(template_code);
223
+CREATE INDEX IF NOT EXISTS idx_bpm_form_category ON bpm_form_template(category);
224
+
225
+COMMENT ON TABLE bpm_form_template IS '流程表单模板表';
226
+
227
+-- 8. 流程统计表
228
+CREATE TABLE IF NOT EXISTS bpm_process_stat (
229
+    id                          BIGSERIAL PRIMARY KEY,
230
+    definition_id               BIGINT NOT NULL REFERENCES bpm_process_definition(id),
231
+    process_key                 VARCHAR(100),
232
+    period                      VARCHAR(20),            -- day/week/month/quarter/year
233
+    stat_date                   VARCHAR(20),
234
+    start_count                 INTEGER DEFAULT 0,
235
+    completed_count             INTEGER DEFAULT 0,
236
+    rejected_count              INTEGER DEFAULT 0,
237
+    terminated_count            INTEGER DEFAULT 0,
238
+    avg_duration_seconds        BIGINT,
239
+    max_duration_seconds        BIGINT,
240
+    min_duration_seconds        BIGINT,
241
+    avg_node_duration_seconds   BIGINT,
242
+    timeout_count               INTEGER DEFAULT 0,
243
+    first_pass_rate             DOUBLE PRECISION,
244
+    tenant_id                   VARCHAR(50),
245
+    deleted                     INTEGER NOT NULL DEFAULT 0,
246
+    created_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
247
+    updated_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
248
+);
249
+
250
+CREATE INDEX IF NOT EXISTS idx_bpm_stat_def ON bpm_process_stat(definition_id);
251
+CREATE INDEX IF NOT EXISTS idx_bpm_stat_date ON bpm_process_stat(stat_date);
252
+CREATE INDEX IF NOT EXISTS idx_bpm_stat_period ON bpm_process_stat(period);
253
+
254
+COMMENT ON TABLE bpm_process_stat IS '流程统计表';
255
+
256
+-- ==========================================
257
+-- 初始化数据:示例流程定义
258
+-- ==========================================
259
+
260
+INSERT INTO bpm_process_definition (process_key, process_name, description, category, version, status, created_by, sort_order, published_at, created_at, updated_at)
261
+VALUES
262
+    ('water_revenue_approval', '水费审批流程', '水费收缴相关审批流程', 'revenue', 1, 1, 'admin', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
263
+    ('water_patrol_approval', '巡检审批流程', '巡检任务审批流程', 'patrol', 1, 1, 'admin', 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
264
+    ('water_dispatch_approval', '调度审批流程', '供水调度审批流程', 'dispatch', 1, 1, 'admin', 3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
265
+    ('water_maintenance_approval', '维修审批流程', '设备维修审批流程', 'maintenance', 1, 1, 'admin', 4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
266
+ON CONFLICT DO NOTHING;
267
+
268
+-- 示例节点:水费审批流程
269
+INSERT INTO bpm_process_node (definition_id, node_id, node_name, node_type, assignee_type, assignee_value, assignee_name, sort_order, created_at, updated_at)
270
+SELECT d.id, n.node_id, n.node_name, n.node_type, n.assignee_type, n.assignee_value, n.assignee_name, n.sort_order, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
271
+FROM bpm_process_definition d,
272
+     (VALUES
273
+        ('start', '开始', 'start', 'initiator', NULL, '发起人', 1),
274
+        ('dept_review', '部门审核', 'userTask', 'role', '10', '部门主管', 2),
275
+        ('finance_review', '财务审核', 'userTask', 'role', '20', '财务主管', 3),
276
+        ('end', '结束', 'end', NULL, NULL, NULL, 4)
277
+     ) AS n(node_id, node_name, node_type, assignee_type, assignee_value, assignee_name, sort_order)
278
+WHERE d.process_key = 'water_revenue_approval' AND d.version = 1
279
+ON CONFLICT DO NOTHING;

+ 227
- 0
wm-bpm/src/test/java/com/water/bpm/service/ProcessDefinitionServiceTest.java Zobrazit soubor

1
+package com.water.bpm.service;
2
+
3
+import com.water.bpm.entity.BpmProcessDefinition;
4
+import com.water.bpm.entity.BpmProcessNode;
5
+import com.water.bpm.mapper.BpmProcessDefinitionMapper;
6
+import com.water.bpm.mapper.BpmProcessNodeMapper;
7
+import com.water.common.core.exception.BusinessException;
8
+import org.junit.jupiter.api.DisplayName;
9
+import org.junit.jupiter.api.Test;
10
+import org.junit.jupiter.api.extension.ExtendWith;
11
+import org.mockito.InjectMocks;
12
+import org.mockito.Mock;
13
+import org.mockito.junit.jupiter.MockitoExtension;
14
+
15
+import java.util.Arrays;
16
+import java.util.Collections;
17
+import java.util.List;
18
+
19
+import static org.junit.jupiter.api.Assertions.*;
20
+import static org.mockito.ArgumentMatchers.*;
21
+import static org.mockito.Mockito.*;
22
+
23
+/**
24
+ * 流程定义管理服务测试
25
+ */
26
+@ExtendWith(MockitoExtension.class)
27
+class ProcessDefinitionServiceTest {
28
+
29
+    @Mock
30
+    private BpmProcessDefinitionMapper definitionMapper;
31
+    @Mock
32
+    private BpmProcessNodeMapper processNodeMapper;
33
+
34
+    @InjectMocks
35
+    private ProcessDefinitionService definitionService;
36
+
37
+    @Test
38
+    @DisplayName("创建流程定义 - 成功")
39
+    void create_success() {
40
+        when(definitionMapper.selectByProcessKey("test_key")).thenReturn(null);
41
+        when(definitionMapper.insert(any(BpmProcessDefinition.class))).thenAnswer(inv -> {
42
+            BpmProcessDefinition def = inv.getArgument(0);
43
+            def.setId(1L);
44
+            return 1;
45
+        });
46
+
47
+        BpmProcessDefinition def = new BpmProcessDefinition();
48
+        def.setProcessKey("test_key");
49
+        def.setProcessName("测试流程");
50
+        def.setCategory("test");
51
+
52
+        BpmProcessDefinition result = definitionService.create(def);
53
+
54
+        assertNotNull(result);
55
+        assertEquals("test_key", result.getProcessKey());
56
+        assertEquals(0, result.getStatus()); // 草稿
57
+        assertEquals(1, result.getVersion());
58
+        verify(definitionMapper).insert(any(BpmProcessDefinition.class));
59
+    }
60
+
61
+    @Test
62
+    @DisplayName("创建流程定义 - processKey 重复")
63
+    void create_duplicateKey() {
64
+        BpmProcessDefinition existing = new BpmProcessDefinition();
65
+        existing.setId(1L);
66
+        existing.setProcessKey("test_key");
67
+        when(definitionMapper.selectByProcessKey("test_key")).thenReturn(existing);
68
+
69
+        BpmProcessDefinition def = new BpmProcessDefinition();
70
+        def.setProcessKey("test_key");
71
+
72
+        assertThrows(BusinessException.class, () -> definitionService.create(def));
73
+    }
74
+
75
+    @Test
76
+    @DisplayName("发布流程定义 - 成功")
77
+    void publish_success() {
78
+        BpmProcessDefinition def = new BpmProcessDefinition();
79
+        def.setId(1L);
80
+        def.setStatus(0);
81
+        when(definitionMapper.selectById(1L)).thenReturn(def);
82
+        when(definitionMapper.updateById(any())).thenReturn(1);
83
+
84
+        BpmProcessDefinition result = definitionService.publish(1L);
85
+
86
+        assertEquals(1, result.getStatus());
87
+        assertNotNull(result.getPublishedAt());
88
+    }
89
+
90
+    @Test
91
+    @DisplayName("发布已发布的定义 - 异常")
92
+    void publish_alreadyPublished() {
93
+        BpmProcessDefinition def = new BpmProcessDefinition();
94
+        def.setId(1L);
95
+        def.setStatus(1);
96
+        when(definitionMapper.selectById(1L)).thenReturn(def);
97
+
98
+        assertThrows(BusinessException.class, () -> definitionService.publish(1L));
99
+    }
100
+
101
+    @Test
102
+    @DisplayName("停用流程定义 - 成功")
103
+    void deactivate_success() {
104
+        BpmProcessDefinition def = new BpmProcessDefinition();
105
+        def.setId(1L);
106
+        def.setStatus(1);
107
+        when(definitionMapper.selectById(1L)).thenReturn(def);
108
+        when(definitionMapper.updateById(any())).thenReturn(1);
109
+
110
+        BpmProcessDefinition result = definitionService.deactivate(1L);
111
+
112
+        assertEquals(2, result.getStatus());
113
+    }
114
+
115
+    @Test
116
+    @DisplayName("停用草稿定义 - 异常")
117
+    void deactivate_notPublished() {
118
+        BpmProcessDefinition def = new BpmProcessDefinition();
119
+        def.setId(1L);
120
+        def.setStatus(0);
121
+        when(definitionMapper.selectById(1L)).thenReturn(def);
122
+
123
+        assertThrows(BusinessException.class, () -> definitionService.deactivate(1L));
124
+    }
125
+
126
+    @Test
127
+    @DisplayName("创建新版本 - 成功")
128
+    void createNewVersion_success() {
129
+        BpmProcessDefinition oldDef = new BpmProcessDefinition();
130
+        oldDef.setId(1L);
131
+        oldDef.setProcessKey("test_key");
132
+        oldDef.setProcessName("测试流程");
133
+        oldDef.setVersion(1);
134
+        oldDef.setStatus(1);
135
+        when(definitionMapper.selectById(1L)).thenReturn(oldDef);
136
+        when(definitionMapper.updateById(any())).thenReturn(1);
137
+        when(processNodeMapper.selectByDefinitionId(1L)).thenReturn(Collections.emptyList());
138
+        when(definitionMapper.insert(any())).thenAnswer(inv -> {
139
+            BpmProcessDefinition def = inv.getArgument(0);
140
+            def.setId(2L);
141
+            return 1;
142
+        });
143
+
144
+        BpmProcessDefinition result = definitionService.createNewVersion(1L);
145
+
146
+        assertEquals(2, result.getVersion());
147
+        assertEquals(0, result.getStatus()); // 草稿
148
+        assertEquals("test_key", result.getProcessKey());
149
+    }
150
+
151
+    @Test
152
+    @DisplayName("更新草稿定义 - 成功")
153
+    void update_success() {
154
+        BpmProcessDefinition existing = new BpmProcessDefinition();
155
+        existing.setId(1L);
156
+        existing.setStatus(0);
157
+        when(definitionMapper.selectById(1L)).thenReturn(existing);
158
+        when(definitionMapper.updateById(any())).thenReturn(1);
159
+        when(definitionMapper.selectById(1L)).thenReturn(existing);
160
+
161
+        BpmProcessDefinition update = new BpmProcessDefinition();
162
+        update.setProcessName("更新后名称");
163
+
164
+        definitionService.update(1L, update);
165
+        verify(definitionMapper).updateById(any());
166
+    }
167
+
168
+    @Test
169
+    @DisplayName("更新已发布定义 - 异常")
170
+    void update_published() {
171
+        BpmProcessDefinition existing = new BpmProcessDefinition();
172
+        existing.setId(1L);
173
+        existing.setStatus(1);
174
+        when(definitionMapper.selectById(1L)).thenReturn(existing);
175
+
176
+        BpmProcessDefinition update = new BpmProcessDefinition();
177
+        update.setProcessName("更新后名称");
178
+
179
+        assertThrows(BusinessException.class, () -> definitionService.update(1L, update));
180
+    }
181
+
182
+    @Test
183
+    @DisplayName("查询定义列表")
184
+    void list() {
185
+        when(definitionMapper.selectList(any())).thenReturn(Arrays.asList(
186
+                new BpmProcessDefinition(), new BpmProcessDefinition()
187
+        ));
188
+
189
+        List<BpmProcessDefinition> result = definitionService.list("test", null);
190
+        assertEquals(2, result.size());
191
+    }
192
+
193
+    @Test
194
+    @DisplayName("获取节点列表")
195
+    void getNodes() {
196
+        BpmProcessNode node = new BpmProcessNode();
197
+        node.setNodeId("start");
198
+        when(processNodeMapper.selectByDefinitionId(1L)).thenReturn(List.of(node));
199
+
200
+        List<BpmProcessNode> result = definitionService.getNodes(1L);
201
+        assertEquals(1, result.size());
202
+    }
203
+
204
+    @Test
205
+    @DisplayName("删除草稿定义 - 成功")
206
+    void delete_success() {
207
+        BpmProcessDefinition def = new BpmProcessDefinition();
208
+        def.setId(1L);
209
+        def.setStatus(0);
210
+        when(definitionMapper.selectById(1L)).thenReturn(def);
211
+        when(definitionMapper.deleteById(1L)).thenReturn(1);
212
+
213
+        definitionService.delete(1L);
214
+        verify(definitionMapper).deleteById(1L);
215
+    }
216
+
217
+    @Test
218
+    @DisplayName("删除已发布定义 - 异常")
219
+    void delete_published() {
220
+        BpmProcessDefinition def = new BpmProcessDefinition();
221
+        def.setId(1L);
222
+        def.setStatus(1);
223
+        when(definitionMapper.selectById(1L)).thenReturn(def);
224
+
225
+        assertThrows(BusinessException.class, () -> definitionService.delete(1L));
226
+    }
227
+}

+ 517
- 0
wm-bpm/src/test/java/com/water/bpm/service/ProcessEngineTest.java Zobrazit soubor

1
+package com.water.bpm.service;
2
+
3
+import com.fasterxml.jackson.databind.ObjectMapper;
4
+import com.water.bpm.entity.*;
5
+import com.water.bpm.mapper.*;
6
+import com.water.common.core.exception.BusinessException;
7
+import org.junit.jupiter.api.BeforeEach;
8
+import org.junit.jupiter.api.DisplayName;
9
+import org.junit.jupiter.api.Test;
10
+import org.junit.jupiter.api.extension.ExtendWith;
11
+import org.mockito.ArgumentCaptor;
12
+import org.mockito.InjectMocks;
13
+import org.mockito.Mock;
14
+import org.mockito.Spy;
15
+import org.mockito.junit.jupiter.MockitoExtension;
16
+
17
+import java.time.LocalDateTime;
18
+import java.util.*;
19
+
20
+import static org.junit.jupiter.api.Assertions.*;
21
+import static org.mockito.ArgumentMatchers.*;
22
+import static org.mockito.Mockito.*;
23
+
24
+/**
25
+ * 流程引擎核心单元测试
26
+ * 覆盖:启动流程、审批通过、驳回、转办、会签、待办查询
27
+ */
28
+@ExtendWith(MockitoExtension.class)
29
+class ProcessEngineTest {
30
+
31
+    @Mock
32
+    private BpmProcessDefinitionMapper definitionMapper;
33
+    @Mock
34
+    private BpmProcessInstanceMapper instanceMapper;
35
+    @Mock
36
+    private BpmApprovalRecordMapper approvalRecordMapper;
37
+    @Mock
38
+    private BpmTodoTaskMapper todoTaskMapper;
39
+    @Mock
40
+    private BpmProcessNodeMapper processNodeMapper;
41
+
42
+    @Spy
43
+    private ObjectMapper objectMapper = new ObjectMapper();
44
+
45
+    @InjectMocks
46
+    private ProcessEngine processEngine;
47
+
48
+    private BpmProcessDefinition testDefinition;
49
+    private BpmProcessNode startNode;
50
+    private BpmProcessNode reviewNode;
51
+    private BpmProcessNode endNode;
52
+    private List<BpmProcessNode> testNodes;
53
+
54
+    @BeforeEach
55
+    void setUp() {
56
+        testDefinition = new BpmProcessDefinition();
57
+        testDefinition.setId(1L);
58
+        testDefinition.setProcessKey("test_process");
59
+        testDefinition.setProcessName("测试流程");
60
+        testDefinition.setStatus(1);
61
+        testDefinition.setVersion(1);
62
+
63
+        startNode = new BpmProcessNode();
64
+        startNode.setId(1L);
65
+        startNode.setNodeId("start");
66
+        startNode.setNodeName("开始");
67
+        startNode.setNodeType("start");
68
+        startNode.setAssigneeType("initiator");
69
+        startNode.setSortOrder(1);
70
+
71
+        reviewNode = new BpmProcessNode();
72
+        reviewNode.setId(2L);
73
+        reviewNode.setNodeId("review");
74
+        reviewNode.setNodeName("审核");
75
+        reviewNode.setNodeType("userTask");
76
+        reviewNode.setAssigneeType("user");
77
+        reviewNode.setAssigneeValue("100");
78
+        reviewNode.setAssigneeName("审核人");
79
+        reviewNode.setSortOrder(2);
80
+
81
+        endNode = new BpmProcessNode();
82
+        endNode.setId(3L);
83
+        endNode.setNodeId("end");
84
+        endNode.setNodeName("结束");
85
+        endNode.setNodeType("end");
86
+        endNode.setSortOrder(3);
87
+
88
+        testNodes = Arrays.asList(startNode, reviewNode, endNode);
89
+    }
90
+
91
+    // ==================== 启动流程测试 ====================
92
+
93
+    @Test
94
+    @DisplayName("启动流程 - 成功场景")
95
+    void startProcess_success() {
96
+        when(definitionMapper.selectById(1L)).thenReturn(testDefinition);
97
+        when(processNodeMapper.selectByDefinitionId(1L)).thenReturn(testNodes);
98
+        when(instanceMapper.insert(any(BpmProcessInstance.class))).thenAnswer(inv -> {
99
+            BpmProcessInstance inst = inv.getArgument(0);
100
+            inst.setId(100L);
101
+            return 1;
102
+        });
103
+        when(todoTaskMapper.insert(any(BpmTodoTask.class))).thenReturn(1);
104
+        when(approvalRecordMapper.insert(any(BpmApprovalRecord.class))).thenReturn(1);
105
+        when(instanceMapper.updateById(any(BpmProcessInstance.class))).thenReturn(1);
106
+
107
+        Map<String, Object> formData = Map.of("amount", 1000, "reason", "测试");
108
+        BpmProcessInstance result = processEngine.startProcess(
109
+                1L, 1L, "张三", "BIZ-001", "test", "测试流程", formData, null, 0);
110
+
111
+        assertNotNull(result);
112
+        assertEquals("running", result.getStatus());
113
+        assertEquals("start", result.getCurrentNodeId());
114
+        assertEquals("张三", result.getInitiatorName());
115
+        assertEquals("BIZ-001", result.getBusinessKey());
116
+        assertNotNull(result.getInstanceId());
117
+
118
+        // 验证实例已插入
119
+        verify(instanceMapper).insert(any(BpmProcessInstance.class));
120
+        // 验证待办已创建
121
+        verify(todoTaskMapper).insert(any(BpmTodoTask.class));
122
+        // 验证启动记录已插入
123
+        verify(approvalRecordMapper).insert(any(BpmApprovalRecord.class));
124
+    }
125
+
126
+    @Test
127
+    @DisplayName("启动流程 - 定义不存在")
128
+    void startProcess_definitionNotFound() {
129
+        when(definitionMapper.selectById(999L)).thenReturn(null);
130
+
131
+        assertThrows(BusinessException.class, () ->
132
+                processEngine.startProcess(999L, 1L, "张三", "BIZ-001", null, null, null, null, 0));
133
+    }
134
+
135
+    @Test
136
+    @DisplayName("启动流程 - 定义未发布")
137
+    void startProcess_definitionNotPublished() {
138
+        testDefinition.setStatus(0); // 草稿
139
+        when(definitionMapper.selectById(1L)).thenReturn(testDefinition);
140
+
141
+        assertThrows(BusinessException.class, () ->
142
+                processEngine.startProcess(1L, 1L, "张三", "BIZ-001", null, null, null, null, 0));
143
+    }
144
+
145
+    @Test
146
+    @DisplayName("启动流程 - 无节点配置")
147
+    void startProcess_noNodes() {
148
+        when(definitionMapper.selectById(1L)).thenReturn(testDefinition);
149
+        when(processNodeMapper.selectByDefinitionId(1L)).thenReturn(Collections.emptyList());
150
+
151
+        assertThrows(BusinessException.class, () ->
152
+                processEngine.startProcess(1L, 1L, "张三", "BIZ-001", null, null, null, null, 0));
153
+    }
154
+
155
+    // ==================== 审批通过测试 ====================
156
+
157
+    @Test
158
+    @DisplayName("审批通过 - 流转到下一节点")
159
+    void approveTask_success() {
160
+        BpmProcessInstance instance = createRunningInstance();
161
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
162
+        when(approvalRecordMapper.insert(any())).thenReturn(1);
163
+        when(todoTaskMapper.update(any(), any())).thenReturn(1);
164
+        when(processNodeMapper.selectByDefinitionId(1L)).thenReturn(testNodes);
165
+        when(todoTaskMapper.insert(any())).thenReturn(1);
166
+        when(instanceMapper.updateById(any())).thenReturn(1);
167
+
168
+        BpmProcessInstance result = processEngine.approveTask(
169
+                "test-uuid", 100L, "审核人", "review", "同意");
170
+
171
+        assertNotNull(result);
172
+        assertEquals("review", result.getCurrentNodeId());
173
+
174
+        // 验证审批记录已创建
175
+        ArgumentCaptor<BpmApprovalRecord> recordCaptor = ArgumentCaptor.forClass(BpmApprovalRecord.class);
176
+        verify(approvalRecordMapper).insert(recordCaptor.capture());
177
+        assertEquals("approve", recordCaptor.getValue().getAction());
178
+    }
179
+
180
+    @Test
181
+    @DisplayName("审批通过 - 流程完成(到达结束节点)")
182
+    void approveTask_completeProcess() {
183
+        BpmProcessInstance instance = createRunningInstance();
184
+        instance.setCurrentNodeId("review");
185
+        instance.setCurrentNodeName("审核");
186
+
187
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
188
+        when(approvalRecordMapper.insert(any())).thenReturn(1);
189
+        when(todoTaskMapper.update(any(), any())).thenReturn(1);
190
+        when(processNodeMapper.selectByDefinitionId(1L)).thenReturn(testNodes);
191
+        when(instanceMapper.updateById(any())).thenReturn(1);
192
+        when(todoTaskMapper.update(any(), any())).thenReturn(1);
193
+
194
+        BpmProcessInstance result = processEngine.approveTask(
195
+                "test-uuid", 100L, "审核人", "review", "同意");
196
+
197
+        assertNotNull(result);
198
+        assertEquals("completed", result.getStatus());
199
+        assertNotNull(result.getCompletedAt());
200
+    }
201
+
202
+    @Test
203
+    @DisplayName("审批通过 - 实例不在运行状态")
204
+    void approveTask_instanceNotRunning() {
205
+        BpmProcessInstance instance = createRunningInstance();
206
+        instance.setStatus("completed");
207
+
208
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
209
+
210
+        assertThrows(BusinessException.class, () ->
211
+                processEngine.approveTask("test-uuid", 100L, "审核人", "review", "同意"));
212
+    }
213
+
214
+    @Test
215
+    @DisplayName("审批通过 - 非当前审批人")
216
+    void approveTask_wrongApprover() {
217
+        BpmProcessInstance instance = createRunningInstance();
218
+        instance.setCurrentAssigneeId(100L);
219
+
220
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
221
+
222
+        assertThrows(BusinessException.class, () ->
223
+                processEngine.approveTask("test-uuid", 999L, "其他人", "review", "同意"));
224
+    }
225
+
226
+    // ==================== 驳回测试 ====================
227
+
228
+    @Test
229
+    @DisplayName("驳回 - 成功场景")
230
+    void rejectTask_success() {
231
+        BpmProcessInstance instance = createRunningInstance();
232
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
233
+        when(approvalRecordMapper.insert(any())).thenReturn(1);
234
+        when(todoTaskMapper.update(any(), any())).thenReturn(1);
235
+        when(instanceMapper.updateById(any())).thenReturn(1);
236
+
237
+        BpmProcessInstance result = processEngine.rejectTask(
238
+                "test-uuid", 100L, "审核人", "review", "不符合要求");
239
+
240
+        assertNotNull(result);
241
+        assertEquals("rejected", result.getStatus());
242
+        assertNotNull(result.getCompletedAt());
243
+
244
+        // 验证审批记录
245
+        ArgumentCaptor<BpmApprovalRecord> recordCaptor = ArgumentCaptor.forClass(BpmApprovalRecord.class);
246
+        verify(approvalRecordMapper).insert(recordCaptor.capture());
247
+        assertEquals("reject", recordCaptor.getValue().getAction());
248
+    }
249
+
250
+    @Test
251
+    @DisplayName("驳回 - 实例不存在")
252
+    void rejectTask_instanceNotFound() {
253
+        when(instanceMapper.selectOne(any())).thenReturn(null);
254
+
255
+        assertThrows(BusinessException.class, () ->
256
+                processEngine.rejectTask("non-exist", 100L, "审核人", "review", "拒绝"));
257
+    }
258
+
259
+    // ==================== 转办测试 ====================
260
+
261
+    @Test
262
+    @DisplayName("转办 - 成功场景")
263
+    void transferTask_success() {
264
+        BpmProcessInstance instance = createRunningInstance();
265
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
266
+        when(approvalRecordMapper.insert(any())).thenReturn(1);
267
+
268
+        BpmTodoTask currentTodo = new BpmTodoTask();
269
+        currentTodo.setId(1L);
270
+        currentTodo.setInstanceId(instance.getId());
271
+        currentTodo.setStatus("pending");
272
+        when(todoTaskMapper.selectPendingByUserId(100L)).thenReturn(List.of(currentTodo));
273
+        when(todoTaskMapper.updateById(any())).thenReturn(1);
274
+
275
+        when(processNodeMapper.selectByDefinitionId(1L)).thenReturn(testNodes);
276
+        when(todoTaskMapper.insert(any())).thenReturn(1);
277
+        when(instanceMapper.updateById(any())).thenReturn(1);
278
+
279
+        BpmProcessInstance result = processEngine.transferTask(
280
+                "test-uuid", 100L, "审核人", 200L, "新审核人", "review", "转交处理");
281
+
282
+        assertNotNull(result);
283
+        assertEquals(Long.valueOf(200L), result.getCurrentAssigneeId());
284
+        assertEquals("新审核人", result.getCurrentAssigneeName());
285
+        assertEquals("running", result.getStatus());
286
+
287
+        // 验证转办记录
288
+        ArgumentCaptor<BpmApprovalRecord> recordCaptor = ArgumentCaptor.forClass(BpmApprovalRecord.class);
289
+        verify(approvalRecordMapper).insert(recordCaptor.capture());
290
+        BpmApprovalRecord record = recordCaptor.getValue();
291
+        assertEquals("transfer", record.getAction());
292
+        assertEquals(Long.valueOf(200L), record.getTargetAssigneeId());
293
+    }
294
+
295
+    @Test
296
+    @DisplayName("转办 - 目标人相同")
297
+    void transferTask_sameTarget() {
298
+        BpmProcessInstance instance = createRunningInstance();
299
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
300
+
301
+        assertThrows(BusinessException.class, () ->
302
+                processEngine.transferTask("test-uuid", 100L, "审核人", 100L, "审核人", "review", ""));
303
+    }
304
+
305
+    @Test
306
+    @DisplayName("转办 - 目标人为空")
307
+    void transferTask_nullTarget() {
308
+        BpmProcessInstance instance = createRunningInstance();
309
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
310
+
311
+        assertThrows(BusinessException.class, () ->
312
+                processEngine.transferTask("test-uuid", 100L, "审核人", null, null, "review", ""));
313
+    }
314
+
315
+    // ==================== 会签测试 ====================
316
+
317
+    @Test
318
+    @DisplayName("发起会签 - 成功场景")
319
+    void initiateCountersign_success() {
320
+        BpmProcessInstance instance = createRunningInstance();
321
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
322
+        when(approvalRecordMapper.insert(any())).thenReturn(1);
323
+        when(todoTaskMapper.update(any(), any())).thenReturn(1);
324
+        when(processNodeMapper.selectByDefinitionId(1L)).thenReturn(testNodes);
325
+        when(todoTaskMapper.insert(any())).thenReturn(1);
326
+        when(instanceMapper.updateById(any())).thenReturn(1);
327
+
328
+        List<Long> signerIds = Arrays.asList(101L, 102L, 103L);
329
+        List<String> signerNames = Arrays.asList("会签人A", "会签人B", "会签人C");
330
+
331
+        BpmProcessInstance result = processEngine.initiateCountersign(
332
+                "test-uuid", 1L, signerIds, signerNames, "review", "需要三人会签");
333
+
334
+        assertNotNull(result);
335
+
336
+        // 验证会签记录
337
+        ArgumentCaptor<BpmApprovalRecord> recordCaptor = ArgumentCaptor.forClass(BpmApprovalRecord.class);
338
+        verify(approvalRecordMapper).insert(recordCaptor.capture());
339
+        BpmApprovalRecord record = recordCaptor.getValue();
340
+        assertEquals("countersign", record.getAction());
341
+        assertEquals(3, record.getCountersignTotal());
342
+        assertEquals(0, record.getCountersignApproved());
343
+
344
+        // 验证为每个会签人创建了待办
345
+        verify(todoTaskMapper, times(3)).insert(any(BpmTodoTask.class));
346
+    }
347
+
348
+    @Test
349
+    @DisplayName("发起会签 - 少于2人")
350
+    void initiateCountersign_tooFewSigners() {
351
+        BpmProcessInstance instance = createRunningInstance();
352
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
353
+
354
+        assertThrows(BusinessException.class, () ->
355
+                processEngine.initiateCountersign("test-uuid", 1L,
356
+                        List.of(101L), List.of("单人"), "review", ""));
357
+    }
358
+
359
+    @Test
360
+    @DisplayName("会签审批 - 单人通过后仍有待办")
361
+    void countersignApprove_partialApproval() {
362
+        BpmProcessInstance instance = createRunningInstance();
363
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
364
+
365
+        BpmTodoTask signerTodo = new BpmTodoTask();
366
+        signerTodo.setId(1L);
367
+        signerTodo.setInstanceId(instance.getId());
368
+        signerTodo.setStatus("pending");
369
+        when(todoTaskMapper.selectPendingByUserId(101L)).thenReturn(List.of(signerTodo));
370
+        when(approvalRecordMapper.insert(any())).thenReturn(1);
371
+        when(todoTaskMapper.update(any(), any())).thenReturn(1);
372
+
373
+        // 还有其他会签人待办
374
+        BpmTodoTask remainingTodo = new BpmTodoTask();
375
+        remainingTodo.setId(2L);
376
+        remainingTodo.setInstanceId(instance.getId());
377
+        when(todoTaskMapper.selectPendingByInstanceId(instance.getId())).thenReturn(List.of(remainingTodo));
378
+
379
+        BpmProcessInstance result = processEngine.countersignApprove(
380
+                "test-uuid", 101L, "会签人A", "review", "同意");
381
+
382
+        assertNotNull(result);
383
+        assertEquals("running", result.getStatus());
384
+    }
385
+
386
+    @Test
387
+    @DisplayName("会签审批 - 全部通过流转到下一节点")
388
+    void countersignApprove_allApproved() {
389
+        BpmProcessInstance instance = createRunningInstance();
390
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
391
+
392
+        BpmTodoTask signerTodo = new BpmTodoTask();
393
+        signerTodo.setId(1L);
394
+        signerTodo.setInstanceId(instance.getId());
395
+        signerTodo.setStatus("pending");
396
+        when(todoTaskMapper.selectPendingByUserId(101L)).thenReturn(List.of(signerTodo));
397
+        when(approvalRecordMapper.insert(any())).thenReturn(1);
398
+        when(todoTaskMapper.update(any(), any())).thenReturn(1);
399
+
400
+        // 所有会签人已完成
401
+        when(todoTaskMapper.selectPendingByInstanceId(instance.getId())).thenReturn(Collections.emptyList());
402
+        when(processNodeMapper.selectByDefinitionId(1L)).thenReturn(testNodes);
403
+        when(instanceMapper.updateById(any())).thenReturn(1);
404
+
405
+        BpmProcessInstance result = processEngine.countersignApprove(
406
+                "test-uuid", 101L, "会签人A", "review", "同意");
407
+
408
+        assertNotNull(result);
409
+        // 应该流转到下一节点或完成
410
+        assertTrue("completed".equals(result.getStatus()) || "running".equals(result.getStatus()));
411
+    }
412
+
413
+    @Test
414
+    @DisplayName("会签审批 - 无待办权限")
415
+    void countersignApprove_noPermission() {
416
+        BpmProcessInstance instance = createRunningInstance();
417
+        when(instanceMapper.selectOne(any())).thenReturn(instance);
418
+        when(todoTaskMapper.selectPendingByUserId(999L)).thenReturn(Collections.emptyList());
419
+
420
+        assertThrows(BusinessException.class, () ->
421
+                processEngine.countersignApprove("test-uuid", 999L, "无权限人", "review", "同意"));
422
+    }
423
+
424
+    // ==================== 待办查询测试 ====================
425
+
426
+    @Test
427
+    @DisplayName("查询待办列表 - 有数据")
428
+    void getTodoTasks_hasData() {
429
+        BpmTodoTask todo1 = new BpmTodoTask();
430
+        todo1.setId(1L);
431
+        todo1.setStatus("pending");
432
+        BpmTodoTask todo2 = new BpmTodoTask();
433
+        todo2.setId(2L);
434
+        todo2.setStatus("pending");
435
+
436
+        when(todoTaskMapper.selectPendingByUserId(100L)).thenReturn(Arrays.asList(todo1, todo2));
437
+
438
+        List<BpmTodoTask> result = processEngine.getTodoTasks(100L);
439
+        assertEquals(2, result.size());
440
+    }
441
+
442
+    @Test
443
+    @DisplayName("查询待办列表 - 空列表")
444
+    void getTodoTasks_empty() {
445
+        when(todoTaskMapper.selectPendingByUserId(100L)).thenReturn(Collections.emptyList());
446
+
447
+        List<BpmTodoTask> result = processEngine.getTodoTasks(100L);
448
+        assertTrue(result.isEmpty());
449
+    }
450
+
451
+    @Test
452
+    @DisplayName("查询待办数量")
453
+    void getTodoCount() {
454
+        when(todoTaskMapper.countPendingByUserId(100L)).thenReturn(5);
455
+
456
+        Integer count = processEngine.getTodoCount(100L);
457
+        assertEquals(5, count);
458
+    }
459
+
460
+    @Test
461
+    @DisplayName("查询已办列表")
462
+    void getDoneTasks() {
463
+        BpmTodoTask done1 = new BpmTodoTask();
464
+        done1.setId(1L);
465
+        done1.setStatus("completed");
466
+        when(todoTaskMapper.selectDoneByUserId(100L)).thenReturn(List.of(done1));
467
+
468
+        List<BpmTodoTask> result = processEngine.getDoneTasks(100L);
469
+        assertEquals(1, result.size());
470
+        assertEquals("completed", result.get(0).getStatus());
471
+    }
472
+
473
+    @Test
474
+    @DisplayName("查询我发起的流程")
475
+    void getMyInitiated() {
476
+        BpmProcessInstance inst = createRunningInstance();
477
+        when(instanceMapper.selectMyInitiated(1L)).thenReturn(List.of(inst));
478
+
479
+        List<BpmProcessInstance> result = processEngine.getMyInitiated(1L);
480
+        assertEquals(1, result.size());
481
+    }
482
+
483
+    @Test
484
+    @DisplayName("查询审批记录")
485
+    void getApprovalRecords() {
486
+        BpmApprovalRecord record = new BpmApprovalRecord();
487
+        record.setAction("approve");
488
+        when(approvalRecordMapper.selectByInstanceId(100L)).thenReturn(List.of(record));
489
+
490
+        List<BpmApprovalRecord> records = processEngine.getApprovalRecords(100L);
491
+        assertEquals(1, records.size());
492
+    }
493
+
494
+    // ==================== 辅助方法 ====================
495
+
496
+    private BpmProcessInstance createRunningInstance() {
497
+        BpmProcessInstance instance = new BpmProcessInstance();
498
+        instance.setId(100L);
499
+        instance.setInstanceId("test-uuid");
500
+        instance.setDefinitionId(1L);
501
+        instance.setProcessKey("test_process");
502
+        instance.setProcessName("测试流程");
503
+        instance.setTitle("测试流程 - 张三");
504
+        instance.setBusinessKey("BIZ-001");
505
+        instance.setInitiatorId(1L);
506
+        instance.setInitiatorName("张三");
507
+        instance.setCurrentNodeId("review");
508
+        instance.setCurrentNodeName("审核");
509
+        instance.setCurrentAssigneeId(100L);
510
+        instance.setCurrentAssigneeName("审核人");
511
+        instance.setStatus("running");
512
+        instance.setStartedAt(LocalDateTime.now().minusHours(1));
513
+        instance.setCreatedAt(LocalDateTime.now().minusHours(1));
514
+        instance.setPriority(0);
515
+        return instance;
516
+    }
517
+}