Commit d2bf9945f7690760f7f881e2b406c0e41b6d6e85
1 parent
176e8714
feat: 工作流支持审批人撤回
Showing
8 changed files
with
151 additions
and
16 deletions
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
| ... | ... | @@ -97,6 +97,9 @@ public class BpmModelMetaInfoVO { |
| 97 | 97 | @Schema(description = "任务后置通知设置", example = "{}") |
| 98 | 98 | private HttpRequestSetting taskAfterTriggerSetting; |
| 99 | 99 | |
| 100 | + @Schema(description = "允许允许审批人撤回任务", example = "false") | |
| 101 | + private Boolean allowWithdrawTask; | |
| 102 | + | |
| 100 | 103 | @Schema(description = "流程 ID 规则") |
| 101 | 104 | @Data |
| 102 | 105 | @Valid | ... | ... |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java
| ... | ... | @@ -219,6 +219,14 @@ public class BpmTaskController { |
| 219 | 219 | return success(true); |
| 220 | 220 | } |
| 221 | 221 | |
| 222 | + @PutMapping("/withdraw") | |
| 223 | + @Operation(summary = "撤回任务") | |
| 224 | + @PreAuthorize("@ss.hasPermission('bpm:task:update')") | |
| 225 | + public CommonResult<Boolean> withdrawTask(@RequestParam("taskId") String taskId) { | |
| 226 | + taskService.withdrawTask(getLoginUserId(), taskId); | |
| 227 | + return success(true); | |
| 228 | + } | |
| 229 | + | |
| 222 | 230 | @GetMapping("/list-by-parent-task-id") |
| 223 | 231 | @Operation(summary = "获得指定父级任务的子任务列表") // 目前用于,减签的时候,获得子任务列表 |
| 224 | 232 | @Parameter(name = "parentTaskId", description = "父级任务编号", required = true) | ... | ... |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java
| ... | ... | @@ -219,4 +219,9 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { |
| 219 | 219 | @TableField(typeHandler = JacksonTypeHandler.class) |
| 220 | 220 | private BpmModelMetaInfoVO.HttpRequestSetting taskAfterTriggerSetting; |
| 221 | 221 | |
| 222 | + /** | |
| 223 | + * 是否允许审批人撤回任务 | |
| 224 | + */ | |
| 225 | + private Boolean allowWithdrawTask; | |
| 226 | + | |
| 222 | 227 | } | ... | ... |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
| ... | ... | @@ -61,6 +61,10 @@ public interface ErrorCodeConstants { |
| 61 | 61 | ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!"); |
| 62 | 62 | ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); |
| 63 | 63 | ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!"); |
| 64 | + ErrorCode TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING = new ErrorCode(1_009_005_017, "撤回失败,流程实例未运行!"); | |
| 65 | + ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!"); | |
| 66 | + ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!"); | |
| 67 | + ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,下一节点不满足撤回条件!"); | |
| 64 | 68 | |
| 65 | 69 | // ========== 动态表单模块 1-009-010-000 ========== |
| 66 | 70 | ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); | ... | ... |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
| ... | ... | @@ -909,6 +909,48 @@ public class BpmnModelUtils { |
| 909 | 909 | } |
| 910 | 910 | |
| 911 | 911 | /** |
| 912 | + * 查找起始节点下一个用户任务列表列表 | |
| 913 | + * @param source 起始节点 | |
| 914 | + * @return 结果 | |
| 915 | + */ | |
| 916 | + public static List<UserTask> getNextUserTasks(FlowElement source) { | |
| 917 | + return getNextUserTasks(source, null, null); | |
| 918 | + } | |
| 919 | + | |
| 920 | + /** | |
| 921 | + * 查找起始节点下一个用户任务列表列表 | |
| 922 | + * @param source 起始节点 | |
| 923 | + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 | |
| 924 | + * @param userTaskList 用户任务列表 | |
| 925 | + * @return 结果 | |
| 926 | + */ | |
| 927 | + public static List<UserTask> getNextUserTasks(FlowElement source, Set<String> hasSequenceFlow, List<UserTask> userTaskList) { | |
| 928 | + hasSequenceFlow = Optional.ofNullable(hasSequenceFlow).orElse(new HashSet<>()); | |
| 929 | + userTaskList = Optional.ofNullable(userTaskList).orElse(new ArrayList<>()); | |
| 930 | + // 获取出口连线 | |
| 931 | + List<SequenceFlow> sequenceFlows = getElementOutgoingFlows(source); | |
| 932 | + if (!sequenceFlows.isEmpty()) { | |
| 933 | + for (SequenceFlow sequenceFlow : sequenceFlows) { | |
| 934 | + // 如果发现连线重复,说明循环了,跳过这个循环 | |
| 935 | + if (hasSequenceFlow.contains(sequenceFlow.getId())) { | |
| 936 | + continue; | |
| 937 | + } | |
| 938 | + // 添加已经走过的连线 | |
| 939 | + hasSequenceFlow.add(sequenceFlow.getId()); | |
| 940 | + FlowElement targetFlowElement = sequenceFlow.getTargetFlowElement(); | |
| 941 | + if (targetFlowElement instanceof UserTask) { | |
| 942 | + // 若节点为用户任务,加入到结果列表中 | |
| 943 | + userTaskList.add((UserTask) targetFlowElement); | |
| 944 | + } else { | |
| 945 | + // 若节点非用户任务,继续递归查找下一个节点 | |
| 946 | + getNextUserTasks(targetFlowElement, hasSequenceFlow, userTaskList); | |
| 947 | + } | |
| 948 | + } | |
| 949 | + } | |
| 950 | + return userTaskList; | |
| 951 | + } | |
| 952 | + | |
| 953 | + /** | |
| 912 | 954 | * 处理排它网关 |
| 913 | 955 | * |
| 914 | 956 | * @param gateway 排他网关 | ... | ... |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
| ... | ... | @@ -250,6 +250,14 @@ public interface BpmTaskService { |
| 250 | 250 | */ |
| 251 | 251 | void copyTask(Long userId, @Valid BpmTaskCopyReqVO reqVO); |
| 252 | 252 | |
| 253 | + /** | |
| 254 | + * 撤回任务 | |
| 255 | + * | |
| 256 | + * @param userId 用户编号 | |
| 257 | + * @param taskId 任务编号 | |
| 258 | + */ | |
| 259 | + void withdrawTask(Long userId, String taskId); | |
| 260 | + | |
| 253 | 261 | // ========== Event 事件相关方法 ========== |
| 254 | 262 | |
| 255 | 263 | /** | ... | ... |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
| ... | ... | @@ -40,10 +40,9 @@ import jakarta.annotation.Resource; |
| 40 | 40 | import jakarta.validation.Valid; |
| 41 | 41 | import lombok.extern.slf4j.Slf4j; |
| 42 | 42 | import org.flowable.bpmn.model.*; |
| 43 | -import org.flowable.engine.HistoryService; | |
| 44 | -import org.flowable.engine.ManagementService; | |
| 45 | -import org.flowable.engine.RuntimeService; | |
| 46 | -import org.flowable.engine.TaskService; | |
| 43 | +import org.flowable.common.engine.api.FlowableException; | |
| 44 | +import org.flowable.common.engine.api.FlowableObjectNotFoundException; | |
| 45 | +import org.flowable.engine.*; | |
| 47 | 46 | import org.flowable.engine.history.HistoricActivityInstance; |
| 48 | 47 | import org.flowable.engine.runtime.ActivityInstance; |
| 49 | 48 | import org.flowable.engine.runtime.Execution; |
| ... | ... | @@ -62,6 +61,7 @@ import org.springframework.transaction.support.TransactionSynchronization; |
| 62 | 61 | import org.springframework.transaction.support.TransactionSynchronizationManager; |
| 63 | 62 | |
| 64 | 63 | import java.util.*; |
| 64 | +import java.util.stream.Collectors; | |
| 65 | 65 | import java.util.stream.Stream; |
| 66 | 66 | |
| 67 | 67 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| ... | ... | @@ -196,7 +196,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 196 | 196 | /** |
| 197 | 197 | * 获得用户指定 processInstanceId 流程编号下的首个“待办”(未审批、且可审核)的任务 |
| 198 | 198 | * |
| 199 | - * @param userId 用户编号 | |
| 199 | + * @param userId 用户编号 | |
| 200 | 200 | * @param processInstanceId 流程编号 |
| 201 | 201 | * @return 任务 |
| 202 | 202 | */ |
| ... | ... | @@ -599,15 +599,15 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 599 | 599 | |
| 600 | 600 | /** |
| 601 | 601 | * 校验选择的下一个节点的审批人,是否合法 |
| 602 | - * | |
| 602 | + * <p> | |
| 603 | 603 | * 1. 是否有漏选:没有选择审批人 |
| 604 | 604 | * 2. 是否有多选:非下一个节点 |
| 605 | 605 | * |
| 606 | 606 | * @param taskDefinitionKey 当前任务节点标识 |
| 607 | - * @param variables 流程变量 | |
| 608 | - * @param bpmnModel 流程模型 | |
| 609 | - * @param nextAssignees 下一个节点审批人集合(参数) | |
| 610 | - * @param processInstance 流程实例 | |
| 607 | + * @param variables 流程变量 | |
| 608 | + * @param bpmnModel 流程模型 | |
| 609 | + * @param nextAssignees 下一个节点审批人集合(参数) | |
| 610 | + * @param processInstance 流程实例 | |
| 611 | 611 | */ |
| 612 | 612 | @SuppressWarnings("unchecked") |
| 613 | 613 | private Map<String, Object> validateAndSetNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel, |
| ... | ... | @@ -659,7 +659,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 659 | 659 | approveUserSelectAssignees = new HashMap<>(); |
| 660 | 660 | } |
| 661 | 661 | approveUserSelectAssignees.put(nextFlowNode.getId(), assignees); |
| 662 | - Map<String,List<Long>> existingApproveUserSelectAssignees = (Map<String,List<Long>>) variables.get( | |
| 662 | + Map<String, List<Long>> existingApproveUserSelectAssignees = (Map<String, List<Long>>) variables.get( | |
| 663 | 663 | BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES); |
| 664 | 664 | if (CollUtil.isNotEmpty(existingApproveUserSelectAssignees)) { |
| 665 | 665 | approveUserSelectAssignees.putAll(existingApproveUserSelectAssignees); |
| ... | ... | @@ -1177,6 +1177,70 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 1177 | 1177 | processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getReason(), reqVO.getId()); |
| 1178 | 1178 | } |
| 1179 | 1179 | |
| 1180 | + @Override | |
| 1181 | + @Transactional(rollbackFor = Exception.class) | |
| 1182 | + public void withdrawTask(Long userId, String taskId) { | |
| 1183 | + // 1.查询本人已办任务 | |
| 1184 | + HistoricTaskInstance taskInstance = historyService.createHistoricTaskInstanceQuery() | |
| 1185 | + .taskId(taskId).taskAssignee(userId.toString()).finished().singleResult(); | |
| 1186 | + if (ObjectUtil.isNull(taskInstance)) { | |
| 1187 | + throw exception(TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS); | |
| 1188 | + } | |
| 1189 | + // 2.校验流程是否结束 | |
| 1190 | + ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() | |
| 1191 | + .processInstanceId(taskInstance.getProcessInstanceId()) | |
| 1192 | + .active() | |
| 1193 | + .singleResult(); | |
| 1194 | + if (ObjectUtil.isNull(processInstance)) { | |
| 1195 | + throw exception(TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING); | |
| 1196 | + } | |
| 1197 | + // 3.判断此流程是否允许撤回 | |
| 1198 | + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService | |
| 1199 | + .getProcessDefinitionInfo(processInstance.getProcessDefinitionId()); | |
| 1200 | + if (ObjectUtil.isNull(processDefinitionInfo) || !Boolean.TRUE.equals(processDefinitionInfo.getAllowWithdrawTask())) { | |
| 1201 | + throw exception(TASK_WITHDRAW_FAIL_NOT_ALLOW); | |
| 1202 | + } | |
| 1203 | + // 4.判断此任务下一节点是否满足撤回 | |
| 1204 | + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskInstance.getProcessDefinitionId()); | |
| 1205 | + UserTask userTask = (UserTask) BpmnModelUtils.getFlowElementById(bpmnModel, taskInstance.getTaskDefinitionKey()); | |
| 1206 | + List<UserTask> nextUserTaskList = BpmnModelUtils.getNextUserTasks(userTask); | |
| 1207 | + List<String> nextUserTaskKeys = nextUserTaskList.stream().map(UserTask::getId).toList(); | |
| 1208 | + if (CollUtil.isEmpty(nextUserTaskKeys)) { | |
| 1209 | + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); | |
| 1210 | + } | |
| 1211 | + long nextUserTaskFinishedCount = historyService.createHistoricTaskInstanceQuery() | |
| 1212 | + .processInstanceId(processInstance.getProcessInstanceId()) | |
| 1213 | + .taskDefinitionKeys(nextUserTaskKeys) | |
| 1214 | + .taskCreatedAfter(taskInstance.getEndTime()) // TODO @芋艿:是否选择升级flowable版本解决taskCreatedAfter、taskCreatedBefore问题,升级7.1.0可以 | |
| 1215 | + .finished() | |
| 1216 | + .count(); | |
| 1217 | + if (nextUserTaskFinishedCount > 0) { | |
| 1218 | + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); | |
| 1219 | + } | |
| 1220 | + // 5.获取需要撤回的运行任务 | |
| 1221 | + List<Task> runningTaskList = taskService.createTaskQuery() | |
| 1222 | + .processInstanceId(processInstance.getProcessInstanceId()) | |
| 1223 | + .taskDefinitionKeys(nextUserTaskKeys) | |
| 1224 | + .active().list(); | |
| 1225 | + if (CollUtil.isEmpty(runningTaskList)) { | |
| 1226 | + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); | |
| 1227 | + } | |
| 1228 | + List<String> withdrawExecutionIds = new ArrayList<>(); | |
| 1229 | + for (Task task : runningTaskList) { | |
| 1230 | + // 标记撤回任务为取消 | |
| 1231 | + // TODO @芋艿:是否需要添加被撤回状态? | |
| 1232 | + taskService.addComment(task.getId(), taskInstance.getProcessInstanceId(), BpmCommentTypeEnum.CANCEL.getType(), | |
| 1233 | + BpmCommentTypeEnum.CANCEL.formatComment("前一节点撤回")); | |
| 1234 | + updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_WITHDRAW.getReason()); | |
| 1235 | + withdrawExecutionIds.add(task.getExecutionId()); | |
| 1236 | + } | |
| 1237 | + // 6.执行撤回操作 | |
| 1238 | + runtimeService.createChangeActivityStateBuilder() | |
| 1239 | + .processInstanceId(processInstance.getProcessInstanceId()) | |
| 1240 | + .moveExecutionsToSingleActivityId(withdrawExecutionIds, taskInstance.getTaskDefinitionKey()) | |
| 1241 | + .changeState(); | |
| 1242 | + } | |
| 1243 | + | |
| 1180 | 1244 | /** |
| 1181 | 1245 | * 校验任务是否能被减签 |
| 1182 | 1246 | * |
| ... | ... | @@ -1223,7 +1287,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 1223 | 1287 | } |
| 1224 | 1288 | |
| 1225 | 1289 | // 2. 任务前置通知 |
| 1226 | - if (ObjUtil.isNotNull(processDefinitionInfo.getTaskBeforeTriggerSetting())){ | |
| 1290 | + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskBeforeTriggerSetting())) { | |
| 1227 | 1291 | BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskBeforeTriggerSetting(); |
| 1228 | 1292 | BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, |
| 1229 | 1293 | setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); |
| ... | ... | @@ -1350,7 +1414,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 1350 | 1414 | .taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus()) |
| 1351 | 1415 | .finished(); |
| 1352 | 1416 | if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType()) |
| 1353 | - && sameAssigneeQuery.count() > 0) { | |
| 1417 | + && sameAssigneeQuery.count() > 0) { | |
| 1354 | 1418 | getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) |
| 1355 | 1419 | .setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName())); |
| 1356 | 1420 | return; |
| ... | ... | @@ -1362,7 +1426,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 1362 | 1426 | return; |
| 1363 | 1427 | } |
| 1364 | 1428 | List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点 |
| 1365 | - BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), | |
| 1429 | + BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), | |
| 1366 | 1430 | SequenceFlow::getSourceRef); |
| 1367 | 1431 | if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) { |
| 1368 | 1432 | getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) |
| ... | ... | @@ -1387,7 +1451,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 1387 | 1451 | PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); |
| 1388 | 1452 | if (userTaskElement.getId().equals(START_USER_NODE_ID) |
| 1389 | 1453 | && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 |
| 1390 | - || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 | |
| 1454 | + || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 | |
| 1391 | 1455 | && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { |
| 1392 | 1456 | getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) |
| 1393 | 1457 | .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); |
| ... | ... | @@ -1456,7 +1520,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 1456 | 1520 | } |
| 1457 | 1521 | |
| 1458 | 1522 | // 任务后置通知 |
| 1459 | - if (ObjUtil.isNotNull(processDefinitionInfo.getTaskAfterTriggerSetting())){ | |
| 1523 | + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskAfterTriggerSetting())) { | |
| 1460 | 1524 | BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskAfterTriggerSetting(); |
| 1461 | 1525 | BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, |
| 1462 | 1526 | setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); | ... | ... |