diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/ModelUtils.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/ModelUtils.java new file mode 100644 index 000000000..84bd0342a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/ModelUtils.java @@ -0,0 +1,274 @@ +package cn.iocoder.yudao.framework.flowable.core.util; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.Process; +import org.flowable.bpmn.model.*; +import org.flowable.common.engine.impl.util.io.StringStreamSource; + +import java.util.*; + +/** + * 流程模型转操作工具类 + */ +public class ModelUtils { + + /** + * 根据节点,获取入口连线 + * + * @param source 起始节点 + * @return 入口连线列表 + */ + public static List getElementIncomingFlows(FlowElement source) { + List sequenceFlows = new ArrayList<>(); + if (source instanceof FlowNode) { + sequenceFlows = ((FlowNode) source).getIncomingFlows(); + } + return sequenceFlows; + } + + + /** + * 根据节点,获取出口连线 + * + * @param source 起始节点 + * @return 出口连线列表 + */ + public static List getElementOutgoingFlows(FlowElement source) { + List sequenceFlows = new ArrayList<>(); + if (source instanceof FlowNode) { + sequenceFlows = ((FlowNode) source).getOutgoingFlows(); + } + return sequenceFlows; + } + + + /** + * 获取流程元素信息 + * + * @param model bpmnModel对象 + * @param flowElementId 元素ID + * @return 元素信息 + */ + public static FlowElement getFlowElementById(BpmnModel model, String flowElementId) { + Process process = model.getMainProcess(); + return process.getFlowElement(flowElementId); + } + + + /** + * 找到 source 节点之前的所有用户任务节点 + * + * @param source 起始节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 已找到的用户任务节点 + * @return + */ + public static List getPreUserTaskList(FlowElement source, Set hasSequenceFlow, List userTaskList) { + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + userTaskList = getPreUserTaskList(source.getSubProcess(), hasSequenceFlow, userTaskList); + } + + // 根据类型,获取入口连线 + List sequenceFlows = getElementIncomingFlows(source); + + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 类型为用户节点,则新增父级节点 + if (sequenceFlow.getSourceFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getSourceFlowElement()); + } + // 类型为子流程,则添加子流程开始节点出口处相连的节点 + if (sequenceFlow.getSourceFlowElement() instanceof SubProcess) { + // 获取子流程用户任务节点 + List childUserTaskList = findChildProcessUserTasks((StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (childUserTaskList != null && childUserTaskList.size() > 0) { + userTaskList.addAll(childUserTaskList); + } + } + // 继续迭代 + userTaskList = getPreUserTaskList(sequenceFlow.getSourceFlowElement(), hasSequenceFlow, userTaskList); + } + } + return userTaskList; + } + + /** + * 迭代获取子流程用户任务节点 + * + * @param source 起始节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 需要撤回的用户任务列表 + * @return + */ + public static List findChildProcessUserTasks(FlowElement source, Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 + if (sequenceFlow.getTargetFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = findChildProcessUserTasks((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (childUserTaskList != null && childUserTaskList.size() > 0) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + userTaskList = findChildProcessUserTasks(sequenceFlow.getTargetFlowElement(), hasSequenceFlow, userTaskList); + } + } + return userTaskList; + } + + + /** + * 迭代从后向前扫描,判断目标节点相对于当前节点是否是串行 + * 不存在直接回退到子流程中的情况,但存在从子流程出去到父流程情况 + * + * @param source 起始节点 + * @param target 目标节点 + * @param visitedElements 已经经过的连线的 ID,用于判断线路是否重复 + * @return 结果 + */ + public static boolean isSequentialReachable(FlowElement source, FlowElement target, Set visitedElements) { + visitedElements = visitedElements == null ? new HashSet<>() : visitedElements; + //不能是开始事件和子流程 + if (source instanceof StartEvent && isInEventSubprocess(source)) { + return false; + } + + // 根据类型,获取入口连线 + List sequenceFlows = getElementIncomingFlows(source); + if (sequenceFlows != null && sequenceFlows.size() > 0) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (visitedElements.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + visitedElements.add(sequenceFlow.getId()); + FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement(); + // 这条线路存在目标节点,这条线路完成,进入下个线路 + if (target.getId().equals(sourceFlowElement.getId())) { + continue; + } + // 如果目标节点为并行网关,则不继续 + if (sourceFlowElement instanceof ParallelGateway) { + return false; + } + // 否则就继续迭代 + boolean isSequential = isSequentialReachable(sourceFlowElement, target, visitedElements); + if (!isSequential) { + return false; + } + } + } + return true; + } + + /** + * 判断当前节点是否属于不同的子流程 + * + * @param flowElement 被判断的节点 + * @return true表示属于子流程 + */ + protected static boolean isInEventSubprocess(FlowElement flowElement) { + FlowElementsContainer flowElementsContainer = flowElement.getParentContainer(); + while (flowElementsContainer != null) { + if (flowElementsContainer instanceof EventSubProcess) { + return true; + } + + if (flowElementsContainer instanceof FlowElement) { + flowElementsContainer = ((FlowElement) flowElementsContainer).getParentContainer(); + } else { + flowElementsContainer = null; + } + } + return false; + } + + + /** + * 根据正在运行的任务节点,迭代获取子级任务节点列表,向后找 + * + * @param source 起始节点 + * @param runTaskKeyList 正在运行的任务 Key,用于校验任务节点是否是正在运行的节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 需要撤回的用户任务列表 + * @return + */ + public static List iteratorFindChildUserTasks(FlowElement source, List runTaskKeyList, Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + userTaskList = iteratorFindChildUserTasks(source.getSubProcess(), runTaskKeyList, hasSequenceFlow, userTaskList); + } + + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 + if (sequenceFlow.getTargetFlowElement() instanceof UserTask && runTaskKeyList.contains((sequenceFlow.getTargetFlowElement()).getId())) { + userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = iteratorFindChildUserTasks((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), runTaskKeyList, hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (childUserTaskList != null && childUserTaskList.size() > 0) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + userTaskList = iteratorFindChildUserTasks(sequenceFlow.getTargetFlowElement(), runTaskKeyList, hasSequenceFlow, userTaskList); + } + } + return userTaskList; + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index 555d3f548..6db214c23 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode; /** * Bpm 错误码枚举类 - * + *

* bpm 系统,使用 1-009-000-000 段 */ public interface ErrorCodeConstants { @@ -45,7 +45,11 @@ public interface ErrorCodeConstants { // ========== 流程任务 1-009-005-000 ========== ErrorCode TASK_COMPLETE_FAIL_NOT_EXISTS = new ErrorCode(1_009_005_000, "审批任务失败,原因:该任务不处于未审批"); ErrorCode TASK_COMPLETE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "审批任务失败,原因:该任务的审批人不是你"); - + ErrorCode TASK_NOT_EXISTS = new ErrorCode(1_009_005_002, "流程任务不存在"); + ErrorCode TASK_IS_NOT_ACTIVITY = new ErrorCode(1_009_005_003, "当前任务不属于活动状态,不能操作"); + ErrorCode TASK_SOURCE_TARGET_ERROR = new ErrorCode(1_009_005_004, " 当前节点相对于目标节点,不属于串行关系,无法回退"); + ErrorCode TASK_TARGET_NODE_NOT_EXISTS = new ErrorCode(1_009_005_005, " 目标节点不存在"); + ErrorCode TASK_ROLLBACK_FAIL = new ErrorCode(1_009_005_006, "回退任务失败,选择回退的节点没有需要回滚的任务!请重新选择其他任务节点"); // ========== 流程任务分配规则 1-009-006-000 ========== ErrorCode TASK_ASSIGN_RULE_EXISTS = new ErrorCode(1_009_006_000, "流程({}) 的任务({}) 已经存在分配规则"); ErrorCode TASK_ASSIGN_RULE_NOT_EXISTS = new ErrorCode(1_009_006_001, "流程任务分配规则不存在"); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java index 74b2ceb34..39ca76d98 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java @@ -75,4 +75,19 @@ public class BpmTaskController { return success(true); } + @GetMapping("/get-return-list") + @Operation(summary = "获取所有可回退的节点", description = "用于【流程详情】的【回退】按钮") + @PreAuthorize("@ss.hasPermission('bpm:task:rollback')") + public CommonResult> getReturnList(String taskId) { + return success(taskService.findReturnTaskList(taskId)); + } + + @PutMapping("/rollback") + @Operation(summary = "回退任务", description = "用于【流程详情】的【回退】按钮") + @PreAuthorize("@ss.hasPermission('bpm:task:rollback')") + public CommonResult getReturnList(@Valid @RequestBody BpmTaskRollbackReqVO reqVO) { + taskService.taskReturn(reqVO); + return success(true); + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRollbackReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRollbackReqVO.java new file mode 100644 index 000000000..546004969 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRollbackReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 回退流程任务的 Request VO") +@Data +public class BpmTaskRollbackReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "回退到的任务Key", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotEmpty(message = "回退到的任务Key不能为空") + private String targetDefinitionKey; + + @Schema(description = "回退意见") + private String reason; + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRollbackRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRollbackRespVO.java new file mode 100644 index 000000000..8ce9e7f3e --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRollbackRespVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * + */ +@Schema(description = "管理后台 - 流程任务的 可回退的节点 Response VO") +@Data +public class BpmTaskRollbackRespVO { + + @Schema(description = "任务定义的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "Activity_one") + private String taskDefinitionKey; + + @Schema(description = "任务名词", requiredMode = Schema.RequiredMode.REQUIRED, example = "经理审批") + private String name; +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java index 82ad35d6a..ebefd804f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java @@ -6,11 +6,13 @@ import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskDonePageItemRespVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRollbackRespVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskTodoPageItemRespVO; import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmTaskExtDO; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.bpmn.model.FlowElement; import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.runtime.ProcessInstance; @@ -55,8 +57,8 @@ public interface BpmTaskConvert { } default List convertList2(List tasks, - Map bpmTaskExtDOMap, Map historicProcessInstanceMap, - Map userMap) { + Map bpmTaskExtDOMap, Map historicProcessInstanceMap, + Map userMap) { return CollectionUtils.convertList(tasks, task -> { BpmTaskDonePageItemRespVO respVO = convert2(task); BpmTaskExtDO taskExtDO = bpmTaskExtDOMap.get(task.getId()); @@ -82,8 +84,8 @@ public interface BpmTaskConvert { BpmTaskTodoPageItemRespVO.ProcessInstance convert(ProcessInstance processInstance, AdminUserRespDTO startUser); default List convertList3(List tasks, - Map bpmTaskExtDOMap, HistoricProcessInstance processInstance, - Map userMap, Map deptMap) { + Map bpmTaskExtDOMap, HistoricProcessInstance processInstance, + Map userMap, Map deptMap) { return CollectionUtils.convertList(tasks, task -> { BpmTaskRespVO respVO = convert3(task); BpmTaskExtDO taskExtDO = bpmTaskExtDOMap.get(task.getId()); @@ -113,29 +115,35 @@ public interface BpmTaskConvert { void copyTo(BpmTaskExtDO from, @MappingTarget BpmTaskDonePageItemRespVO to); @Mappings({@Mapping(source = "processInstance.id", target = "id"), - @Mapping(source = "processInstance.name", target = "name"), - @Mapping(source = "processInstance.startUserId", target = "startUserId"), - @Mapping(source = "processInstance.processDefinitionId", target = "processDefinitionId"), - @Mapping(source = "startUser.nickname", target = "startUserNickname")}) + @Mapping(source = "processInstance.name", target = "name"), + @Mapping(source = "processInstance.startUserId", target = "startUserId"), + @Mapping(source = "processInstance.processDefinitionId", target = "processDefinitionId"), + @Mapping(source = "startUser.nickname", target = "startUserNickname")}) BpmTaskTodoPageItemRespVO.ProcessInstance convert(HistoricProcessInstance processInstance, - AdminUserRespDTO startUser); + AdminUserRespDTO startUser); default BpmTaskExtDO convert2TaskExt(Task task) { BpmTaskExtDO taskExtDO = new BpmTaskExtDO().setTaskId(task.getId()) - .setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())).setName(task.getName()) - .setProcessDefinitionId(task.getProcessDefinitionId()).setProcessInstanceId(task.getProcessInstanceId()); + .setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())).setName(task.getName()) + .setProcessDefinitionId(task.getProcessDefinitionId()).setProcessInstanceId(task.getProcessInstanceId()); taskExtDO.setCreateTime(LocalDateTimeUtil.of(task.getCreateTime())); return taskExtDO; } default BpmMessageSendWhenTaskCreatedReqDTO convert(ProcessInstance processInstance, AdminUserRespDTO startUser, - Task task) { + Task task) { BpmMessageSendWhenTaskCreatedReqDTO reqDTO = new BpmMessageSendWhenTaskCreatedReqDTO(); reqDTO.setProcessInstanceId(processInstance.getProcessInstanceId()) - .setProcessInstanceName(processInstance.getName()).setStartUserId(startUser.getId()) - .setStartUserNickname(startUser.getNickname()).setTaskId(task.getId()).setTaskName(task.getName()) - .setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())); + .setProcessInstanceName(processInstance.getName()).setStartUserId(startUser.getId()) + .setStartUserNickname(startUser.getNickname()).setTaskId(task.getId()).setTaskName(task.getName()) + .setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())); return reqDTO; } + @Mapping(source = "taskDefinitionKey", target = "id") + default List convertList(List elementList) { + return CollectionUtils.convertList(elementList, element -> new BpmTaskRollbackRespVO() + .setName(element.getName()) + .setTaskDefinitionKey(element.getId())); + } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java index a4e4f83d1..0addde074 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java @@ -74,4 +74,12 @@ public interface BpmModelService { */ BpmnModel getBpmnModel(String id); + /** + * 获得流程定义编号对应的 BPMN Model + * + * @param processDefinitionId 流程定义编号 + * @return BPMN Model + */ + BpmnModel getBpmnModelByDefinitionId(String processDefinitionId); + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java index 2e7a07e1e..81c437710 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -233,6 +233,11 @@ public class BpmModelServiceImpl implements BpmModelService { return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true); } + @Override + public BpmnModel getBpmnModelByDefinitionId(String processDefinitionId) { + return repositoryService.getBpmnModel(processDefinitionId); + } + private void checkKeyNCName(String key) { if (!ValidationUtils.isXmlNCName(key)) { throw exception(MODEL_KEY_VALID); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java index fc45887b7..fb3c3bfec 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.bpm.service.task; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; @@ -128,4 +127,17 @@ public interface BpmTaskService { */ void updateTaskExtAssign(Task task); + /** + * 获取当前人物的可回退的流程集合 + * @param taskId 当前的任务ID + * @return 可以回退的节点列表 + */ + List findReturnTaskList(String taskId); + + /** + * 将任务回退到指定的 targetDefinitionKey 位置 + * @param reqVO 回退的任务key和当前所在的任务ID + */ + void taskReturn(BpmTaskRollbackReqVO reqVO); + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 62b638f2e..3ab1ce375 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -8,21 +8,33 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.PageUtils; +import cn.iocoder.yudao.framework.flowable.core.util.ModelUtils; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmTaskExtDO; import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmTaskExtMapper; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum; +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.constants.BpmnXMLConstants; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; +import org.flowable.common.engine.api.FlowableException; +import org.flowable.common.engine.api.FlowableObjectNotFoundException; import org.flowable.engine.HistoryService; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.RuntimeService; import org.flowable.engine.TaskService; +import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.Task; import org.flowable.task.api.TaskQuery; @@ -39,8 +51,7 @@ import java.time.LocalDateTime; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; /** @@ -68,12 +79,17 @@ public class BpmTaskServiceImpl implements BpmTaskService { private BpmTaskExtMapper taskExtMapper; @Resource private BpmMessageService messageService; + @Resource + private BpmModelService bpmModelService; + @Resource + private RuntimeService runtimeService; + @Override public PageResult getTodoTaskPage(Long userId, BpmTaskTodoPageReqVO pageVO) { // 查询待办任务 TaskQuery taskQuery = taskService.createTaskQuery().taskAssignee(String.valueOf(userId)) // 分配给自己 - .orderByTaskCreateTime().desc(); // 创建时间倒序 + .orderByTaskCreateTime().desc(); // 创建时间倒序 if (StrUtil.isNotBlank(pageVO.getName())) { taskQuery.taskNameLike("%" + pageVO.getName() + "%"); } @@ -91,21 +107,21 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 获得 ProcessInstance Map Map processInstanceMap = - processInstanceService.getProcessInstanceMap(convertSet(tasks, Task::getProcessInstanceId)); + processInstanceService.getProcessInstanceMap(convertSet(tasks, Task::getProcessInstanceId)); // 获得 User Map Map userMap = adminUserApi.getUserMap( - convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); + convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); // 拼接结果 return new PageResult<>(BpmTaskConvert.INSTANCE.convertList1(tasks, processInstanceMap, userMap), - taskQuery.count()); + taskQuery.count()); } @Override public PageResult getDoneTaskPage(Long userId, BpmTaskDonePageReqVO pageVO) { // 查询已办任务 HistoricTaskInstanceQuery taskQuery = historyService.createHistoricTaskInstanceQuery().finished() // 已完成 - .taskAssignee(String.valueOf(userId)) // 分配给自己 - .orderByHistoricTaskInstanceEndTime().desc(); // 审批时间倒序 + .taskAssignee(String.valueOf(userId)) // 分配给自己 + .orderByHistoricTaskInstanceEndTime().desc(); // 审批时间倒序 if (StrUtil.isNotBlank(pageVO.getName())) { taskQuery.taskNameLike("%" + pageVO.getName() + "%"); } @@ -123,19 +139,19 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 获得 TaskExtDO Map List bpmTaskExtDOs = - taskExtMapper.selectListByTaskIds(convertSet(tasks, HistoricTaskInstance::getId)); + taskExtMapper.selectListByTaskIds(convertSet(tasks, HistoricTaskInstance::getId)); Map bpmTaskExtDOMap = convertMap(bpmTaskExtDOs, BpmTaskExtDO::getTaskId); // 获得 ProcessInstance Map Map historicProcessInstanceMap = - processInstanceService.getHistoricProcessInstanceMap( - convertSet(tasks, HistoricTaskInstance::getProcessInstanceId)); + processInstanceService.getHistoricProcessInstanceMap( + convertSet(tasks, HistoricTaskInstance::getProcessInstanceId)); // 获得 User Map Map userMap = adminUserApi.getUserMap( - convertSet(historicProcessInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); + convertSet(historicProcessInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); // 拼接结果 return new PageResult<>( - BpmTaskConvert.INSTANCE.convertList2(tasks, bpmTaskExtDOMap, historicProcessInstanceMap, userMap), - taskQuery.count()); + BpmTaskConvert.INSTANCE.convertList2(tasks, bpmTaskExtDOMap, historicProcessInstanceMap, userMap), + taskQuery.count()); } @Override @@ -189,8 +205,8 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 更新任务拓展表为通过 taskExtMapper.updateByTaskId( - new BpmTaskExtDO().setTaskId(task.getId()).setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()) - .setReason(reqVO.getReason())); + new BpmTaskExtDO().setTaskId(task.getId()).setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()) + .setReason(reqVO.getReason())); } @Override @@ -208,8 +224,8 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 更新任务拓展表为不通过 taskExtMapper.updateByTaskId( - new BpmTaskExtDO().setTaskId(task.getId()).setResult(BpmProcessInstanceResultEnum.REJECT.getResult()) - .setEndTime(LocalDateTime.now()).setReason(reqVO.getReason())); + new BpmTaskExtDO().setTaskId(task.getId()).setResult(BpmProcessInstanceResultEnum.REJECT.getResult()) + .setEndTime(LocalDateTime.now()).setReason(reqVO.getReason())); } @Override @@ -245,7 +261,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @Override public void createTaskExt(Task task) { BpmTaskExtDO taskExtDO = - BpmTaskConvert.INSTANCE.convert2TaskExt(task).setResult(BpmProcessInstanceResultEnum.PROCESS.getResult()); + BpmTaskConvert.INSTANCE.convert2TaskExt(task).setResult(BpmProcessInstanceResultEnum.PROCESS.getResult()); taskExtMapper.insert(taskExtDO); } @@ -293,17 +309,17 @@ public class BpmTaskServiceImpl implements BpmTaskService { @Override public void updateTaskExtAssign(Task task) { BpmTaskExtDO taskExtDO = - new BpmTaskExtDO().setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())).setTaskId(task.getId()); + new BpmTaskExtDO().setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())).setTaskId(task.getId()); taskExtMapper.updateByTaskId(taskExtDO); // 发送通知。在事务提交时,批量执行操作,所以直接查询会无法查询到 ProcessInstance,所以这里是通过监听事务的提交来实现。 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { ProcessInstance processInstance = - processInstanceService.getProcessInstance(task.getProcessInstanceId()); + processInstanceService.getProcessInstance(task.getProcessInstanceId()); AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())); messageService.sendMessageWhenTaskAssigned( - BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task)); + BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task)); } }); } @@ -316,4 +332,144 @@ public class BpmTaskServiceImpl implements BpmTaskService { return historyService.createHistoricTaskInstanceQuery().taskId(id).singleResult(); } + @Override + public List findReturnTaskList(String taskId) { + // 当前任务 task + Task task = taskService.createTaskQuery().taskId(taskId).singleResult(); + // 根据流程定义获取流程模型信息 + BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId()); + // 查询该任务的前置任务节点的key集合 + Set historyTaksDefinitionKeySet = getPreTaskByCurrentTask(task, bpmnModel); + if (CollUtil.isEmpty(historyTaksDefinitionKeySet)) { + return Collections.emptyList(); + } + // 获取当前任务节点元素 + FlowElement source = ModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); + List elementList = new ArrayList<>(); + for (String activityId : historyTaksDefinitionKeySet) { + FlowElement target = ModelUtils.getFlowElementById(bpmnModel, activityId); + //不支持串行和子流程 + boolean isSequential = ModelUtils.isSequentialReachable(source, target, new HashSet<>()); + if (isSequential) { + elementList.add(target); + } + } + return BpmTaskConvert.INSTANCE.convertList(elementList); + } + + /** + * 查询当前流程实例符合条件可回退的 taskDefinitionKey 集合 + * + * @param task 当前任务 + * @param bpmnModel 当前流程定义对应的流程模型 + * @return 符合条件的已去重的 taskDefinitionKey集合 + */ + private Set getPreTaskByCurrentTask(Task task, BpmnModel bpmnModel) { + // 获取当前任务节点元素 + FlowElement source = ModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); + //拿到当前任务节点的前置节点集合 + List preUserTaskList = ModelUtils.getPreUserTaskList(source, null, null); + //需要保证这些节点都是在该source节点之前的 + if (CollUtil.isNotEmpty(preUserTaskList)) { + return convertSet(preUserTaskList, UserTask::getId); + } + return Collections.emptySet(); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void taskReturn(BpmTaskRollbackReqVO reqVO) { + // 当前任务 task + Task task = validateRollback(reqVO.getId()); + // 校验源头和目标节点的关系,并返回目标元素 + FlowElement targetElement = validateRollbackProcessDefinition(task.getTaskDefinitionKey(), reqVO.getTargetDefinitionKey(), task.getProcessDefinitionId()); + //调用flowable框架的回退逻辑 + this.handlerRollback(task, targetElement, reqVO); + // 更新任务扩展表 + taskExtMapper.updateByTaskId( + new BpmTaskExtDO().setTaskId(task.getId()).setResult(BpmProcessInstanceResultEnum.BACK.getResult()) + .setEndTime(LocalDateTime.now()).setReason(reqVO.getReason())); + } + + /** + * 校验当前流程是否可以操作回退 + * + * @param taskId 当前任务ID + * @return + */ + private Task validateRollback(String taskId) { + // 当前任务 task + Task task = taskService.createTaskQuery().taskId(taskId).singleResult(); + if (null == task) { + throw exception(TASK_NOT_EXISTS); + } + if (task.isSuspended()) { + throw exception(TASK_IS_NOT_ACTIVITY); + } + return task; + } + + /** + * 回退流程节点时,校验源头节点和目标节点的关系 + * + * @param sourceKey 当前任务节点Key + * @param targetKey 目标任务节点key + * @param processDefinitionId 当前流程定义ID + * @return 目标元素 + */ + private FlowElement validateRollbackProcessDefinition(String sourceKey, String targetKey, String processDefinitionId) { + // 获取流程模型信息 + BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(processDefinitionId); + // 获取当前任务节点元素 + FlowElement source = ModelUtils.getFlowElementById(bpmnModel, sourceKey); + // 获取跳转的节点元素 + FlowElement target = ModelUtils.getFlowElementById(bpmnModel, targetKey); + if (null == target) { + throw exception(TASK_TARGET_NODE_NOT_EXISTS); + } + // 从当前节点向前扫描,判断当前节点与目标节点是否属于串行,若目标节点是在并行网关上或非同一路线上,不可跳转 + boolean isSequential = ModelUtils.isSequentialReachable(source, target, new HashSet<>()); + if (!isSequential) { + throw exception(TASK_SOURCE_TARGET_ERROR); + } + return target; + } + + /** + * 处理回退逻辑 + * + * @param task 当前回退的任务 + * @param targetElement 需要回退到的目标任务 + * @param reqVO 前端参数封装 + */ + public void handlerRollback(Task task, FlowElement targetElement, BpmTaskRollbackReqVO reqVO) { + // 获取所有正常进行的任务节点 Key,这些任务不能直接使用,需要找出其中需要撤回的任务 + List runTaskList = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).list(); + List runTaskKeyList = convertList(runTaskList, Task::getTaskDefinitionKey); + // 通过 targetElement 的出口连线,结合 runTaskList 比对,获取需要撤回的任务 key + List rollbackUserTaskList = ModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null); + // 需退回任务 key + List rollbackTaskDefinitionKeyList = convertList(rollbackUserTaskList, UserTask::getId); + + // 通过 key 和 runTaskList 中的key对比,拿到任务ID,设置设置回退意见 + List currentTaskIds = new ArrayList<>(); + rollbackTaskDefinitionKeyList.forEach(currentId -> runTaskList.forEach(runTask -> { + if (currentId.equals(runTask.getTaskDefinitionKey())) { + currentTaskIds.add(runTask.getId()); + } + })); + if (CollUtil.isEmpty(currentTaskIds)) { + throw exception(TASK_ROLLBACK_FAIL); + } + // 设置回退意见 + for (String currentTaskId : currentTaskIds) { + taskService.addComment(currentTaskId, task.getProcessInstanceId(), BpmProcessInstanceResultEnum.BACK.getResult().toString(), reqVO.getReason()); + } + // 1 对 1 或 多 对 1 情况,currentIds 当前要跳转的节点列表(1或多),targetKey 跳转到的节点(1) + runtimeService.createChangeActivityStateBuilder() + .processInstanceId(task.getProcessInstanceId()) + .moveActivityIdsToSingleActivityId(rollbackTaskDefinitionKeyList, reqVO.getTargetDefinitionKey()) + .changeState(); + } + }