Browse Source

!629 工作流-》流程实例的任务节点回退
Merge pull request !629 from Youkehai/master

芋道源码 1 year ago
parent
commit
3826846368

+ 274 - 0
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<SequenceFlow> getElementIncomingFlows(FlowElement source) {
+        List<SequenceFlow> sequenceFlows = new ArrayList<>();
+        if (source instanceof FlowNode) {
+            sequenceFlows = ((FlowNode) source).getIncomingFlows();
+        }
+        return sequenceFlows;
+    }
+
+
+    /**
+     * 根据节点,获取出口连线
+     *
+     * @param source 起始节点
+     * @return 出口连线列表
+     */
+    public static List<SequenceFlow> getElementOutgoingFlows(FlowElement source) {
+        List<SequenceFlow> 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<UserTask> getPreUserTaskList(FlowElement source, Set<String> hasSequenceFlow, List<UserTask> 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<SequenceFlow> 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<UserTask> 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<UserTask> findChildProcessUserTasks(FlowElement source, Set<String> hasSequenceFlow, List<UserTask> userTaskList) {
+        hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow;
+        userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList;
+
+        // 根据类型,获取出口连线
+        List<SequenceFlow> 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<UserTask> 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<String> visitedElements) {
+        visitedElements = visitedElements == null ? new HashSet<>() : visitedElements;
+        //不能是开始事件和子流程
+        if (source instanceof StartEvent && isInEventSubprocess(source)) {
+            return false;
+        }
+
+        // 根据类型,获取入口连线
+        List<SequenceFlow> 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<UserTask> iteratorFindChildUserTasks(FlowElement source, List<String> runTaskKeyList, Set<String> hasSequenceFlow, List<UserTask> 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<SequenceFlow> 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<UserTask> 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;
+    }
+}

+ 6 - 2
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 错误码枚举类
- *
+ * <p>
  * 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_PENDING = 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_RETURN_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, "流程任务分配规则不存在");

+ 16 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java

@@ -75,4 +75,20 @@ public class BpmTaskController {
         return success(true);
     }
 
+    @GetMapping("/get-return-list")
+    @Operation(summary = "获取所有可回退的节点", description = "用于【流程详情】的【回退】按钮")
+    @Parameter(name = "taskId", description = "当前任务ID", required = true)
+    @PreAuthorize("@ss.hasPermission('bpm:task:return')")
+    public CommonResult<List<BpmTaskSimpleRespVO>> getReturnList(@RequestParam("taskId") String taskId) {
+        return success(taskService.getReturnTaskList(taskId));
+    }
+
+    @PutMapping("/return")
+    @Operation(summary = "回退任务", description = "用于【流程详情】的【回退】按钮")
+    @PreAuthorize("@ss.hasPermission('bpm:task:return')")
+    public CommonResult<Boolean> returnTask(@Valid @RequestBody BpmTaskReturnReqVO reqVO) {
+        taskService.returnTask(reqVO);
+        return success(true);
+    }
+
 }

+ 23 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.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 BpmTaskReturnReqVO {
+
+    @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 = "回退意见", example = "")
+    private String reason;
+
+}

+ 18 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskSimpleRespVO.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 BpmTaskSimpleRespVO {
+
+    @Schema(description = "任务定义的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "Activity_one")
+    private String definitionKey;
+
+    @Schema(description = "任务名词", requiredMode = Schema.RequiredMode.REQUIRED, example = "经理审批")
+    private String name;
+}

+ 23 - 15
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.BpmTaskSimpleRespVO;
 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<BpmTaskDonePageItemRespVO> convertList2(List<HistoricTaskInstance> tasks,
-        Map<String, BpmTaskExtDO> bpmTaskExtDOMap, Map<String, HistoricProcessInstance> historicProcessInstanceMap,
-        Map<Long, AdminUserRespDTO> userMap) {
+                                                         Map<String, BpmTaskExtDO> bpmTaskExtDOMap, Map<String, HistoricProcessInstance> historicProcessInstanceMap,
+                                                         Map<Long, AdminUserRespDTO> 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<BpmTaskRespVO> convertList3(List<HistoricTaskInstance> tasks,
-        Map<String, BpmTaskExtDO> bpmTaskExtDOMap, HistoricProcessInstance processInstance,
-        Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap) {
+                                             Map<String, BpmTaskExtDO> bpmTaskExtDOMap, HistoricProcessInstance processInstance,
+                                             Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> 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<BpmTaskSimpleRespVO> convertList(List<FlowElement> elementList) {
+        return CollectionUtils.convertList(elementList, element -> new BpmTaskSimpleRespVO()
+                .setName(element.getName())
+                .setDefinitionKey(element.getId()));
+    }
 }

+ 8 - 0
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);
+
 }

+ 5 - 0
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);

+ 16 - 7
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.*;
@@ -23,7 +22,6 @@ public interface BpmTaskService {
      *
      * @param userId    用户编号
      * @param pageReqVO 分页请求
-     *
      * @return 流程任务分页
      */
     PageResult<BpmTaskTodoPageItemRespVO> getTodoTaskPage(Long userId, BpmTaskTodoPageReqVO pageReqVO);
@@ -33,7 +31,6 @@ public interface BpmTaskService {
      *
      * @param userId    用户编号
      * @param pageReqVO 分页请求
-     *
      * @return 流程任务分页
      */
     PageResult<BpmTaskDonePageItemRespVO> getDoneTaskPage(Long userId, BpmTaskDonePageReqVO pageReqVO);
@@ -42,19 +39,17 @@ public interface BpmTaskService {
      * 获得流程任务 Map
      *
      * @param processInstanceIds 流程实例的编号数组
-     *
      * @return 流程任务 Map
      */
     default Map<String, List<Task>> getTaskMapByProcessInstanceIds(List<String> processInstanceIds) {
         return CollectionUtils.convertMultiMap(getTasksByProcessInstanceIds(processInstanceIds),
-            Task::getProcessInstanceId);
+                Task::getProcessInstanceId);
     }
 
     /**
      * 获得流程任务列表
      *
      * @param processInstanceIds 流程实例的编号数组
-     *
      * @return 流程任务列表
      */
     List<Task> getTasksByProcessInstanceIds(List<String> processInstanceIds);
@@ -63,7 +58,6 @@ public interface BpmTaskService {
      * 获得指令流程实例的流程任务列表,包括所有状态的
      *
      * @param processInstanceId 流程实例的编号
-     *
      * @return 流程任务列表
      */
     List<BpmTaskRespVO> getTaskListByProcessInstanceId(String processInstanceId);
@@ -128,4 +122,19 @@ public interface BpmTaskService {
      */
     void updateTaskExtAssign(Task task);
 
+    /**
+     * 获取当前任务的可回退的流程集合
+     *
+     * @param taskId 当前的任务ID
+     * @return 可以回退的节点列表
+     */
+    List<BpmTaskSimpleRespVO> getReturnTaskList(String taskId);
+
+    /**
+     * 将任务回退到指定的 targetDefinitionKey 位置
+     *
+     * @param reqVO 回退的任务key和当前所在的任务ID
+     */
+    void returnTask(BpmTaskReturnReqVO reqVO);
+
 }

+ 175 - 23
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java

@@ -1,26 +1,31 @@
 package cn.iocoder.yudao.module.bpm.service.task;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.StrUtil;
 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.model.BpmnModel;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.HistoryService;
+import org.flowable.engine.RuntimeService;
 import org.flowable.engine.TaskService;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
@@ -39,8 +44,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 +72,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     private BpmTaskExtMapper taskExtMapper;
     @Resource
     private BpmMessageService messageService;
+    @Resource
+    private BpmModelService bpmModelService;
+    @Resource
+    private RuntimeService runtimeService;
+
 
     @Override
     public PageResult<BpmTaskTodoPageItemRespVO> 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 +100,21 @@ public class BpmTaskServiceImpl implements BpmTaskService {
 
         // 获得 ProcessInstance Map
         Map<String, ProcessInstance> processInstanceMap =
-            processInstanceService.getProcessInstanceMap(convertSet(tasks, Task::getProcessInstanceId));
+                processInstanceService.getProcessInstanceMap(convertSet(tasks, Task::getProcessInstanceId));
         // 获得 User Map
         Map<Long, AdminUserRespDTO> 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<BpmTaskDonePageItemRespVO> 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 +132,19 @@ public class BpmTaskServiceImpl implements BpmTaskService {
 
         // 获得 TaskExtDO Map
         List<BpmTaskExtDO> bpmTaskExtDOs =
-            taskExtMapper.selectListByTaskIds(convertSet(tasks, HistoricTaskInstance::getId));
+                taskExtMapper.selectListByTaskIds(convertSet(tasks, HistoricTaskInstance::getId));
         Map<String, BpmTaskExtDO> bpmTaskExtDOMap = convertMap(bpmTaskExtDOs, BpmTaskExtDO::getTaskId);
         // 获得 ProcessInstance Map
         Map<String, HistoricProcessInstance> historicProcessInstanceMap =
-            processInstanceService.getHistoricProcessInstanceMap(
-                convertSet(tasks, HistoricTaskInstance::getProcessInstanceId));
+                processInstanceService.getHistoricProcessInstanceMap(
+                        convertSet(tasks, HistoricTaskInstance::getProcessInstanceId));
         // 获得 User Map
         Map<Long, AdminUserRespDTO> 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 +198,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 +217,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 +254,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 +302,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 +325,147 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         return historyService.createHistoricTaskInstanceQuery().taskId(id).singleResult();
     }
 
+    @Override
+    public List<BpmTaskSimpleRespVO> getReturnTaskList(String taskId) {
+        // 当前任务 task
+        Task task = getTask(taskId);
+        if (null == task) {
+            throw exception(TASK_NOT_EXISTS);
+        }
+        // 根据流程定义获取流程模型信息
+        BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
+        // 查询该任务的前置任务节点的key集合
+        Set<String> historyTaksDefinitionKeySet = getPreTaskByCurrentTask(task, bpmnModel);
+        if (CollUtil.isEmpty(historyTaksDefinitionKeySet)) {
+            return Collections.emptyList();
+        }
+        // 获取当前任务节点元素
+        FlowElement source = ModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
+        List<FlowElement> elementList = new ArrayList<>();
+        for (String activityId : historyTaksDefinitionKeySet) {
+            FlowElement target = ModelUtils.getFlowElementById(bpmnModel, activityId);
+            //非 串行和子流程则加入返回节点 elementList
+            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<String> getPreTaskByCurrentTask(Task task, BpmnModel bpmnModel) {
+        // 获取当前任务节点元素
+        FlowElement source = ModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
+        //拿到当前任务节点的前置节点集合
+        List<UserTask> 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 returnTask(BpmTaskReturnReqVO reqVO) {
+        // 当前任务 task
+        Task task = validateReturnTask(reqVO.getId());
+        // 校验源头和目标节点的关系,并返回目标元素
+        FlowElement targetElement = validateReturnProcessDefinition(task.getTaskDefinitionKey(), reqVO.getTargetDefinitionKey(), task.getProcessDefinitionId());
+        //调用flowable框架的回退逻辑
+        this.handlerReturn(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 validateReturnTask(String taskId) {
+        // 当前任务 task
+        Task task = getTask(taskId);
+        if (null == task) {
+            throw exception(TASK_NOT_EXISTS);
+        }
+        if (task.isSuspended()) {
+            throw exception(TASK_IS_PENDING);
+        }
+        return task;
+    }
+
+    /**
+     * 回退流程节点时,校验源头节点和目标节点的关系
+     *
+     * @param sourceKey           当前任务节点Key
+     * @param targetKey           目标任务节点key
+     * @param processDefinitionId 当前流程定义ID
+     * @return 目标元素
+     */
+    private FlowElement validateReturnProcessDefinition(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 handlerReturn(Task task, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
+        // 获取所有正常进行的任务节点 Key,这些任务不能直接使用,需要找出其中需要撤回的任务
+        List<Task> runTaskList = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).list();
+        List<String> runTaskKeyList = convertList(runTaskList, Task::getTaskDefinitionKey);
+        // 通过 targetElement 的出口连线,结合 runTaskList 比对,获取需要撤回的任务 key
+        List<UserTask> returnUserTaskList = ModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
+        // 需退回任务 key
+        List<String> returnTaskDefinitionKeyList = convertList(returnUserTaskList, UserTask::getId);
+
+        // 通过 key 和 runTaskList 中的key对比,拿到任务ID,设置设置回退意见
+        List<String> currentTaskIds = new ArrayList<>();
+        returnTaskDefinitionKeyList.forEach(currentId -> runTaskList.forEach(runTask -> {
+            if (currentId.equals(runTask.getTaskDefinitionKey())) {
+                currentTaskIds.add(runTask.getId());
+            }
+        }));
+        if (CollUtil.isEmpty(currentTaskIds)) {
+            throw exception(TASK_RETURN_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(returnTaskDefinitionKeyList, reqVO.getTargetDefinitionKey())
+                .changeState();
+    }
+
 }