diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index b815780..bb9868b 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -66,6 +66,9 @@ public interface ErrorCodeConstants { ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!"); ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!"); ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_020, "撤回失败,下一节点不满足撤回条件!"); + ErrorCode TASK_SIGN_DELETE_FAIL_NOT_SELF = new ErrorCode(1_009_005_021, "任务减签失败,原因:你不是该任务的加签人"); + ErrorCode TASK_OPERATE_FAIL_APPROVING = new ErrorCode(1_009_005_022, "操作失败,原因:该任务已审批通过,等待加签任务审批完成"); + ErrorCode TASK_SIGN_CREATE_TYPE_NOT_EXISTS = new ErrorCode(1_009_005_023, "任务加签失败,加签类型({})不存在"); // ========== 动态表单模块 1-009-010-000 ========== ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 87abd54..b1d045e 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -323,6 +323,18 @@ public class BpmTaskServiceImpl implements BpmTaskService { && ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) { throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); } + // 为什么 assignee 为空、owner 非空时,不允许操作? + // 例如说:【加签】等待中(WAIT)的任务(向前加签的父任务、向后加签的子任务),只有 owner 没有 assignee, + // 需要等待其它任务审批后,才能恢复 assignee 继续审批。此时,任何人都不能直接审批、拒绝它 + if (StrUtil.isBlank(task.getAssignee()) && StrUtil.isNotBlank(task.getOwner())) { + throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + } + // 为什么审批通过中(APPROVING)时,不允许操作? + // 例如说:【向后加签】的父任务已经审批通过,需要等待加签出来的子任务审批完成后自动完成。此时,不能重复审批、拒绝、继续加签等 + Integer status = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS); + if (BpmTaskStatusEnum.APPROVING.getStatus().equals(status)) { + throw exception(TASK_OPERATE_FAIL_APPROVING); + } return task; } @@ -763,11 +775,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 2. 激活子任务 List childrenTaskList = getTaskListByParentTaskId(task.getId()); - for (Task childrenTask : childrenTaskList) { - taskService.resolveTask(childrenTask.getId()); - // 更新子 task 状态 - updateTaskStatus(childrenTask.getId(), BpmTaskStatusEnum.RUNNING.getStatus()); - } + childrenTaskList.forEach(childrenTask -> resolveWaitTask(childrenTask.getId())); } /** @@ -789,8 +797,8 @@ public class BpmTaskServiceImpl implements BpmTaskService { } // 1.2 只处理加签的父任务 Task parentTask = validateTaskExists(parentTaskId); - String scopeType = parentTask.getScopeType(); - if (BpmTaskSignTypeEnum.of(scopeType) == null) { + BpmTaskSignTypeEnum signType = BpmTaskSignTypeEnum.of(parentTask.getScopeType()); + if (signType == null) { return; } @@ -799,14 +807,11 @@ public class BpmTaskServiceImpl implements BpmTaskService { parentTaskImpl.setScopeType(null); taskService.saveTask(parentTaskImpl); - // 3.1 情况一:处理向【向前】加签 - if (BpmTaskSignTypeEnum.BEFORE.getType().equals(scopeType)) { - // 3.1.1 owner 重新赋值给父任务的 assignee,这样它就可以被审批 - taskService.resolveTask(parentTaskId); - // 3.1.2 更新流程任务 status - updateTaskStatus(parentTaskId, BpmTaskStatusEnum.RUNNING.getStatus()); + // 3.1 情况一:处理向【向前】加签,owner 重新赋值给父任务的 assignee,这样它就可以被审批 + if (signType == BpmTaskSignTypeEnum.BEFORE) { + resolveWaitTask(parentTaskId); // 3.2 情况二:处理向【向后】加签 - } else if (BpmTaskSignTypeEnum.AFTER.getType().equals(scopeType)) { + } else { // 只有 parentTask 处于 APPROVING 的情况下,才可以继续 complete 完成 // 否则,一个未审批的 parentTask 任务,在加签出来的任务都被减签的情况下,就直接完成审批,这样会存在问题 Integer status = (Integer) parentTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS); @@ -928,6 +933,31 @@ public class BpmTaskServiceImpl implements BpmTaskService { taskService.setVariableLocal(id, BpmnVariableConstants.TASK_VARIABLE_REASON, reason); } + /** + * 保存【加签】等待中的任务:设置 owner、置空 assignee,并更新状态为 WAIT + *

+ * 适用于向前加签的父任务、向后加签的子任务,它们需要等待其它任务审批后,再通过 {@link #resolveWaitTask(String)} 恢复审批 + * + * @param task 任务 + * @param owner 拥有人,恢复审批时成为 assignee + */ + private void saveWaitTask(TaskEntityImpl task, String owner) { + task.setOwner(owner); + task.setAssignee(null); + taskService.saveTask(task); + updateTaskStatus(task.getId(), BpmTaskStatusEnum.WAIT.getStatus()); + } + + /** + * 恢复【加签】等待中的任务:将 owner 重新赋值给 assignee,并更新状态为 RUNNING,这样它就可以被审批 + * + * @param id 任务编号 + */ + private void resolveWaitTask(String id) { + taskService.resolveTask(id); + updateTaskStatus(id, BpmTaskStatusEnum.RUNNING.getStatus()); + } + @Override @Transactional(rollbackFor = Exception.class) @DataPermission(enable = false) // 关闭数据权限,避免查询不到用户数据。相关案例:https://gitee.com/zhijiantianya/yudao-cloud/issues/ID1UYA @@ -1167,26 +1197,22 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 1. 获取和校验任务 TaskEntityImpl taskEntity = validateTaskCanCreateSign(userId, reqVO); List userList = adminUserApi.getUserList(reqVO.getUserIds()); - if (CollUtil.isEmpty(userList)) { + if (CollUtil.isEmpty(userList) || userList.size() != reqVO.getUserIds().size()) { throw exception(TASK_SIGN_CREATE_USER_NOT_EXIST); } // 2. 处理当前任务 // 2.1 开启计数功能,主要用于为了让表 ACT_RU_TASK 中的 SUB_TASK_COUNT_ 字段记录下总共有多少子任务,后续可能有用 taskEntity.setCountEnabled(true); - // 2.2 向前加签,设置 owner,置空 assign。等子任务都完成后,再调用 resolveTask 重新将 owner 设置为 assign - // 原因是:不能和向前加签的子任务一起审批,需要等前面的子任务都完成才能审批 - if (reqVO.getType().equals(BpmTaskSignTypeEnum.BEFORE.getType())) { - taskEntity.setOwner(taskEntity.getAssignee()); - taskEntity.setAssignee(null); - } - // 2.4 记录加签方式,完成任务时需要用到判断 + // 2.2 记录加签方式,完成任务时需要用到判断 taskEntity.setScopeType(reqVO.getType()); - // 2.5 保存当前任务修改后的值 - taskService.saveTask(taskEntity); - // 2.6 更新 task 状态为 WAIT,只有在向前加签的时候 - if (reqVO.getType().equals(BpmTaskSignTypeEnum.BEFORE.getType())) { - updateTaskStatus(taskEntity.getId(), BpmTaskStatusEnum.WAIT.getStatus()); + // 2.3 保存当前任务修改后的值 + // 向前加签,设置 owner,置空 assignee,状态为 WAIT。等子任务都完成后,再调用 resolveWaitTask 重新将 owner 设置为 assignee + // 原因是:不能和向前加签的子任务一起审批,需要等前面的子任务都完成才能审批 + if (BpmTaskSignTypeEnum.BEFORE.getType().equals(reqVO.getType())) { + saveWaitTask(taskEntity, taskEntity.getAssignee()); + } else { + taskService.saveTask(taskEntity); } // 3. 创建加签任务 @@ -1202,6 +1228,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { /** * 校验任务是否可以加签,主要校验加签类型是否一致: *

+ * 0. 加签类型必须存在 * 1. 如果存在“向前加签”的任务,则不能“向后加签” * 2. 如果存在“向后加签”的任务,则不能“向前加签” * @@ -1210,6 +1237,9 @@ public class BpmTaskServiceImpl implements BpmTaskService { * @return 当前任务 */ private TaskEntityImpl validateTaskCanCreateSign(Long userId, BpmTaskSignCreateReqVO reqVO) { + if (BpmTaskSignTypeEnum.of(reqVO.getType()) == null) { + throw exception(TASK_SIGN_CREATE_TYPE_NOT_EXISTS, reqVO.getType()); + } TaskEntityImpl taskEntity = (TaskEntityImpl) validateTask(userId, reqVO.getId()); // 向前加签和向后加签不能同时存在 if (taskEntity.getScopeType() != null @@ -1260,20 +1290,14 @@ public class BpmTaskServiceImpl implements BpmTaskService { TaskEntityImpl task = (TaskEntityImpl) taskService.newTask(IdUtil.fastSimpleUUID()); BpmTaskConvert.INSTANCE.copyTo(parentTask, task); - // 2.1 向前加签,设置审批人 + // 2.1 向前加签,设置审批人,子任务可以直接审批 if (BpmTaskSignTypeEnum.BEFORE.getType().equals(parentTask.getScopeType())) { task.setAssignee(assignee); - // 2.2 向后加签,设置 owner 不设置 assignee 是因为不能同时审批,需要等父任务完成 - } else { - task.setOwner(assignee); - } - // 2.3 保存子任务 - taskService.saveTask(task); - - // 3. 向后前签,设置子任务的状态为 WAIT,因为需要等父任务审批完 - if (BpmTaskSignTypeEnum.AFTER.getType().equals(parentTask.getScopeType())) { - updateTaskStatus(task.getId(), BpmTaskStatusEnum.WAIT.getStatus()); + taskService.saveTask(task); + return; } + // 2.2 向后加签,设置 owner 不设置 assignee,状态为 WAIT,是因为不能同时审批,需要等父任务审批完 + saveWaitTask(task, assignee); } @Override @@ -1282,7 +1306,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @SuppressWarnings("DataFlowIssue") public void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO) { // 1.1 校验 task 可以被减签 - Task task = validateTaskCanSignDelete(reqVO.getId()); + Task task = validateTaskCanSignDelete(userId, reqVO.getId()); // 1.2 校验取消人存在 AdminUserRespDTO cancelUser = null; if (StrUtil.isNotBlank(task.getAssignee())) { @@ -1376,11 +1400,15 @@ public class BpmTaskServiceImpl implements BpmTaskService { /** * 校验任务是否能被减签 + *

+ * 1. 被减签的任务,必须是通过加签生成的任务 + * 2. 当前用户,必须是父任务(或祖先任务)的审批人、拥有人,即加签人 * - * @param id 任务编号 + * @param userId 当前用户编号 + * @param id 任务编号 * @return 任务信息 */ - private Task validateTaskCanSignDelete(String id) { + private Task validateTaskCanSignDelete(Long userId, String id) { Task task = validateTaskExists(id); if (task.getParentTaskId() == null) { throw exception(TASK_SIGN_DELETE_NO_PARENT); @@ -1392,9 +1420,38 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (BpmTaskSignTypeEnum.of(parentTask.getScopeType()) == null) { throw exception(TASK_SIGN_DELETE_NO_PARENT); } + if (!isSignCreateUser(userId, parentTask)) { + throw exception(TASK_SIGN_DELETE_FAIL_NOT_SELF); + } return task; } + /** + * 判断指定用户,是否为父任务(或祖先任务)的审批人、拥有人 + *

+ * 为什么包含祖先任务?多层加签时,最上层的加签人,可以减签它下面所有层级的加签任务 + * + * @param userId 用户编号 + * @param parentTask 父任务 + * @return 是否 + */ + private boolean isSignCreateUser(Long userId, Task parentTask) { + if (userId == null) { + return false; + } + Task task = parentTask; + for (int i = 0; i < Short.MAX_VALUE && task != null; i++) { + if (isAssignUserTask(userId, task) || isOwnerUserTask(userId, task)) { + return true; + } + if (task.getParentTaskId() == null) { + return false; + } + task = getTask(task.getParentTaskId()); + } + return false; + } + // ========== Event 事件相关方法 ========== @Override @@ -1671,7 +1728,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { log.error("[processTaskTimeout][processInstanceId({}) 没有找到流程实例]", processInstanceId); return; } - List taskList = getRunningTaskListByProcessInstanceId(processInstanceId, true, taskDefineKey); + // 过滤【向后加签】审批通过中(APPROVING)的父任务:它已经审批通过,等待加签任务审批后自动完成,无需超时处理 + List taskList = filterList(getRunningTaskListByProcessInstanceId(processInstanceId, true, taskDefineKey), + task -> ObjectUtil.notEqual(task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS), + BpmTaskStatusEnum.APPROVING.getStatus())); // TODO 优化:未来需要考虑加签的情况 if (CollUtil.isEmpty(taskList)) { log.error("[processTaskTimeout][processInstanceId({}) 定义Key({}) 没有找到任务]", processInstanceId, taskDefineKey); diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmTaskSignFlowableTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmTaskSignFlowableTest.java new file mode 100644 index 0000000..27f8a1a --- /dev/null +++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmTaskSignFlowableTest.java @@ -0,0 +1,435 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable; + +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; +import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl; +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl; +import jakarta.annotation.Resource; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.TaskService; +import org.flowable.engine.repository.Deployment; +import org.flowable.task.api.Task; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link BpmTaskServiceImpl} 加签、减签的 Flowable 集成测试 + * + *

覆盖向前/向后加签、多层加签、加签任务审批不通过、部分减签、递归减签,以及等待中(WAIT)任务、减签的权限校验

+ * + * @author HUIHUI + */ +public class BpmTaskSignFlowableTest extends BaseFlowableUnitTest { + + @Resource + private RepositoryService repositoryService; + @Resource + private TaskService flowableTaskService; + + @Resource + private BpmTaskServiceImpl bpmTaskService; + @Resource + private BpmProcessInstanceServiceImpl processInstanceService; + + private Deployment deployment; + + @BeforeEach + public void setUp() { + // 部署两级审批流程:task1 -> task2;审批人统一由测试上下文计算为用户 1 + deployment = repositoryService.createDeployment() + .addClasspathResource("bpmn/two-step-approve.bpmn20.xml") + .deploy(); + mockFlowableBackedServices(repositoryService); + } + + @AfterEach + public void tearDown() { + // 清理测试数据:加签子任务没有 execution,需要先删除,否则 Flowable 级联删除流程实例时会空指针 + List signTaskIds = flowableTaskService.createTaskQuery().deploymentId(deployment.getId()).list() + .stream().filter(task -> task.getExecutionId() == null).map(Task::getId).toList(); + flowableTaskService.deleteTasks(signTaskIds); + repositoryService.deleteDeployment(deployment.getId(), true); + } + + // ========== 加签 ========== + + @Test + public void testCreateSignTask_beforeMultiLevel_thenApprove() { + // 准备参数:task1 向前加签给用户 2,用户 2 再向前加签给用户 3 + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L).get(0); + Task grandchildTask = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.BEFORE, 3L).get(0); + // 断言:task1、子任务都处于等待中,只有孙任务可以审批 + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE); + assertWaitTask(childTask.getId(), "2", BpmTaskSignTypeEnum.BEFORE); + assertRunningTask(grandchildTask.getId(), "3"); + + // 调用:用户 3 审批孙任务 + approveTask(3L, grandchildTask.getId()); + // 断言:子任务恢复给用户 2 审批,task1 继续等待 + assertRunningTask(childTask.getId(), "2"); + assertNull(bpmTaskService.getTask(childTask.getId()).getScopeType()); + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE); + + // 调用:用户 2 审批子任务 + approveTask(2L, childTask.getId()); + // 断言:task1 恢复给用户 1 审批 + assertRunningTask(task1.getId(), "1"); + assertNull(bpmTaskService.getTask(task1.getId()).getScopeType()); + + // 调用:用户 1 审批 task1 + approveTask(1L, task1.getId()); + // 断言:流程流转到 task2 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey()); + } + + @Test + public void testCreateSignTask_afterMultiLevel_thenApprove() { + // 准备参数:task1 向后加签给用户 2,用户 1 审批后,用户 2 再向后加签给用户 3 + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L).get(0); + assertWaitTask(childTask.getId(), "2", null); + approveTask(1L, task1.getId()); + assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(task1.getId())); + assertRunningTask(childTask.getId(), "2"); + Task grandchildTask = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.AFTER, 3L).get(0); + assertWaitTask(grandchildTask.getId(), "3", null); + + // 调用:用户 2 审批子任务 + approveTask(2L, childTask.getId()); + // 断言:子任务处于审批通过中,孙任务恢复给用户 3 审批 + assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(childTask.getId())); + assertRunningTask(grandchildTask.getId(), "3"); + + // 调用:用户 3 审批孙任务 + approveTask(3L, grandchildTask.getId()); + // 断言:子任务、task1 依次递归完成,流程流转到 task2 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(childTask.getId())); + assertNull(bpmTaskService.getTask(childTask.getId())); + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId())); + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey()); + } + + // ========== 等待中(WAIT)任务的权限 ========== + + @Test + public void testApproveTask_beforeSignParentWait() { + // 准备参数:task1 向前加签给用户 2,task1 只有 owner 没有 assignee + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L).get(0); + + // 调用,并断言异常:任何人(包括 owner、无 userId 的系统调用)都不能直接操作等待中的 task1 + assertServiceException(() -> approveTask(1L, task1.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> approveTask(3L, task1.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> approveTask(null, task1.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> bpmTaskService.rejectTask(3L, new BpmTaskRejectReqVO() + .setId(task1.getId()).setReason("不同意")), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> bpmTaskService.transferTask(3L, new BpmTaskTransferReqVO() + .setId(task1.getId()).setAssigneeUserId(3L).setReason("转办")), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 3L), + TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + // 断言:task1 仍等待子任务审批,流程未结束 + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE); + assertNotNull(processInstanceService.getProcessInstance(processInstanceId)); + + // 调用 + 断言:用户 2 审批子任务后,用户 1 可以正常审批 task1 + approveTask(2L, childTask.getId()); + approveTask(1L, task1.getId()); + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey()); + } + + @Test + public void testApproveTask_afterSignChildWait() { + // 准备参数:task1 向后加签给用户 2,子任务只有 owner 没有 assignee + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L).get(0); + + // 调用,并断言异常:task1 审批前,任何人(包括 owner)都不能直接操作等待中的子任务 + assertServiceException(() -> approveTask(2L, childTask.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> approveTask(3L, childTask.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> approveTask(null, childTask.getId()), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> bpmTaskService.rejectTask(3L, new BpmTaskRejectReqVO() + .setId(childTask.getId()).setReason("不同意")), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + // 断言:子任务仍等待,task1 未审批,流程未结束 + assertWaitTask(childTask.getId(), "2", null); + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(task1.getId())); + assertNotNull(processInstanceService.getProcessInstance(processInstanceId)); + + // 调用 + 断言:用户 1 审批 task1 后,用户 2 可以正常审批子任务 + approveTask(1L, task1.getId()); + approveTask(2L, childTask.getId()); + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId())); + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey()); + } + + @Test + public void testApproveTask_emptyAssigneeAndOwner_withoutUserId() { + // 准备参数:task1 计算不到审批人,没有 assignee、owner + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of()); + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + assertNull(task1.getAssignee()); + assertNull(task1.getOwner()); + + // 调用:保持自动审批的历史行为,userId 为 null 时允许审批 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of(1L)); + approveTask(null, task1.getId()); + // 断言:流程流转到 task2 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId())); + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey()); + } + + // ========== 审批不通过 ========== + + @Test + public void testRejectTask_afterSignChild() { + // 准备参数:task1 向后加签给用户 2,并且用户 1 已审批 task1 + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L).get(0); + approveTask(1L, task1.getId()); + // 断言:审批通过中的 task1,不能再被审批不通过 + assertServiceException(() -> bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO() + .setId(task1.getId()).setReason("不同意")), TASK_OPERATE_FAIL_APPROVING); + + // 调用:用户 2 审批不通过加签任务 + bpmTaskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(childTask.getId()).setReason("不同意")); + // 断言:加签任务、task1 都标记为不通过,流程实例不通过并结束,没有残留的任务 + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(childTask.getId())); + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(task1.getId())); + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId), + eq(BpmCommentTypeEnum.REJECT), eq("加签任务不通过")); + assertProcessInstanceRejected(processInstanceId); + } + + @Test + public void testRejectTask_afterMultiLevelMiddle_afterDeleteSign() { + // 准备参数:task1 向后加签给用户 2、用户 4,用户 1 审批后,用户 2 再向后加签给用户 3 + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Map childTasks = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L, 4L).stream() + .collect(Collectors.toMap(Task::getOwner, Function.identity())); + approveTask(1L, task1.getId()); + Task childTask = childTasks.get("2"); + Task grandchildTask = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.AFTER, 3L).get(0); + + // 调用:用户 1 减签用户 4 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTasks.get("4").getId()) + .setReason("减签")); + // 断言:用户 4 的任务被取消,task1 仍处于审批通过中,等待用户 2 审批 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTasks.get("4").getId())); + assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(task1.getId())); + assertRunningTask(childTask.getId(), "2"); + assertWaitTask(grandchildTask.getId(), "3", null); + + // 调用:用户 2(中间节点)审批不通过 + bpmTaskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(childTask.getId()).setReason("不同意")); + // 断言:中间节点、task1 标记为不通过,等待中的孙任务被取消,流程实例不通过并结束,没有残留的任务 + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(childTask.getId())); + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(task1.getId())); + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(grandchildTask.getId())); + assertProcessInstanceRejected(processInstanceId); + } + + // ========== 减签 ========== + + @Test + public void testDeleteSignTask_afterPartial_thenApprove() { + // 准备参数:task1 向后加签给用户 2、用户 3,并且用户 1 已审批 task1 + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Map childTasks = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L, 3L).stream() + .collect(Collectors.toMap(Task::getOwner, Function.identity())); + approveTask(1L, task1.getId()); + + // 调用:用户 1 减签用户 2 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTasks.get("2").getId()) + .setReason("减签")); + // 断言:用户 2 的任务被取消,task1 仍处于审批通过中,等待用户 3 审批 + assertNull(bpmTaskService.getTask(childTasks.get("2").getId())); + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTasks.get("2").getId())); + assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(task1.getId())); + assertEquals(BpmTaskSignTypeEnum.AFTER.getType(), bpmTaskService.getTask(task1.getId()).getScopeType()); + assertRunningTask(childTasks.get("3").getId(), "3"); + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId), + eq(BpmCommentTypeEnum.SUB_SIGN), eq("用户1"), eq("用户2")); + + // 调用:用户 3 审批 + approveTask(3L, childTasks.get("3").getId()); + // 断言:task1 审批通过,流程流转到 task2 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId())); + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey()); + } + + @Test + public void testDeleteSignTask_afterLast_completeApprovingParent() { + // 准备参数:task1 向后加签给用户 2,并且用户 1 已审批 task1 + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.AFTER, 2L).get(0); + approveTask(1L, task1.getId()); + + // 调用:减签最后一个加签任务 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTask.getId()).setReason("减签")); + // 断言:审批通过中的 task1 直接完成,流程流转到 task2 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTask.getId())); + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), getTaskStatus(task1.getId())); + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey()); + } + + @Test + public void testDeleteSignTask_recursive() { + // 准备参数:task1 向前加签给用户 2,用户 2 再向前加签给用户 3 + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L).get(0); + Task grandchildTask = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.BEFORE, 3L).get(0); + + // 调用:用户 1 减签用户 2 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTask.getId()).setReason("减签")); + // 断言:子任务、孙任务都被取消删除,task1 恢复给用户 1 审批 + assertNull(bpmTaskService.getTask(childTask.getId())); + assertNull(bpmTaskService.getTask(grandchildTask.getId())); + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTask.getId())); + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(grandchildTask.getId())); + assertRunningTask(task1.getId(), "1"); + assertNull(bpmTaskService.getTask(task1.getId()).getScopeType()); + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId), + eq(BpmCommentTypeEnum.SUB_SIGN), eq("用户1"), eq("用户2")); + + // 调用 + 断言:用户 1 审批,流程流转到 task2 + approveTask(1L, task1.getId()); + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey()); + } + + @Test + public void testDeleteSignTask_notSignUser() { + // 准备参数:task1 向前加签给用户 2、用户 3 + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Map childTasks = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L, 3L).stream() + .collect(Collectors.toMap(Task::getAssignee, Function.identity())); + String childTaskId = childTasks.get("2").getId(); + + // 调用,并断言异常:其它加签人、被减签人自己、无关用户、无 userId,都不能减签 + for (Long userId : new Long[]{3L, 2L, 99L, null}) { + assertServiceException(() -> bpmTaskService.deleteSignTask(userId, new BpmTaskSignDeleteReqVO() + .setId(childTaskId).setReason("减签")), TASK_SIGN_DELETE_FAIL_NOT_SELF); + } + // 断言:加签任务都还在,task1 继续等待 + assertRunningTask(childTaskId, "2"); + assertEquals(2, bpmTaskService.getTaskListByParentTaskId(task1.getId()).size()); + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE); + } + + @Test + public void testDeleteSignTask_byParentAndAncestorSignUser() { + // 准备参数:task1 向前加签给用户 2,用户 2 再向前加签给用户 3、用户 4 + String processInstanceId = startProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Task childTask = createSignTask(1L, task1.getId(), BpmTaskSignTypeEnum.BEFORE, 2L).get(0); + Map grandchildTasks = createSignTask(2L, childTask.getId(), BpmTaskSignTypeEnum.BEFORE, 3L, 4L) + .stream().collect(Collectors.toMap(Task::getAssignee, Function.identity())); + + // 调用:用户 2(父任务的加签人)减签用户 3 + bpmTaskService.deleteSignTask(2L, new BpmTaskSignDeleteReqVO().setId(grandchildTasks.get("3").getId()) + .setReason("减签")); + // 断言:用户 3 的任务被取消,子任务继续等待用户 4 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(grandchildTasks.get("3").getId())); + assertWaitTask(childTask.getId(), "2", BpmTaskSignTypeEnum.BEFORE); + + // 调用:用户 1(祖先任务的加签人)减签用户 4 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(grandchildTasks.get("4").getId()) + .setReason("减签")); + // 断言:子任务恢复给用户 2 审批,task1 继续等待 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(grandchildTasks.get("4").getId())); + assertRunningTask(childTask.getId(), "2"); + assertWaitTask(task1.getId(), "1", BpmTaskSignTypeEnum.BEFORE); + } + + // ========== 私有方法 ========== + + private String startProcessInstance() { + return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO() + .setProcessDefinitionKey("twoStepApprove").setBusinessKey("business-1")); + } + + private Task getRunningTask(String processInstanceId) { + List tasks = bpmTaskService.getRunningTaskListByProcessInstanceId(processInstanceId, null, null); + assertEquals(1, tasks.size()); + return tasks.get(0); + } + + private List createSignTask(Long userId, String taskId, BpmTaskSignTypeEnum type, Long... userIds) { + bpmTaskService.createSignTask(userId, new BpmTaskSignCreateReqVO().setId(taskId) + .setType(type.getType()).setUserIds(Set.of(userIds)).setReason("加签")); + List childTasks = bpmTaskService.getTaskListByParentTaskId(taskId); + assertEquals(userIds.length, childTasks.size()); + return childTasks; + } + + private void approveTask(Long userId, String taskId) { + bpmTaskService.approveTask(userId, new BpmTaskApproveReqVO().setId(taskId).setReason("同意")); + } + + private Integer getTaskStatus(String taskId) { + return (Integer) bpmTaskService.getHistoricTask(taskId).getTaskLocalVariables() + .get(BpmnVariableConstants.TASK_VARIABLE_STATUS); + } + + /** + * 断言流程实例不通过并结束,并且没有残留的任务(包括没有 execution 的加签任务) + */ + private void assertProcessInstanceRejected(String processInstanceId) { + assertNull(processInstanceService.getProcessInstance(processInstanceId)); + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), processInstanceService + .getHistoricProcessInstance(processInstanceId).getProcessVariables() + .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS)); + assertEquals(0, flowableTaskService.createTaskQuery().deploymentId(deployment.getId()).count()); + } + + /** + * 断言任务处于等待中:只有 owner 没有 assignee,状态为 WAIT + */ + private void assertWaitTask(String taskId, String owner, BpmTaskSignTypeEnum signType) { + Task task = bpmTaskService.getTask(taskId); + assertEquals(owner, task.getOwner()); + assertNull(task.getAssignee()); + assertEquals(signType != null ? signType.getType() : null, task.getScopeType()); + assertEquals(BpmTaskStatusEnum.WAIT.getStatus(), getTaskStatus(taskId)); + } + + /** + * 断言任务可以被 assignee 审批:状态为 RUNNING + */ + private void assertRunningTask(String taskId, String assignee) { + Task task = bpmTaskService.getTask(taskId); + assertEquals(assignee, task.getAssignee()); + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(taskId)); + } + +} diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImplTest.java index cab1b9d..6934443 100644 --- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImplTest.java +++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImplTest.java @@ -156,6 +156,33 @@ public class BpmTaskServiceImplTest extends BaseMockitoUnitTest { } @Test + public void testValidateTask_ownerOnly() { + // 准备参数:加签等待中的任务,只有 owner 没有 assignee + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTaskExists("task-1"); + when(task.getAssignee()).thenReturn(null); + when(task.getOwner()).thenReturn("1"); + + // 调用,并断言异常:owner 自己、其它用户、无 userId 都不允许操作 + assertServiceException(() -> spyService.validateTask(1L, "task-1"), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> spyService.validateTask(2L, "task-1"), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + assertServiceException(() -> spyService.validateTask(null, "task-1"), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + } + + @Test + public void testValidateTask_approving() { + // 准备参数:向后加签的父任务,已审批通过,等待加签任务审批 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTaskExists("task-1"); + when(task.getAssignee()).thenReturn("1"); + when(task.getTaskLocalVariables()).thenReturn(Map.of(BpmnVariableConstants.TASK_VARIABLE_STATUS, + BpmTaskStatusEnum.APPROVING.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> spyService.validateTask(1L, "task-1"), TASK_OPERATE_FAIL_APPROVING); + } + + @Test public void testValidateTaskExists_notExists() { // 准备参数 BpmTaskServiceImpl spyService = spy(taskService); @@ -443,6 +470,14 @@ public class BpmTaskServiceImplTest extends BaseMockitoUnitTest { } @Test + public void testCreateSignTask_typeNotExists() { + // 调用,并断言异常 + assertServiceException(() -> taskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId("task-1") + .setType("unknown").setUserIds(Set.of(2L)).setReason("加签")), + TASK_SIGN_CREATE_TYPE_NOT_EXISTS, "unknown"); + } + + @Test public void testDeleteSignTask_parentNotExists() { // 准备参数 BpmTaskServiceImpl spyService = spy(taskService);