diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java index d2316f58eb..dca75851e9 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java +++ b/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 { @Schema(description = "任务后置通知设置", example = "{}") private HttpRequestSetting taskAfterTriggerSetting; + @Schema(description = "允许允许审批人撤回任务", example = "false") + private Boolean allowWithdrawTask; + @Schema(description = "流程 ID 规则") @Data @Valid diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java index b796c5c17e..b327b8e777 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java @@ -219,6 +219,14 @@ public class BpmTaskController { return success(true); } + @PutMapping("/withdraw") + @Operation(summary = "撤回任务") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult withdrawTask(@RequestParam("taskId") String taskId) { + taskService.withdrawTask(getLoginUserId(), taskId); + return success(true); + } + @GetMapping("/list-by-parent-task-id") @Operation(summary = "获得指定父级任务的子任务列表") // 目前用于,减签的时候,获得子任务列表 @Parameter(name = "parentTaskId", description = "父级任务编号", required = true) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java index c2799ef67f..7f10bda388 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java +++ b/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 { @TableField(typeHandler = JacksonTypeHandler.class) private BpmModelMetaInfoVO.HttpRequestSetting taskAfterTriggerSetting; + /** + * 是否允许审批人撤回任务 + */ + private Boolean allowWithdrawTask; + } 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 4f7d7bf7ce..dabedb5463 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 @@ -61,6 +61,10 @@ public interface ErrorCodeConstants { ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!"); ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!"); + ErrorCode TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING = new ErrorCode(1_009_005_017, "撤回失败,流程实例未运行!"); + 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_019, "撤回失败,下一节点不满足撤回条件!"); // ========== 动态表单模块 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/enums/task/BpmReasonEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java index 46d1482a5e..6ce6f65b82 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java @@ -35,6 +35,7 @@ public enum BpmReasonEnum { APPROVE_TYPE_AUTO_APPROVE("非人工审核,自动通过"), APPROVE_TYPE_AUTO_REJECT("非人工审核,自动不通过"), CANCEL_BY_PROCESS_CLEAN("进程清理自动取消"), + CANCEL_BY_WITHDRAW("前一任务撤回,系统自动取消"), ; private final String reason; diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index 1bafa578db..978693c0c8 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -908,6 +908,48 @@ public class BpmnModelUtils { return nextFlowNodes; } + /** + * 查找起始节点下一个用户任务列表列表 + * @param source 起始节点 + * @return 结果 + */ + public static List getNextUserTasks(FlowElement source) { + return getNextUserTasks(source, null, null); + } + + /** + * 查找起始节点下一个用户任务列表列表 + * @param source 起始节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 用户任务列表 + * @return 结果 + */ + public static List getNextUserTasks(FlowElement source, Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = Optional.ofNullable(hasSequenceFlow).orElse(new HashSet<>()); + userTaskList = Optional.ofNullable(userTaskList).orElse(new ArrayList<>()); + // 获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (!sequenceFlows.isEmpty()) { + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + FlowElement targetFlowElement = sequenceFlow.getTargetFlowElement(); + if (targetFlowElement instanceof UserTask) { + // 若节点为用户任务,加入到结果列表中 + userTaskList.add((UserTask) targetFlowElement); + } else { + // 若节点非用户任务,继续递归查找下一个节点 + getNextUserTasks(targetFlowElement, hasSequenceFlow, userTaskList); + } + } + } + return userTaskList; + } + /** * 处理排它网关 * diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java index 0a5c866fda..34db2876fa 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java @@ -250,6 +250,14 @@ public interface BpmTaskService { */ void copyTask(Long userId, @Valid BpmTaskCopyReqVO reqVO); + /** + * 撤回任务 + * + * @param userId 用户编号 + * @param taskId 任务编号 + */ + void withdrawTask(Long userId, String taskId); + // ========== Event 事件相关方法 ========== /** 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 8ea607a773..d75a91c12b 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 @@ -40,10 +40,9 @@ import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.*; -import org.flowable.engine.HistoryService; -import org.flowable.engine.ManagementService; -import org.flowable.engine.RuntimeService; -import org.flowable.engine.TaskService; +import org.flowable.common.engine.api.FlowableException; +import org.flowable.common.engine.api.FlowableObjectNotFoundException; +import org.flowable.engine.*; import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.engine.runtime.ActivityInstance; import org.flowable.engine.runtime.Execution; @@ -62,6 +61,7 @@ import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -196,7 +196,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { /** * 获得用户指定 processInstanceId 流程编号下的首个“待办”(未审批、且可审核)的任务 * - * @param userId 用户编号 + * @param userId 用户编号 * @param processInstanceId 流程编号 * @return 任务 */ @@ -599,15 +599,15 @@ public class BpmTaskServiceImpl implements BpmTaskService { /** * 校验选择的下一个节点的审批人,是否合法 - * + *

* 1. 是否有漏选:没有选择审批人 * 2. 是否有多选:非下一个节点 * * @param taskDefinitionKey 当前任务节点标识 - * @param variables 流程变量 - * @param bpmnModel 流程模型 - * @param nextAssignees 下一个节点审批人集合(参数) - * @param processInstance 流程实例 + * @param variables 流程变量 + * @param bpmnModel 流程模型 + * @param nextAssignees 下一个节点审批人集合(参数) + * @param processInstance 流程实例 */ @SuppressWarnings("unchecked") private Map validateAndSetNextAssignees(String taskDefinitionKey, Map variables, BpmnModel bpmnModel, @@ -659,7 +659,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { approveUserSelectAssignees = new HashMap<>(); } approveUserSelectAssignees.put(nextFlowNode.getId(), assignees); - Map> existingApproveUserSelectAssignees = (Map>) variables.get( + Map> existingApproveUserSelectAssignees = (Map>) variables.get( BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES); if (CollUtil.isNotEmpty(existingApproveUserSelectAssignees)) { approveUserSelectAssignees.putAll(existingApproveUserSelectAssignees); @@ -1177,6 +1177,70 @@ public class BpmTaskServiceImpl implements BpmTaskService { processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getReason(), reqVO.getId()); } + @Override + @Transactional(rollbackFor = Exception.class) + public void withdrawTask(Long userId, String taskId) { + // 1.查询本人已办任务 + HistoricTaskInstance taskInstance = historyService.createHistoricTaskInstanceQuery() + .taskId(taskId).taskAssignee(userId.toString()).finished().singleResult(); + if (ObjectUtil.isNull(taskInstance)) { + throw exception(TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS); + } + // 2.校验流程是否结束 + ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() + .processInstanceId(taskInstance.getProcessInstanceId()) + .active() + .singleResult(); + if (ObjectUtil.isNull(processInstance)) { + throw exception(TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING); + } + // 3.判断此流程是否允许撤回 + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService + .getProcessDefinitionInfo(processInstance.getProcessDefinitionId()); + if (ObjectUtil.isNull(processDefinitionInfo) || !Boolean.TRUE.equals(processDefinitionInfo.getAllowWithdrawTask())) { + throw exception(TASK_WITHDRAW_FAIL_NOT_ALLOW); + } + // 4.判断此任务下一节点是否满足撤回 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskInstance.getProcessDefinitionId()); + UserTask userTask = (UserTask) BpmnModelUtils.getFlowElementById(bpmnModel, taskInstance.getTaskDefinitionKey()); + List nextUserTaskList = BpmnModelUtils.getNextUserTasks(userTask); + List nextUserTaskKeys = nextUserTaskList.stream().map(UserTask::getId).toList(); + if (CollUtil.isEmpty(nextUserTaskKeys)) { + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); + } + long nextUserTaskFinishedCount = historyService.createHistoricTaskInstanceQuery() + .processInstanceId(processInstance.getProcessInstanceId()) + .taskDefinitionKeys(nextUserTaskKeys) + .taskCreatedAfter(taskInstance.getEndTime()) // TODO @芋艿:是否选择升级flowable版本解决taskCreatedAfter、taskCreatedBefore问题,升级7.1.0可以 + .finished() + .count(); + if (nextUserTaskFinishedCount > 0) { + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); + } + // 5.获取需要撤回的运行任务 + List runningTaskList = taskService.createTaskQuery() + .processInstanceId(processInstance.getProcessInstanceId()) + .taskDefinitionKeys(nextUserTaskKeys) + .active().list(); + if (CollUtil.isEmpty(runningTaskList)) { + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); + } + List withdrawExecutionIds = new ArrayList<>(); + for (Task task : runningTaskList) { + // 标记撤回任务为取消 + // TODO @芋艿:是否需要添加被撤回状态? + taskService.addComment(task.getId(), taskInstance.getProcessInstanceId(), BpmCommentTypeEnum.CANCEL.getType(), + BpmCommentTypeEnum.CANCEL.formatComment("前一节点撤回")); + updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_WITHDRAW.getReason()); + withdrawExecutionIds.add(task.getExecutionId()); + } + // 6.执行撤回操作 + runtimeService.createChangeActivityStateBuilder() + .processInstanceId(processInstance.getProcessInstanceId()) + .moveExecutionsToSingleActivityId(withdrawExecutionIds, taskInstance.getTaskDefinitionKey()) + .changeState(); + } + /** * 校验任务是否能被减签 * @@ -1223,7 +1287,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } // 2. 任务前置通知 - if (ObjUtil.isNotNull(processDefinitionInfo.getTaskBeforeTriggerSetting())){ + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskBeforeTriggerSetting())) { BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskBeforeTriggerSetting(); BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); @@ -1350,7 +1414,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { .taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus()) .finished(); if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType()) - && sameAssigneeQuery.count() > 0) { + && sameAssigneeQuery.count() > 0) { getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) .setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName())); return; @@ -1362,7 +1426,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { return; } List sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点 - BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), + BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), SequenceFlow::getSourceRef); if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) { getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) @@ -1387,7 +1451,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); if (userTaskElement.getId().equals(START_USER_NODE_ID) && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 - || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 + || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); @@ -1456,7 +1520,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } // 任务后置通知 - if (ObjUtil.isNotNull(processDefinitionInfo.getTaskAfterTriggerSetting())){ + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskAfterTriggerSetting())) { BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskAfterTriggerSetting(); BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse());