Commit 8dc18cb413e8a4d6b966e24ec420f4afbef15425

Authored by YunaiV
1 parent e0f0cab7

fix(bpm): 优化加减签状态流转并补充 Flowable 单测

yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
@@ -66,6 +66,9 @@ public interface ErrorCodeConstants { @@ -66,6 +66,9 @@ public interface ErrorCodeConstants {
66 ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!"); 66 ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!");
67 ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!"); 67 ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!");
68 ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_020, "撤回失败,下一节点不满足撤回条件!"); 68 ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_020, "撤回失败,下一节点不满足撤回条件!");
  69 + ErrorCode TASK_SIGN_DELETE_FAIL_NOT_SELF = new ErrorCode(1_009_005_021, "任务减签失败,原因:你不是该任务的加签人");
  70 + ErrorCode TASK_OPERATE_FAIL_APPROVING = new ErrorCode(1_009_005_022, "操作失败,原因:该任务已审批通过,等待加签任务审批完成");
  71 + ErrorCode TASK_SIGN_CREATE_TYPE_NOT_EXISTS = new ErrorCode(1_009_005_023, "任务加签失败,加签类型({})不存在");
69 72
70 // ========== 动态表单模块 1-009-010-000 ========== 73 // ========== 动态表单模块 1-009-010-000 ==========
71 ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); 74 ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在");
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
@@ -323,6 +323,18 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -323,6 +323,18 @@ public class BpmTaskServiceImpl implements BpmTaskService {
323 && ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) { 323 && ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) {
324 throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); 324 throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
325 } 325 }
  326 + // 为什么 assignee 为空、owner 非空时,不允许操作?
  327 + // 例如说:【加签】等待中(WAIT)的任务(向前加签的父任务、向后加签的子任务),只有 owner 没有 assignee,
  328 + // 需要等待其它任务审批后,才能恢复 assignee 继续审批。此时,任何人都不能直接审批、拒绝它
  329 + if (StrUtil.isBlank(task.getAssignee()) && StrUtil.isNotBlank(task.getOwner())) {
  330 + throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  331 + }
  332 + // 为什么审批通过中(APPROVING)时,不允许操作?
  333 + // 例如说:【向后加签】的父任务已经审批通过,需要等待加签出来的子任务审批完成后自动完成。此时,不能重复审批、拒绝、继续加签等
  334 + Integer status = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
  335 + if (BpmTaskStatusEnum.APPROVING.getStatus().equals(status)) {
  336 + throw exception(TASK_OPERATE_FAIL_APPROVING);
  337 + }
326 return task; 338 return task;
327 } 339 }
328 340
@@ -763,11 +775,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -763,11 +775,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
763 775
764 // 2. 激活子任务 776 // 2. 激活子任务
765 List<Task> childrenTaskList = getTaskListByParentTaskId(task.getId()); 777 List<Task> childrenTaskList = getTaskListByParentTaskId(task.getId());
766 - for (Task childrenTask : childrenTaskList) {  
767 - taskService.resolveTask(childrenTask.getId());  
768 - // 更新子 task 状态  
769 - updateTaskStatus(childrenTask.getId(), BpmTaskStatusEnum.RUNNING.getStatus());  
770 - } 778 + childrenTaskList.forEach(childrenTask -> resolveWaitTask(childrenTask.getId()));
771 } 779 }
772 780
773 /** 781 /**
@@ -789,8 +797,8 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -789,8 +797,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
789 } 797 }
790 // 1.2 只处理加签的父任务 798 // 1.2 只处理加签的父任务
791 Task parentTask = validateTaskExists(parentTaskId); 799 Task parentTask = validateTaskExists(parentTaskId);
792 - String scopeType = parentTask.getScopeType();  
793 - if (BpmTaskSignTypeEnum.of(scopeType) == null) { 800 + BpmTaskSignTypeEnum signType = BpmTaskSignTypeEnum.of(parentTask.getScopeType());
  801 + if (signType == null) {
794 return; 802 return;
795 } 803 }
796 804
@@ -799,14 +807,11 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -799,14 +807,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
799 parentTaskImpl.setScopeType(null); 807 parentTaskImpl.setScopeType(null);
800 taskService.saveTask(parentTaskImpl); 808 taskService.saveTask(parentTaskImpl);
801 809
802 - // 3.1 情况一:处理向【向前】加签  
803 - if (BpmTaskSignTypeEnum.BEFORE.getType().equals(scopeType)) {  
804 - // 3.1.1 owner 重新赋值给父任务的 assignee,这样它就可以被审批  
805 - taskService.resolveTask(parentTaskId);  
806 - // 3.1.2 更新流程任务 status  
807 - updateTaskStatus(parentTaskId, BpmTaskStatusEnum.RUNNING.getStatus()); 810 + // 3.1 情况一:处理向【向前】加签,owner 重新赋值给父任务的 assignee,这样它就可以被审批
  811 + if (signType == BpmTaskSignTypeEnum.BEFORE) {
  812 + resolveWaitTask(parentTaskId);
808 // 3.2 情况二:处理向【向后】加签 813 // 3.2 情况二:处理向【向后】加签
809 - } else if (BpmTaskSignTypeEnum.AFTER.getType().equals(scopeType)) { 814 + } else {
810 // 只有 parentTask 处于 APPROVING 的情况下,才可以继续 complete 完成 815 // 只有 parentTask 处于 APPROVING 的情况下,才可以继续 complete 完成
811 // 否则,一个未审批的 parentTask 任务,在加签出来的任务都被减签的情况下,就直接完成审批,这样会存在问题 816 // 否则,一个未审批的 parentTask 任务,在加签出来的任务都被减签的情况下,就直接完成审批,这样会存在问题
812 Integer status = (Integer) parentTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS); 817 Integer status = (Integer) parentTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
@@ -928,6 +933,31 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -928,6 +933,31 @@ public class BpmTaskServiceImpl implements BpmTaskService {
928 taskService.setVariableLocal(id, BpmnVariableConstants.TASK_VARIABLE_REASON, reason); 933 taskService.setVariableLocal(id, BpmnVariableConstants.TASK_VARIABLE_REASON, reason);
929 } 934 }
930 935
  936 + /**
  937 + * 保存【加签】等待中的任务:设置 owner、置空 assignee,并更新状态为 WAIT
  938 + * <p>
  939 + * 适用于向前加签的父任务、向后加签的子任务,它们需要等待其它任务审批后,再通过 {@link #resolveWaitTask(String)} 恢复审批
  940 + *
  941 + * @param task 任务
  942 + * @param owner 拥有人,恢复审批时成为 assignee
  943 + */
  944 + private void saveWaitTask(TaskEntityImpl task, String owner) {
  945 + task.setOwner(owner);
  946 + task.setAssignee(null);
  947 + taskService.saveTask(task);
  948 + updateTaskStatus(task.getId(), BpmTaskStatusEnum.WAIT.getStatus());
  949 + }
  950 +
  951 + /**
  952 + * 恢复【加签】等待中的任务:将 owner 重新赋值给 assignee,并更新状态为 RUNNING,这样它就可以被审批
  953 + *
  954 + * @param id 任务编号
  955 + */
  956 + private void resolveWaitTask(String id) {
  957 + taskService.resolveTask(id);
  958 + updateTaskStatus(id, BpmTaskStatusEnum.RUNNING.getStatus());
  959 + }
  960 +
931 @Override 961 @Override
932 @Transactional(rollbackFor = Exception.class) 962 @Transactional(rollbackFor = Exception.class)
933 @DataPermission(enable = false) // 关闭数据权限,避免查询不到用户数据。相关案例:https://gitee.com/zhijiantianya/yudao-cloud/issues/ID1UYA 963 @DataPermission(enable = false) // 关闭数据权限,避免查询不到用户数据。相关案例:https://gitee.com/zhijiantianya/yudao-cloud/issues/ID1UYA
@@ -1167,26 +1197,22 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -1167,26 +1197,22 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1167 // 1. 获取和校验任务 1197 // 1. 获取和校验任务
1168 TaskEntityImpl taskEntity = validateTaskCanCreateSign(userId, reqVO); 1198 TaskEntityImpl taskEntity = validateTaskCanCreateSign(userId, reqVO);
1169 List<AdminUserRespDTO> userList = adminUserApi.getUserList(reqVO.getUserIds()); 1199 List<AdminUserRespDTO> userList = adminUserApi.getUserList(reqVO.getUserIds());
1170 - if (CollUtil.isEmpty(userList)) { 1200 + if (CollUtil.isEmpty(userList) || userList.size() != reqVO.getUserIds().size()) {
1171 throw exception(TASK_SIGN_CREATE_USER_NOT_EXIST); 1201 throw exception(TASK_SIGN_CREATE_USER_NOT_EXIST);
1172 } 1202 }
1173 1203
1174 // 2. 处理当前任务 1204 // 2. 处理当前任务
1175 // 2.1 开启计数功能,主要用于为了让表 ACT_RU_TASK 中的 SUB_TASK_COUNT_ 字段记录下总共有多少子任务,后续可能有用 1205 // 2.1 开启计数功能,主要用于为了让表 ACT_RU_TASK 中的 SUB_TASK_COUNT_ 字段记录下总共有多少子任务,后续可能有用
1176 taskEntity.setCountEnabled(true); 1206 taskEntity.setCountEnabled(true);
1177 - // 2.2 向前加签,设置 owner,置空 assign。等子任务都完成后,再调用 resolveTask 重新将 owner 设置为 assign  
1178 - // 原因是:不能和向前加签的子任务一起审批,需要等前面的子任务都完成才能审批  
1179 - if (reqVO.getType().equals(BpmTaskSignTypeEnum.BEFORE.getType())) {  
1180 - taskEntity.setOwner(taskEntity.getAssignee());  
1181 - taskEntity.setAssignee(null);  
1182 - }  
1183 - // 2.4 记录加签方式,完成任务时需要用到判断 1207 + // 2.2 记录加签方式,完成任务时需要用到判断
1184 taskEntity.setScopeType(reqVO.getType()); 1208 taskEntity.setScopeType(reqVO.getType());
1185 - // 2.5 保存当前任务修改后的值  
1186 - taskService.saveTask(taskEntity);  
1187 - // 2.6 更新 task 状态为 WAIT,只有在向前加签的时候  
1188 - if (reqVO.getType().equals(BpmTaskSignTypeEnum.BEFORE.getType())) {  
1189 - updateTaskStatus(taskEntity.getId(), BpmTaskStatusEnum.WAIT.getStatus()); 1209 + // 2.3 保存当前任务修改后的值
  1210 + // 向前加签,设置 owner,置空 assignee,状态为 WAIT。等子任务都完成后,再调用 resolveWaitTask 重新将 owner 设置为 assignee
  1211 + // 原因是:不能和向前加签的子任务一起审批,需要等前面的子任务都完成才能审批
  1212 + if (BpmTaskSignTypeEnum.BEFORE.getType().equals(reqVO.getType())) {
  1213 + saveWaitTask(taskEntity, taskEntity.getAssignee());
  1214 + } else {
  1215 + taskService.saveTask(taskEntity);
1190 } 1216 }
1191 1217
1192 // 3. 创建加签任务 1218 // 3. 创建加签任务
@@ -1202,6 +1228,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -1202,6 +1228,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1202 /** 1228 /**
1203 * 校验任务是否可以加签,主要校验加签类型是否一致: 1229 * 校验任务是否可以加签,主要校验加签类型是否一致:
1204 * <p> 1230 * <p>
  1231 + * 0. 加签类型必须存在
1205 * 1. 如果存在“向前加签”的任务,则不能“向后加签” 1232 * 1. 如果存在“向前加签”的任务,则不能“向后加签”
1206 * 2. 如果存在“向后加签”的任务,则不能“向前加签” 1233 * 2. 如果存在“向后加签”的任务,则不能“向前加签”
1207 * 1234 *
@@ -1210,6 +1237,9 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -1210,6 +1237,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1210 * @return 当前任务 1237 * @return 当前任务
1211 */ 1238 */
1212 private TaskEntityImpl validateTaskCanCreateSign(Long userId, BpmTaskSignCreateReqVO reqVO) { 1239 private TaskEntityImpl validateTaskCanCreateSign(Long userId, BpmTaskSignCreateReqVO reqVO) {
  1240 + if (BpmTaskSignTypeEnum.of(reqVO.getType()) == null) {
  1241 + throw exception(TASK_SIGN_CREATE_TYPE_NOT_EXISTS, reqVO.getType());
  1242 + }
1213 TaskEntityImpl taskEntity = (TaskEntityImpl) validateTask(userId, reqVO.getId()); 1243 TaskEntityImpl taskEntity = (TaskEntityImpl) validateTask(userId, reqVO.getId());
1214 // 向前加签和向后加签不能同时存在 1244 // 向前加签和向后加签不能同时存在
1215 if (taskEntity.getScopeType() != null 1245 if (taskEntity.getScopeType() != null
@@ -1260,20 +1290,14 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -1260,20 +1290,14 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1260 TaskEntityImpl task = (TaskEntityImpl) taskService.newTask(IdUtil.fastSimpleUUID()); 1290 TaskEntityImpl task = (TaskEntityImpl) taskService.newTask(IdUtil.fastSimpleUUID());
1261 BpmTaskConvert.INSTANCE.copyTo(parentTask, task); 1291 BpmTaskConvert.INSTANCE.copyTo(parentTask, task);
1262 1292
1263 - // 2.1 向前加签,设置审批人 1293 + // 2.1 向前加签,设置审批人,子任务可以直接审批
1264 if (BpmTaskSignTypeEnum.BEFORE.getType().equals(parentTask.getScopeType())) { 1294 if (BpmTaskSignTypeEnum.BEFORE.getType().equals(parentTask.getScopeType())) {
1265 task.setAssignee(assignee); 1295 task.setAssignee(assignee);
1266 - // 2.2 向后加签,设置 owner 不设置 assignee 是因为不能同时审批,需要等父任务完成  
1267 - } else {  
1268 - task.setOwner(assignee);  
1269 - }  
1270 - // 2.3 保存子任务  
1271 - taskService.saveTask(task);  
1272 -  
1273 - // 3. 向后前签,设置子任务的状态为 WAIT,因为需要等父任务审批完  
1274 - if (BpmTaskSignTypeEnum.AFTER.getType().equals(parentTask.getScopeType())) {  
1275 - updateTaskStatus(task.getId(), BpmTaskStatusEnum.WAIT.getStatus()); 1296 + taskService.saveTask(task);
  1297 + return;
1276 } 1298 }
  1299 + // 2.2 向后加签,设置 owner 不设置 assignee,状态为 WAIT,是因为不能同时审批,需要等父任务审批完
  1300 + saveWaitTask(task, assignee);
1277 } 1301 }
1278 1302
1279 @Override 1303 @Override
@@ -1282,7 +1306,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -1282,7 +1306,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1282 @SuppressWarnings("DataFlowIssue") 1306 @SuppressWarnings("DataFlowIssue")
1283 public void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO) { 1307 public void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO) {
1284 // 1.1 校验 task 可以被减签 1308 // 1.1 校验 task 可以被减签
1285 - Task task = validateTaskCanSignDelete(reqVO.getId()); 1309 + Task task = validateTaskCanSignDelete(userId, reqVO.getId());
1286 // 1.2 校验取消人存在 1310 // 1.2 校验取消人存在
1287 AdminUserRespDTO cancelUser = null; 1311 AdminUserRespDTO cancelUser = null;
1288 if (StrUtil.isNotBlank(task.getAssignee())) { 1312 if (StrUtil.isNotBlank(task.getAssignee())) {
@@ -1376,11 +1400,15 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -1376,11 +1400,15 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1376 1400
1377 /** 1401 /**
1378 * 校验任务是否能被减签 1402 * 校验任务是否能被减签
  1403 + * <p>
  1404 + * 1. 被减签的任务,必须是通过加签生成的任务
  1405 + * 2. 当前用户,必须是父任务(或祖先任务)的审批人、拥有人,即加签人
1379 * 1406 *
1380 - * @param id 任务编号 1407 + * @param userId 当前用户编号
  1408 + * @param id 任务编号
1381 * @return 任务信息 1409 * @return 任务信息
1382 */ 1410 */
1383 - private Task validateTaskCanSignDelete(String id) { 1411 + private Task validateTaskCanSignDelete(Long userId, String id) {
1384 Task task = validateTaskExists(id); 1412 Task task = validateTaskExists(id);
1385 if (task.getParentTaskId() == null) { 1413 if (task.getParentTaskId() == null) {
1386 throw exception(TASK_SIGN_DELETE_NO_PARENT); 1414 throw exception(TASK_SIGN_DELETE_NO_PARENT);
@@ -1392,9 +1420,38 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -1392,9 +1420,38 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1392 if (BpmTaskSignTypeEnum.of(parentTask.getScopeType()) == null) { 1420 if (BpmTaskSignTypeEnum.of(parentTask.getScopeType()) == null) {
1393 throw exception(TASK_SIGN_DELETE_NO_PARENT); 1421 throw exception(TASK_SIGN_DELETE_NO_PARENT);
1394 } 1422 }
  1423 + if (!isSignCreateUser(userId, parentTask)) {
  1424 + throw exception(TASK_SIGN_DELETE_FAIL_NOT_SELF);
  1425 + }
1395 return task; 1426 return task;
1396 } 1427 }
1397 1428
  1429 + /**
  1430 + * 判断指定用户,是否为父任务(或祖先任务)的审批人、拥有人
  1431 + * <p>
  1432 + * 为什么包含祖先任务?多层加签时,最上层的加签人,可以减签它下面所有层级的加签任务
  1433 + *
  1434 + * @param userId 用户编号
  1435 + * @param parentTask 父任务
  1436 + * @return 是否
  1437 + */
  1438 + private boolean isSignCreateUser(Long userId, Task parentTask) {
  1439 + if (userId == null) {
  1440 + return false;
  1441 + }
  1442 + Task task = parentTask;
  1443 + for (int i = 0; i < Short.MAX_VALUE && task != null; i++) {
  1444 + if (isAssignUserTask(userId, task) || isOwnerUserTask(userId, task)) {
  1445 + return true;
  1446 + }
  1447 + if (task.getParentTaskId() == null) {
  1448 + return false;
  1449 + }
  1450 + task = getTask(task.getParentTaskId());
  1451 + }
  1452 + return false;
  1453 + }
  1454 +
1398 // ========== Event 事件相关方法 ========== 1455 // ========== Event 事件相关方法 ==========
1399 1456
1400 @Override 1457 @Override
@@ -1671,7 +1728,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -1671,7 +1728,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1671 log.error("[processTaskTimeout][processInstanceId({}) 没有找到流程实例]", processInstanceId); 1728 log.error("[processTaskTimeout][processInstanceId({}) 没有找到流程实例]", processInstanceId);
1672 return; 1729 return;
1673 } 1730 }
1674 - List<Task> taskList = getRunningTaskListByProcessInstanceId(processInstanceId, true, taskDefineKey); 1731 + // 过滤【向后加签】审批通过中(APPROVING)的父任务:它已经审批通过,等待加签任务审批后自动完成,无需超时处理
  1732 + List<Task> taskList = filterList(getRunningTaskListByProcessInstanceId(processInstanceId, true, taskDefineKey),
  1733 + task -> ObjectUtil.notEqual(task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS),
  1734 + BpmTaskStatusEnum.APPROVING.getStatus()));
1675 // TODO 优化:未来需要考虑加签的情况 1735 // TODO 优化:未来需要考虑加签的情况
1676 if (CollUtil.isEmpty(taskList)) { 1736 if (CollUtil.isEmpty(taskList)) {
1677 log.error("[processTaskTimeout][processInstanceId({}) 定义Key({}) 没有找到任务]", processInstanceId, taskDefineKey); 1737 log.error("[processTaskTimeout][processInstanceId({}) 定义Key({}) 没有找到任务]", processInstanceId, taskDefineKey);
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmTaskSignFlowableTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  4 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
  5 +import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
  6 +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
  7 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
  8 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  9 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
  10 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
  11 +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
  12 +import jakarta.annotation.Resource;
  13 +import org.flowable.engine.RepositoryService;
  14 +import org.flowable.engine.TaskService;
  15 +import org.flowable.engine.repository.Deployment;
  16 +import org.flowable.task.api.Task;
  17 +import org.junit.jupiter.api.AfterEach;
  18 +import org.junit.jupiter.api.BeforeEach;
  19 +import org.junit.jupiter.api.Test;
  20 +
  21 +import java.util.List;
  22 +import java.util.Map;
  23 +import java.util.Set;
  24 +import java.util.function.Function;
  25 +import java.util.stream.Collectors;
  26 +
  27 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  28 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
  29 +import static org.junit.jupiter.api.Assertions.*;
  30 +import static org.mockito.ArgumentMatchers.any;
  31 +import static org.mockito.ArgumentMatchers.eq;
  32 +import static org.mockito.Mockito.verify;
  33 +import static org.mockito.Mockito.when;
  34 +
  35 +/**
  36 + * {@link BpmTaskServiceImpl} 加签、减签的 Flowable 集成测试
  37 + *
  38 + * <p>覆盖向前/向后加签、多层加签、加签任务审批不通过、部分减签、递归减签,以及等待中(WAIT)任务、减签的权限校验</p>
  39 + *
  40 + * @author HUIHUI
  41 + */
  42 +public class BpmTaskSignFlowableTest extends BaseFlowableUnitTest {
  43 +
  44 + @Resource
  45 + private RepositoryService repositoryService;
  46 + @Resource
  47 + private TaskService flowableTaskService;
  48 +
  49 + @Resource
  50 + private BpmTaskServiceImpl bpmTaskService;
  51 + @Resource
  52 + private BpmProcessInstanceServiceImpl processInstanceService;
  53 +
  54 + private Deployment deployment;
  55 +
  56 + @BeforeEach
  57 + public void setUp() {
  58 + // 部署两级审批流程:task1 -> task2;审批人统一由测试上下文计算为用户 1
  59 + deployment = repositoryService.createDeployment()
  60 + .addClasspathResource("bpmn/two-step-approve.bpmn20.xml")
  61 + .deploy();
  62 + mockFlowableBackedServices(repositoryService);
  63 + }
  64 +
  65 + @AfterEach
  66 + public void tearDown() {
  67 + // 清理测试数据:加签子任务没有 execution,需要先删除,否则 Flowable 级联删除流程实例时会空指针
  68 + List<String> signTaskIds = flowableTaskService.createTaskQuery().deploymentId(deployment.getId()).list()
  69 + .stream().filter(task -> task.getExecutionId() == null).map(Task::getId).toList();
  70 + flowableTaskService.deleteTasks(signTaskIds);
  71 + repositoryService.deleteDeployment(deployment.getId(), true);
  72 + }
  73 +
  74 + // ========== 加签 ==========
  75 +
  76 + @Test
  77 + public void testCreateSignTask_beforeMultiLevel_thenApprove() {
  78 + // 准备参数:task1 向前加签给用户 2,用户 2 再向前加签给用户 3
  79 + String processInstanceId = startProcessInstance();
  80 + Task task1 = getRunningTask(processInstanceId);
  81 + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L).get(0);
  82 + Task grandchildTask = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.BEFORE, 3L).get(0);
  83 + // 断言:task1、子任务都处于等待中,只有孙任务可以审批
  84 + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE);
  85 + assertWaitTask(childTask.getId(), "2", BpmTaskSignTypeEnum.BEFORE);
  86 + assertRunningTask(grandchildTask.getId(), "3");
  87 +
  88 + // 调用:用户 3 审批孙任务
  89 + approveTask(3L, grandchildTask.getId());
  90 + // 断言:子任务恢复给用户 2 审批,task1 继续等待
  91 + assertRunningTask(childTask.getId(), "2");
  92 + assertNull(bpmTaskService.getTask(childTask.getId()).getScopeType());
  93 + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE);
  94 +
  95 + // 调用:用户 2 审批子任务
  96 + approveTask(2L, childTask.getId());
  97 + // 断言:task1 恢复给用户 1 审批
  98 + assertRunningTask(task1.getId(), "1");
  99 + assertNull(bpmTaskService.getTask(task1.getId()).getScopeType());
  100 +
  101 + // 调用:用户 1 审批 task1
  102 + approveTask(1L, task1.getId());
  103 + // 断言:流程流转到 task2
  104 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  105 + }
  106 +
  107 + @Test
  108 + public void testCreateSignTask_afterMultiLevel_thenApprove() {
  109 + // 准备参数:task1 向后加签给用户 2,用户 1 审批后,用户 2 再向后加签给用户 3
  110 + String processInstanceId = startProcessInstance();
  111 + Task task1 = getRunningTask(processInstanceId);
  112 + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L).get(0);
  113 + assertWaitTask(childTask.getId(), "2", null);
  114 + approveTask(1L, task1.getId());
  115 + assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(task1.getId()));
  116 + assertRunningTask(childTask.getId(), "2");
  117 + Task grandchildTask = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.AFTER, 3L).get(0);
  118 + assertWaitTask(grandchildTask.getId(), "3", null);
  119 +
  120 + // 调用:用户 2 审批子任务
  121 + approveTask(2L, childTask.getId());
  122 + // 断言:子任务处于审批通过中,孙任务恢复给用户 3 审批
  123 + assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(childTask.getId()));
  124 + assertRunningTask(grandchildTask.getId(), "3");
  125 +
  126 + // 调用:用户 3 审批孙任务
  127 + approveTask(3L, grandchildTask.getId());
  128 + // 断言:子任务、task1 依次递归完成,流程流转到 task2
  129 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(childTask.getId()));
  130 + assertNull(bpmTaskService.getTask(childTask.getId()));
  131 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId()));
  132 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  133 + }
  134 +
  135 + // ========== 等待中(WAIT)任务的权限 ==========
  136 +
  137 + @Test
  138 + public void testApproveTask_beforeSignParentWait() {
  139 + // 准备参数:task1 向前加签给用户 2,task1 只有 owner 没有 assignee
  140 + String processInstanceId = startProcessInstance();
  141 + Task task1 = getRunningTask(processInstanceId);
  142 + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L).get(0);
  143 +
  144 + // 调用,并断言异常:任何人(包括 owner、无 userId 的系统调用)都不能直接操作等待中的 task1
  145 + assertServiceException(() -> approveTask(1L, task1.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  146 + assertServiceException(() -> approveTask(3L, task1.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  147 + assertServiceException(() -> approveTask(null, task1.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  148 + assertServiceException(() -> bpmTaskService.rejectTask(3L, new BpmTaskRejectReqVO()
  149 + .setId(task1.getId()).setReason("不同意")), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  150 + assertServiceException(() -> bpmTaskService.transferTask(3L, new BpmTaskTransferReqVO()
  151 + .setId(task1.getId()).setAssigneeUserId(3L).setReason("转办")), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  152 + assertServiceException(() -> createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 3L),
  153 + TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  154 + // 断言:task1 仍等待子任务审批,流程未结束
  155 + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE);
  156 + assertNotNull(processInstanceService.getProcessInstance(processInstanceId));
  157 +
  158 + // 调用 + 断言:用户 2 审批子任务后,用户 1 可以正常审批 task1
  159 + approveTask(2L, childTask.getId());
  160 + approveTask(1L, task1.getId());
  161 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  162 + }
  163 +
  164 + @Test
  165 + public void testApproveTask_afterSignChildWait() {
  166 + // 准备参数:task1 向后加签给用户 2,子任务只有 owner 没有 assignee
  167 + String processInstanceId = startProcessInstance();
  168 + Task task1 = getRunningTask(processInstanceId);
  169 + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L).get(0);
  170 +
  171 + // 调用,并断言异常:task1 审批前,任何人(包括 owner)都不能直接操作等待中的子任务
  172 + assertServiceException(() -> approveTask(2L, childTask.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  173 + assertServiceException(() -> approveTask(3L, childTask.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  174 + assertServiceException(() -> approveTask(null, childTask.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  175 + assertServiceException(() -> bpmTaskService.rejectTask(3L, new BpmTaskRejectReqVO()
  176 + .setId(childTask.getId()).setReason("不同意")), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  177 + // 断言:子任务仍等待,task1 未审批,流程未结束
  178 + assertWaitTask(childTask.getId(), "2", null);
  179 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(task1.getId()));
  180 + assertNotNull(processInstanceService.getProcessInstance(processInstanceId));
  181 +
  182 + // 调用 + 断言:用户 1 审批 task1 后,用户 2 可以正常审批子任务
  183 + approveTask(1L, task1.getId());
  184 + approveTask(2L, childTask.getId());
  185 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId()));
  186 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  187 + }
  188 +
  189 + @Test
  190 + public void testApproveTask_emptyAssigneeAndOwner_withoutUserId() {
  191 + // 准备参数:task1 计算不到审批人,没有 assignee、owner
  192 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of());
  193 + String processInstanceId = startProcessInstance();
  194 + Task task1 = getRunningTask(processInstanceId);
  195 + assertNull(task1.getAssignee());
  196 + assertNull(task1.getOwner());
  197 +
  198 + // 调用:保持自动审批的历史行为,userId 为 null 时允许审批
  199 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of(1L));
  200 + approveTask(null, task1.getId());
  201 + // 断言:流程流转到 task2
  202 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId()));
  203 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  204 + }
  205 +
  206 + // ========== 审批不通过 ==========
  207 +
  208 + @Test
  209 + public void testRejectTask_afterSignChild() {
  210 + // 准备参数:task1 向后加签给用户 2,并且用户 1 已审批 task1
  211 + String processInstanceId = startProcessInstance();
  212 + Task task1 = getRunningTask(processInstanceId);
  213 + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L).get(0);
  214 + approveTask(1L, task1.getId());
  215 + // 断言:审批通过中的 task1,不能再被审批不通过
  216 + assertServiceException(() -> bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO()
  217 + .setId(task1.getId()).setReason("不同意")), TASK_OPERATE_FAIL_APPROVING);
  218 +
  219 + // 调用:用户 2 审批不通过加签任务
  220 + bpmTaskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(childTask.getId()).setReason("不同意"));
  221 + // 断言:加签任务、task1 都标记为不通过,流程实例不通过并结束,没有残留的任务
  222 + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(childTask.getId()));
  223 + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(task1.getId()));
  224 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  225 + eq(BpmCommentTypeEnum.REJECT), eq("加签任务不通过"));
  226 + assertProcessInstanceRejected(processInstanceId);
  227 + }
  228 +
  229 + @Test
  230 + public void testRejectTask_afterMultiLevelMiddle_afterDeleteSign() {
  231 + // 准备参数:task1 向后加签给用户 2、用户 4,用户 1 审批后,用户 2 再向后加签给用户 3
  232 + String processInstanceId = startProcessInstance();
  233 + Task task1 = getRunningTask(processInstanceId);
  234 + Map<String, Task> childTasks = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L, 4L).stream()
  235 + .collect(Collectors.toMap(Task::getOwner, Function.identity()));
  236 + approveTask(1L, task1.getId());
  237 + Task childTask = childTasks.get("2");
  238 + Task grandchildTask = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.AFTER, 3L).get(0);
  239 +
  240 + // 调用:用户 1 减签用户 4
  241 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTasks.get("4").getId())
  242 + .setReason("减签"));
  243 + // 断言:用户 4 的任务被取消,task1 仍处于审批通过中,等待用户 2 审批
  244 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTasks.get("4").getId()));
  245 + assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(task1.getId()));
  246 + assertRunningTask(childTask.getId(), "2");
  247 + assertWaitTask(grandchildTask.getId(), "3", null);
  248 +
  249 + // 调用:用户 2(中间节点)审批不通过
  250 + bpmTaskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(childTask.getId()).setReason("不同意"));
  251 + // 断言:中间节点、task1 标记为不通过,等待中的孙任务被取消,流程实例不通过并结束,没有残留的任务
  252 + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(childTask.getId()));
  253 + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(task1.getId()));
  254 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(grandchildTask.getId()));
  255 + assertProcessInstanceRejected(processInstanceId);
  256 + }
  257 +
  258 + // ========== 减签 ==========
  259 +
  260 + @Test
  261 + public void testDeleteSignTask_afterPartial_thenApprove() {
  262 + // 准备参数:task1 向后加签给用户 2、用户 3,并且用户 1 已审批 task1
  263 + String processInstanceId = startProcessInstance();
  264 + Task task1 = getRunningTask(processInstanceId);
  265 + Map<String, Task> childTasks = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L, 3L).stream()
  266 + .collect(Collectors.toMap(Task::getOwner, Function.identity()));
  267 + approveTask(1L, task1.getId());
  268 +
  269 + // 调用:用户 1 减签用户 2
  270 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTasks.get("2").getId())
  271 + .setReason("减签"));
  272 + // 断言:用户 2 的任务被取消,task1 仍处于审批通过中,等待用户 3 审批
  273 + assertNull(bpmTaskService.getTask(childTasks.get("2").getId()));
  274 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTasks.get("2").getId()));
  275 + assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(task1.getId()));
  276 + assertEquals(BpmTaskSignTypeEnum.AFTER.getType(), bpmTaskService.getTask(task1.getId()).getScopeType());
  277 + assertRunningTask(childTasks.get("3").getId(), "3");
  278 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  279 + eq(BpmCommentTypeEnum.SUB_SIGN), eq("用户1"), eq("用户2"));
  280 +
  281 + // 调用:用户 3 审批
  282 + approveTask(3L, childTasks.get("3").getId());
  283 + // 断言:task1 审批通过,流程流转到 task2
  284 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId()));
  285 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  286 + }
  287 +
  288 + @Test
  289 + public void testDeleteSignTask_afterLast_completeApprovingParent() {
  290 + // 准备参数:task1 向后加签给用户 2,并且用户 1 已审批 task1
  291 + String processInstanceId = startProcessInstance();
  292 + Task task1 = getRunningTask(processInstanceId);
  293 + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L).get(0);
  294 + approveTask(1L, task1.getId());
  295 +
  296 + // 调用:减签最后一个加签任务
  297 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTask.getId()).setReason("减签"));
  298 + // 断言:审批通过中的 task1 直接完成,流程流转到 task2
  299 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTask.getId()));
  300 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId()));
  301 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  302 + }
  303 +
  304 + @Test
  305 + public void testDeleteSignTask_recursive() {
  306 + // 准备参数:task1 向前加签给用户 2,用户 2 再向前加签给用户 3
  307 + String processInstanceId = startProcessInstance();
  308 + Task task1 = getRunningTask(processInstanceId);
  309 + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L).get(0);
  310 + Task grandchildTask = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.BEFORE, 3L).get(0);
  311 +
  312 + // 调用:用户 1 减签用户 2
  313 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTask.getId()).setReason("减签"));
  314 + // 断言:子任务、孙任务都被取消删除,task1 恢复给用户 1 审批
  315 + assertNull(bpmTaskService.getTask(childTask.getId()));
  316 + assertNull(bpmTaskService.getTask(grandchildTask.getId()));
  317 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTask.getId()));
  318 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(grandchildTask.getId()));
  319 + assertRunningTask(task1.getId(), "1");
  320 + assertNull(bpmTaskService.getTask(task1.getId()).getScopeType());
  321 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  322 + eq(BpmCommentTypeEnum.SUB_SIGN), eq("用户1"), eq("用户2"));
  323 +
  324 + // 调用 + 断言:用户 1 审批,流程流转到 task2
  325 + approveTask(1L, task1.getId());
  326 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  327 + }
  328 +
  329 + @Test
  330 + public void testDeleteSignTask_notSignUser() {
  331 + // 准备参数:task1 向前加签给用户 2、用户 3
  332 + String processInstanceId = startProcessInstance();
  333 + Task task1 = getRunningTask(processInstanceId);
  334 + Map<String, Task> childTasks = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L, 3L).stream()
  335 + .collect(Collectors.toMap(Task::getAssignee, Function.identity()));
  336 + String childTaskId = childTasks.get("2").getId();
  337 +
  338 + // 调用,并断言异常:其它加签人、被减签人自己、无关用户、无 userId,都不能减签
  339 + for (Long userId : new Long[]{3L, 2L, 99L, null}) {
  340 + assertServiceException(() -> bpmTaskService.deleteSignTask(userId, new BpmTaskSignDeleteReqVO()
  341 + .setId(childTaskId).setReason("减签")), TASK_SIGN_DELETE_FAIL_NOT_SELF);
  342 + }
  343 + // 断言:加签任务都还在,task1 继续等待
  344 + assertRunningTask(childTaskId, "2");
  345 + assertEquals(2, bpmTaskService.getTaskListByParentTaskId(task1.getId()).size());
  346 + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE);
  347 + }
  348 +
  349 + @Test
  350 + public void testDeleteSignTask_byParentAndAncestorSignUser() {
  351 + // 准备参数:task1 向前加签给用户 2,用户 2 再向前加签给用户 3、用户 4
  352 + String processInstanceId = startProcessInstance();
  353 + Task task1 = getRunningTask(processInstanceId);
  354 + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L).get(0);
  355 + Map<String, Task> grandchildTasks = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.BEFORE, 3L, 4L)
  356 + .stream().collect(Collectors.toMap(Task::getAssignee, Function.identity()));
  357 +
  358 + // 调用:用户 2(父任务的加签人)减签用户 3
  359 + bpmTaskService.deleteSignTask(2L, new BpmTaskSignDeleteReqVO().setId(grandchildTasks.get("3").getId())
  360 + .setReason("减签"));
  361 + // 断言:用户 3 的任务被取消,子任务继续等待用户 4
  362 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(grandchildTasks.get("3").getId()));
  363 + assertWaitTask(childTask.getId(), "2", BpmTaskSignTypeEnum.BEFORE);
  364 +
  365 + // 调用:用户 1(祖先任务的加签人)减签用户 4
  366 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(grandchildTasks.get("4").getId())
  367 + .setReason("减签"));
  368 + // 断言:子任务恢复给用户 2 审批,task1 继续等待
  369 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(grandchildTasks.get("4").getId()));
  370 + assertRunningTask(childTask.getId(), "2");
  371 + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE);
  372 + }
  373 +
  374 + // ========== 私有方法 ==========
  375 +
  376 + private String startProcessInstance() {
  377 + return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
  378 + .setProcessDefinitionKey("twoStepApprove").setBusinessKey("business-1"));
  379 + }
  380 +
  381 + private Task getRunningTask(String processInstanceId) {
  382 + List<Task> tasks = bpmTaskService.getRunningTaskListByProcessInstanceId(processInstanceId, null, null);
  383 + assertEquals(1, tasks.size());
  384 + return tasks.get(0);
  385 + }
  386 +
  387 + private List<Task> createSignTask(Long userId, String taskId, BpmTaskSignTypeEnum type, Long... userIds) {
  388 + bpmTaskService.createSignTask(userId, new BpmTaskSignCreateReqVO().setId(taskId)
  389 + .setType(type.getType()).setUserIds(Set.of(userIds)).setReason("加签"));
  390 + List<Task> childTasks = bpmTaskService.getTaskListByParentTaskId(taskId);
  391 + assertEquals(userIds.length, childTasks.size());
  392 + return childTasks;
  393 + }
  394 +
  395 + private void approveTask(Long userId, String taskId) {
  396 + bpmTaskService.approveTask(userId, new BpmTaskApproveReqVO().setId(taskId).setReason("同意"));
  397 + }
  398 +
  399 + private Integer getTaskStatus(String taskId) {
  400 + return (Integer) bpmTaskService.getHistoricTask(taskId).getTaskLocalVariables()
  401 + .get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
  402 + }
  403 +
  404 + /**
  405 + * 断言流程实例不通过并结束,并且没有残留的任务(包括没有 execution 的加签任务)
  406 + */
  407 + private void assertProcessInstanceRejected(String processInstanceId) {
  408 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  409 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), processInstanceService
  410 + .getHistoricProcessInstance(processInstanceId).getProcessVariables()
  411 + .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  412 + assertEquals(0, flowableTaskService.createTaskQuery().deploymentId(deployment.getId()).count());
  413 + }
  414 +
  415 + /**
  416 + * 断言任务处于等待中:只有 owner 没有 assignee,状态为 WAIT
  417 + */
  418 + private void assertWaitTask(String taskId, String owner, BpmTaskSignTypeEnum signType) {
  419 + Task task = bpmTaskService.getTask(taskId);
  420 + assertEquals(owner, task.getOwner());
  421 + assertNull(task.getAssignee());
  422 + assertEquals(signType != null ? signType.getType() : null, task.getScopeType());
  423 + assertEquals(BpmTaskStatusEnum.WAIT.getStatus(), getTaskStatus(taskId));
  424 + }
  425 +
  426 + /**
  427 + * 断言任务可以被 assignee 审批:状态为 RUNNING
  428 + */
  429 + private void assertRunningTask(String taskId, String assignee) {
  430 + Task task = bpmTaskService.getTask(taskId);
  431 + assertEquals(assignee, task.getAssignee());
  432 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(taskId));
  433 + }
  434 +
  435 +}
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImplTest.java
@@ -156,6 +156,33 @@ public class BpmTaskServiceImplTest extends BaseMockitoUnitTest { @@ -156,6 +156,33 @@ public class BpmTaskServiceImplTest extends BaseMockitoUnitTest {
156 } 156 }
157 157
158 @Test 158 @Test
  159 + public void testValidateTask_ownerOnly() {
  160 + // 准备参数:加签等待中的任务,只有 owner 没有 assignee
  161 + BpmTaskServiceImpl spyService = spy(taskService);
  162 + doReturn(task).when(spyService).validateTaskExists("task-1");
  163 + when(task.getAssignee()).thenReturn(null);
  164 + when(task.getOwner()).thenReturn("1");
  165 +
  166 + // 调用,并断言异常:owner 自己、其它用户、无 userId 都不允许操作
  167 + assertServiceException(() -> spyService.validateTask(1L, "task-1"), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  168 + assertServiceException(() -> spyService.validateTask(2L, "task-1"), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  169 + assertServiceException(() -> spyService.validateTask(null, "task-1"), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  170 + }
  171 +
  172 + @Test
  173 + public void testValidateTask_approving() {
  174 + // 准备参数:向后加签的父任务,已审批通过,等待加签任务审批
  175 + BpmTaskServiceImpl spyService = spy(taskService);
  176 + doReturn(task).when(spyService).validateTaskExists("task-1");
  177 + when(task.getAssignee()).thenReturn("1");
  178 + when(task.getTaskLocalVariables()).thenReturn(Map.of(BpmnVariableConstants.TASK_VARIABLE_STATUS,
  179 + BpmTaskStatusEnum.APPROVING.getStatus()));
  180 +
  181 + // 调用,并断言异常
  182 + assertServiceException(() -> spyService.validateTask(1L, "task-1"), TASK_OPERATE_FAIL_APPROVING);
  183 + }
  184 +
  185 + @Test
159 public void testValidateTaskExists_notExists() { 186 public void testValidateTaskExists_notExists() {
160 // 准备参数 187 // 准备参数
161 BpmTaskServiceImpl spyService = spy(taskService); 188 BpmTaskServiceImpl spyService = spy(taskService);
@@ -443,6 +470,14 @@ public class BpmTaskServiceImplTest extends BaseMockitoUnitTest { @@ -443,6 +470,14 @@ public class BpmTaskServiceImplTest extends BaseMockitoUnitTest {
443 } 470 }
444 471
445 @Test 472 @Test
  473 + public void testCreateSignTask_typeNotExists() {
  474 + // 调用,并断言异常
  475 + assertServiceException(() -> taskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId("task-1")
  476 + .setType("unknown").setUserIds(Set.of(2L)).setReason("加签")),
  477 + TASK_SIGN_CREATE_TYPE_NOT_EXISTS, "unknown");
  478 + }
  479 +
  480 + @Test
446 public void testDeleteSignTask_parentNotExists() { 481 public void testDeleteSignTask_parentNotExists() {
447 // 准备参数 482 // 准备参数
448 BpmTaskServiceImpl spyService = spy(taskService); 483 BpmTaskServiceImpl spyService = spy(taskService);