소스 검색

!1271 feat:审批通过时,查询下一个执行节点,校验流程执行正确与否
Merge pull request !1271 from SamllNorth_Lee/feature/bpm

芋道源码 5 달 전
부모
커밋
8a5638bdea
11개의 변경된 파일334개의 추가작업 그리고 107개의 파일을 삭제
  1. 2 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
  2. 12 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
  3. 78 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java
  4. 1 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java
  5. 6 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java
  6. 50 47
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
  7. 25 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
  8. 15 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
  9. 10 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
  10. 93 18
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
  11. 42 38
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java

+ 2 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java

@@ -24,6 +24,7 @@ public interface ErrorCodeConstants {
     ErrorCode MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS = new ErrorCode(1_009_002_005, "部署流程失败,原因:BPMN 流程图中,没有开始事件");
     ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在");
     ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员");
+    ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_RULE_TYPE_NOT_APPROVE_USER_SELECT = new ErrorCode(1_009_002_008, "部署流程失败,原因:BPMN 流程图中,用户任务({})的规则类型不能是【审批人自选】");
 
     // ========== 流程定义 1-009-003-000 ==========
     ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图");
@@ -40,6 +41,7 @@ public interface ErrorCodeConstants {
     ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程");
     ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消");
     ErrorCode PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 触发器请求调用失败");
+    ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "任务({})的下一个执行节点审批人未配置");
 
     // ========== 流程任务 1-009-005-000 ==========
     ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");

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

@@ -178,10 +178,22 @@ public class BpmProcessInstanceController {
         return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO));
     }
 
+    @GetMapping("/get-next-approval-nodes")
+    @Operation(summary = "获取下一个执行的流程节点")
+    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
+    @SuppressWarnings("unchecked")
+    public CommonResult<List<BpmApprovalDetailRespVO.ActivityNode>> getNextApprovalNodes(@Valid BpmApprovalDetailReqVO reqVO) {
+        if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) {
+            reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class));
+        }
+        return success(processInstanceService.getNextApprovalNodes(getLoginUserId(), reqVO));
+    }
+
     @GetMapping("/get-bpmn-model-view")
     @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用")
     @Parameter(name = "id", description = "流程实例的编号", required = true)
     public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView(@RequestParam(value = "id") String id) {
         return success(processInstanceService.getProcessInstanceBpmnModelView(id));
     }
+
 }

+ 78 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java

@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import com.google.common.collect.Sets;
+import jakarta.annotation.Resource;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 审批人自选 {@link BpmTaskCandidateUserStrategy} 实现类
+ * 审批人在审批时选择下一个节点的审批人
+ *
+ * @author smallNorthLee
+ */
+@Component
+public class BpmTaskCandidateApproveUserSelectStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy {
+
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private BpmProcessInstanceService processInstanceService;
+
+    @Override
+    public BpmTaskCandidateStrategyEnum getStrategy() {
+        return BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT;
+    }
+
+    @Override
+    public void validateParam(String param) {}
+
+    @Override
+    public boolean isParamRequired() {
+        return false;
+    }
+
+    @Override
+    public LinkedHashSet<Long> calculateUsersByTask(DelegateExecution execution, String param) {
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
+        Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId());
+        Map<String, List<Long>> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processInstance);
+        Assert.notNull(approveUserSelectAssignees, "流程实例({}) 的下一个执行节点审批人不能为空",
+                execution.getProcessInstanceId());
+        if (approveUserSelectAssignees == null) {
+            return Sets.newLinkedHashSet();
+        }
+        // 获得审批人
+        List<Long> assignees = approveUserSelectAssignees.get(execution.getCurrentActivityId());
+        return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
+    }
+
+    @Override
+    public LinkedHashSet<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
+                                                        Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+        if (processVariables == null) {
+            return Sets.newLinkedHashSet();
+        }
+        // 流程预测时会使用,允许审批人为空,如果为空前端会弹出提示选择下一个节点审批人,避免流程无法进行,审批时会真正校验节点是否配置审批人
+        Map<String, List<Long>> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processVariables);
+        if (approveUserSelectAssignees == null) {
+            return Sets.newLinkedHashSet();
+        }
+        // 获得审批人
+        List<Long> assignees = approveUserSelectAssignees.get(activityId);
+        return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
+    }
+
+}

+ 1 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java

@@ -24,6 +24,7 @@ public enum BpmTaskCandidateStrategyEnum implements ArrayValuable<Integer> {
     MULTI_DEPT_LEADER_MULTI(23, "连续多级部门的负责人"),
     POST(22, "岗位"),
     USER(30, "用户"),
+    APPROVE_USER_SELECT(34, "审批人,在审批时选择下一个节点的审批人"), // 审批人,在审批时选择下一个节点的审批人
     START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时选择此节点的审批人
     START_USER(36, "发起人自己"), // 申请人自己, 一般紧挨开始节点,常用于发起人信息审核场景
     START_USER_DEPT_LEADER(37, "发起人部门负责人"),

+ 6 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java

@@ -29,6 +29,12 @@ public class BpmnVariableConstants {
      * @see ProcessInstance#getProcessVariables()
      */
     public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES";
+    /**
+     * 流程实例的变量 - 审批人选择的审批人 Map
+     *
+     * @see ProcessInstance#getProcessVariables()
+     */
+    public static final String PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES = "PROCESS_APPROVE_USER_SELECT_ASSIGNEES";
     /**
      * 流程实例的变量 - 发起用户 ID
      *

+ 50 - 47
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java

@@ -808,53 +808,26 @@ public class BpmnModelUtils {
         // 情况:ExclusiveGateway 排它,只有一个满足条件的。如果没有,就走默认的
         if (currentElement instanceof ExclusiveGateway) {
             // 查找满足条件的 SequenceFlow 路径
-            Gateway gateway = (Gateway) currentElement;
-            // TODO @小北:当一个网关节点下存在多个满足的并行节点时,只查询一个节点流程流转会存在问题。需要优化,具体见issue:https://github.com/YunaiV/ruoyi-vue-pro/issues/761
-            SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
-                    flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
-                            && (evalConditionExpress(variables, flow.getConditionExpression())));
-            if (matchSequenceFlow == null) {
-                matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
-                        flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
-                // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
-                if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) {
-                    matchSequenceFlow = gateway.getOutgoingFlows().get(0);
-                }
-            }
+            SequenceFlow matchSequenceFlow = findMatchSequenceFlow((Gateway) currentElement, variables);
             // 遍历满足条件的 SequenceFlow 路径
             if (matchSequenceFlow != null) {
                 simulateNextFlowElements(matchSequenceFlow.getTargetFlowElement(), variables, resultElements, visitElements);
             }
-            return;
         }
-
         // 情况:InclusiveGateway 包容,多个满足条件的。如果没有,就走默认的
-        if (currentElement instanceof InclusiveGateway) {
+       else if (currentElement instanceof InclusiveGateway) {
             // 查找满足条件的 SequenceFlow 路径
-            Gateway gateway = (Gateway) currentElement;
-            Collection<SequenceFlow> matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
-                        flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
-                                && evalConditionExpress(variables, flow.getConditionExpression()));
-            if (CollUtil.isEmpty(matchSequenceFlows)) {
-                matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
-                        flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
-                // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
-                if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) {
-                    matchSequenceFlows = gateway.getOutgoingFlows();
-                }
-            }
+            Collection<SequenceFlow> matchSequenceFlows = findMatchSequenceFlows((Gateway) currentElement, variables);
             // 遍历满足条件的 SequenceFlow 路径
             matchSequenceFlows.forEach(
                     flow -> simulateNextFlowElements(flow.getTargetFlowElement(), variables, resultElements, visitElements));
         }
-
         // 情况:ParallelGateway 并行,都满足,都走
-        if (currentElement instanceof ParallelGateway) {
+        else if (currentElement instanceof ParallelGateway) {
             Gateway gateway = (Gateway) currentElement;
             // 遍历子节点
             gateway.getOutgoingFlows().forEach(
                     nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements));
-            return;
         }
     }
 
@@ -883,6 +856,10 @@ public class BpmnModelUtils {
             if (targetElement == null) {
                 continue;
             }
+            // 如果是结束节点,直接返回
+            if (targetElement instanceof EndEvent) {
+                break;
+            }
             // 情况一:处理不同类型的网关
             if (targetElement instanceof Gateway) {
                 Gateway gateway = (Gateway) targetElement;
@@ -911,8 +888,25 @@ public class BpmnModelUtils {
      */
     private static void handleExclusiveGateway(Gateway gateway, BpmnModel bpmnModel,
                                                Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
-       // TODO @小北: 这里和 simulateNextFlowElements 中有重复代码,是否重构??每个网关节点拆分出方法应该比较合理化,@芋艿
-        // TODO @小北:ok,把 simulateNextFlowElements 里面处理网关的,复用这个方法,可以么?
+        // 查找满足条件的 SequenceFlow 路径
+        SequenceFlow matchSequenceFlow = findMatchSequenceFlow(gateway, variables);
+        // 遍历满足条件的 SequenceFlow 路径
+        if (matchSequenceFlow != null) {
+            FlowElement targetElement = bpmnModel.getFlowElement(matchSequenceFlow.getTargetRef());
+            if (targetElement instanceof FlowNode) {
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        }
+    }
+
+    /**
+     * 处理排它网关(Exclusive Gateway),选择符合条件的路径
+     *
+     * @param gateway 排他网关
+     * @param variables 流程变量
+     * @return 符合条件的路径
+     */
+    private static SequenceFlow findMatchSequenceFlow(Gateway gateway, Map<String, Object> variables){
         SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
                 flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
                         && (evalConditionExpress(variables, flow.getConditionExpression())));
@@ -924,13 +918,7 @@ public class BpmnModelUtils {
                 matchSequenceFlow = gateway.getOutgoingFlows().get(0);
             }
         }
-        // 遍历满足条件的 SequenceFlow 路径
-        if (matchSequenceFlow != null) {
-            FlowElement targetElement = bpmnModel.getFlowElement(matchSequenceFlow.getTargetRef());
-            if (targetElement instanceof FlowNode) {
-                nextFlowNodes.add((FlowNode) targetElement);
-            }
-        }
+        return matchSequenceFlow;
     }
 
     /**
@@ -943,6 +931,26 @@ public class BpmnModelUtils {
      */
     private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel,
                                                Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
+        // 查找满足条件的 SequenceFlow 路径集合
+        Collection<SequenceFlow> matchSequenceFlows = findMatchSequenceFlows(gateway, variables);
+        // 遍历满足条件的 SequenceFlow 路径,获取目标节点
+        matchSequenceFlows.forEach(flow -> {
+            FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
+            if (targetElement instanceof FlowNode) {
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        });
+    }
+
+    /**
+     * 处理排它网关(Inclusive Gateway),选择符合条件的路径
+     *
+     * @param gateway 排他网关
+     * @param variables 流程变量
+     * @return 符合条件的路径
+     */
+    private static Collection<SequenceFlow> findMatchSequenceFlows(Gateway gateway, Map<String, Object> variables) {
+        // 查找满足条件的 SequenceFlow 路径
         Collection<SequenceFlow> matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
                 flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
                         && evalConditionExpress(variables, flow.getConditionExpression()));
@@ -954,15 +962,10 @@ public class BpmnModelUtils {
                 matchSequenceFlows = gateway.getOutgoingFlows();
             }
         }
-        // 遍历满足条件的 SequenceFlow 路径,获取目标节点
-        matchSequenceFlows.forEach(flow -> {
-            FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
-            if (targetElement instanceof FlowNode) {
-                nextFlowNodes.add((FlowNode) targetElement);
-            }
-        });
+        return matchSequenceFlows;
     }
 
+
     /**
      * 处理并行网关
      *

+ 25 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java

@@ -200,6 +200,31 @@ public class FlowableUtils {
                 BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES);
     }
 
+    /**
+     * 获得流程实例的审批用户选择的下一个节点的审批人 Map
+     *
+     * @param processInstance 流程实例
+     * @return 审批用户选择的下一个节点的审批人Map
+     */
+    public static Map<String, List<Long>> getApproveUserSelectAssignees(ProcessInstance processInstance) {
+        return processInstance != null ? getApproveUserSelectAssignees(processInstance.getProcessVariables()) : null;
+    }
+
+    /**
+     * 获得流程实例的审批用户选择的下一个节点的审批人 Map
+     *
+     * @param processVariables 流程变量
+     * @return 审批用户选择的下一个节点的审批人Map Map
+     */
+    @SuppressWarnings("unchecked")
+    public static Map<String, List<Long>> getApproveUserSelectAssignees(Map<String, Object> processVariables) {
+        if (processVariables == null) {
+            return null;
+        }
+        return (Map<String, List<Long>>) processVariables.get(
+                BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES);
+    }
+
     /**
      * 获得流程实例的摘要
      *

+ 15 - 4
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java

@@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
@@ -23,9 +24,7 @@ import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
 import jakarta.annotation.Resource;
 import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
-import org.flowable.bpmn.model.BpmnModel;
-import org.flowable.bpmn.model.StartEvent;
-import org.flowable.bpmn.model.UserTask;
+import org.flowable.bpmn.model.*;
 import org.flowable.common.engine.impl.db.SuspensionState;
 import org.flowable.engine.HistoryService;
 import org.flowable.engine.RepositoryService;
@@ -48,6 +47,7 @@ import java.util.Objects;
 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.module.bpm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseCandidateStrategy;
 
 /**
  * 流程模型实现:主要进行 Flowable {@link Model} 的维护
@@ -243,7 +243,18 @@ public class BpmModelServiceImpl implements BpmModelService {
         if (startEvent == null) {
             throw exception(MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS);
         }
-        // 2. 校验 UserTask 的 name 都配置了
+        // 2. 校验第一个用户任务的规则类型是否为 审批人自选,如果是则抛出异常,第一个用户任务的规则类型不允许是审批人自选,因为会出现无审批人的情况
+        List<SequenceFlow> outgoingFlows = startEvent.getOutgoingFlows();
+        if (CollUtil.isNotEmpty(outgoingFlows)){
+            // 2.1 获取第一个用户任务节点
+            FlowElement targetFlowElement = outgoingFlows.get(0).getTargetFlowElement();
+            // 2.2 获取审批人策略
+            Integer candidateStrategy = parseCandidateStrategy(targetFlowElement);
+            if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())){
+                throw exception(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_RULE_TYPE_NOT_APPROVE_USER_SELECT, targetFlowElement.getName());
+            }
+        }
+        // 3. 校验 UserTask 的 name 都配置了
         List<UserTask> userTasks = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class);
         userTasks.forEach(userTask -> {
             if (StrUtil.isEmpty(userTask.getName())) {

+ 10 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java

@@ -96,6 +96,15 @@ public interface BpmProcessInstanceService {
      */
     BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO);
 
+    /**
+     * 获取下一个执行节点信息。
+     *
+     * @param loginUserId 登录人的用户编号
+     * @param reqVO 请求信息
+     * @return 下一个执行节点信息
+     */
+    List<BpmApprovalDetailRespVO.ActivityNode> getNextApprovalNodes(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO);
+
     /**
      * 获取流程实例的 BPMN 模型视图
      *
@@ -172,4 +181,5 @@ public interface BpmProcessInstanceService {
      * @param instance 流程任务
      */
     void processProcessInstanceCompleted(ProcessInstance instance);
+
 }

+ 93 - 18
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java

@@ -11,9 +11,11 @@ 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.json.JsonUtils;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
@@ -55,6 +57,7 @@ import org.flowable.engine.history.HistoricProcessInstanceQuery;
 import org.flowable.engine.repository.ProcessDefinition;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.engine.runtime.ProcessInstanceBuilder;
+import org.flowable.task.api.Task;
 import org.flowable.task.api.history.HistoricTaskInstance;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
@@ -166,7 +169,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         Long startUserId = loginUserId; // 流程发起人
         HistoricProcessInstance historicProcessInstance = null; // 流程实例
         Integer processInstanceStatus = BpmProcessInstanceStatusEnum.NOT_START.getStatus(); // 流程状态
-        Map<String, Object> processVariables = reqVO.getProcessVariables(); // 流程变量
+        Map<String, Object> processVariables = new HashMap<>(); // 流程变量
         // 1.2 如果是流程已发起的场景,则使用流程实例的数据
         if (reqVO.getProcessInstanceId() != null) {
             historicProcessInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId());
@@ -177,10 +180,12 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance);
             // 合并 DB 和前端传递的流量变量,以前端的为主
             Map<String, Object> historicVariables = historicProcessInstance.getProcessVariables();
-            if (CollUtil.isNotEmpty(processVariables)) {
-                historicVariables.putAll(processVariables);
+            if (CollUtil.isNotEmpty(historicVariables)) {
+                processVariables.putAll(historicVariables);
             }
-            processVariables = historicVariables;
+        }
+        if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) {
+            processVariables.putAll(reqVO.getProcessVariables());
         }
         // 1.3 读取其它相关数据
         ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
@@ -217,12 +222,78 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel,
                 processDefinitionInfo,
                 processVariables, activities);
+        // 3.3 如果是发起动作,activityId为开始节点,不校验审批人自选节点
+        if (ObjUtil.isNotNull(reqVO.getActivityId()) && ObjUtil.equals(reqVO.getActivityId(),BpmnModelConstants.START_USER_NODE_ID)){
+            simulateActivityNodes.removeIf(node -> BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy().equals(node.getCandidateStrategy()));
+        }
 
         // 4. 拼接最终数据
         return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance,
                 processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask);
     }
 
+    @Override
+    public List<ActivityNode> getNextApprovalNodes(Long loginUserId, BpmApprovalDetailReqVO reqVO) {
+        // 1.1 校验任务存在
+        Task task = taskService.getTask(reqVO.getTaskId());
+        if (task == null) {
+            throw exception(TASK_NOT_EXISTS);
+        }
+        // 1.2 校验任务是否由当前用户审批
+        if (StrUtil.isNotBlank(task.getAssignee())
+                && ObjectUtil.notEqual(loginUserId, NumberUtils.parseLong(task.getAssignee()))) {
+            throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
+        }
+        // 1.3 校验流程实例存在
+        ProcessInstance instance = getProcessInstance(task.getProcessInstanceId());
+        if (instance == null) {
+            throw exception(PROCESS_INSTANCE_NOT_EXISTS);
+        }
+        // 1.4 校验BpmnModel
+        BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(task.getProcessDefinitionId());
+        if (bpmnModel == null) {
+            return null;
+        }
+        //1.5 流程实例是否存在
+        HistoricProcessInstance historicProcessInstance = getHistoricProcessInstance(task.getProcessInstanceId());
+        if (historicProcessInstance == null) {
+            throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS);
+        }
+
+        // 2 设置流程变量
+        Map<String, Object> processVariables = new HashMap<>();
+        // 2.1 获取历史中流程变量
+        Map<String, Object> historicVariables = historicProcessInstance.getProcessVariables();
+        if (CollUtil.isNotEmpty(historicVariables)) {
+            processVariables.putAll(historicVariables);
+        }
+        // 2.2 合并前端传递的流程变量,以前端为准
+        if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) {
+            processVariables.putAll(reqVO.getProcessVariables());
+        }
+        // 3 获取当前任务节点的信息
+        FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
+        // 3.1 获取下一个将要执行的节点集合
+        List<FlowNode> nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables);
+        return convertList(nextFlowNodes, node -> {
+            List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(),
+                    loginUserId, historicProcessInstance.getProcessDefinitionId(), processVariables);
+            // 3.2 获取节点的审批人信息
+            Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(candidateUserIds);
+            // 3.3 获取节点的审批人部门信息
+            Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+            // 3.4 存在一个节点多人审批的情况,组装审批人信息
+            List<UserSimpleBaseVO> candidateUsers = new ArrayList<>();
+            userMap.forEach((key, value) -> candidateUsers.add(BpmProcessInstanceConvert.INSTANCE.buildUser(key, userMap, deptMap)));
+            return new ActivityNode().setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
+                    .setId(node.getId())
+                    .setName(node.getName())
+                    .setStatus(BpmTaskStatusEnum.RUNNING.getStatus())
+                    .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node))
+                    .setCandidateUsers(candidateUsers);
+        });
+    }
+
     @Override
     @SuppressWarnings("unchecked")
     public PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
@@ -317,9 +388,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
      * 获得【已结束】的活动节点们
      */
     private List<ActivityNode> getEndActivityNodeList(Long startUserId, BpmnModel bpmnModel,
-            BpmProcessDefinitionInfoDO processDefinitionInfo,
-            HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus,
-            List<HistoricActivityInstance> activities, List<HistoricTaskInstance> tasks) {
+              BpmProcessDefinitionInfoDO processDefinitionInfo,
+              HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus,
+              List<HistoricActivityInstance> activities, List<HistoricTaskInstance> tasks) {
         // 遍历 tasks 列表,只处理已结束的 UserTask
         // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities
         // 的话,它无法成为一个节点
@@ -331,7 +402,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
                     .setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey())
                             ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()
                             : ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”的识别
-                                BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()))
+                            BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()))
                     .setStatus(FlowableUtils.getTaskStatus(task))
                     .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
                     .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime()))
@@ -553,7 +624,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     }
 
     private List<Long> getTaskCandidateUserList(BpmnModel bpmnModel, String activityId,
-            Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+                                                Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
         Set<Long> userIds = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId,
                 startUserId, processDefinitionId, processVariables);
         return new ArrayList<>(userIds);
@@ -589,11 +660,11 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         Set<String> finishedTaskActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId,
                 activityInstance -> activityInstance.getEndTime() != null
                         && ObjectUtil.notEqual(activityInstance.getActivityType(),
-                                BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW));
+                        BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW));
         Set<String> finishedSequenceFlowActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId,
                 activityInstance -> activityInstance.getEndTime() != null
                         && ObjectUtil.equals(activityInstance.getActivityType(),
-                                BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW));
+                        BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW));
         // 特殊:会签情况下,会有部分已完成(审批)、部分未完成(待审批),此时需要 finishedTaskActivityIds 移除掉
         finishedTaskActivityIds.removeAll(unfinishedTaskActivityIds);
         // 特殊:如果流程实例被拒绝,则需要计算是哪个活动节点。
@@ -645,8 +716,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     }
 
     private String createProcessInstance0(Long userId, ProcessDefinition definition,
-            Map<String, Object> variables, String businessKey,
-            Map<String, List<Long>> startUserSelectAssignees) {
+                                          Map<String, Object> variables, String businessKey,
+                                          Map<String, List<Long>> startUserSelectAssignees) {
         // 1.1 校验流程定义
         if (definition == null) {
             throw exception(PROCESS_DEFINITION_NOT_EXISTS);
@@ -675,11 +746,15 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中
                 BpmProcessInstanceStatusEnum.RUNNING.getStatus());
         variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为
-                                                                                             // true,不影响没配置
-                                                                                             // skipExpression 的节点
+        // true,不影响没配置
+        // skipExpression 的节点
         if (CollUtil.isNotEmpty(startUserSelectAssignees)) {
+            // 设置流程变量,发起人自选审批人
             variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES,
                     startUserSelectAssignees);
+//            // 设置流程变量,审批人自选审批人
+//            variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES,
+//                    startUserSelectAssignees);
         }
 
         // 3. 创建流程
@@ -711,17 +786,17 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
 
     private void validateStartUserSelectAssignees(Long userId, ProcessDefinition definition,
                                                   Map<String, List<Long>> startUserSelectAssignees,
-                                                  Map<String,Object> variables) {
+                                                  Map<String, Object> variables) {
         // 1. 获取预测的节点信息
         BpmApprovalDetailRespVO detailRespVO = getApprovalDetail(userId, new BpmApprovalDetailReqVO()
                 .setProcessDefinitionId(definition.getId())
                 .setProcessVariables(variables));
         List<ActivityNode> activityNodes = detailRespVO.getActivityNodes();
-        if (CollUtil.isEmpty(activityNodes)){
+        if (CollUtil.isEmpty(activityNodes)) {
             return;
         }
 
-        // 2.1 移除掉不是发起人自选审批人节点
+        // 2.1 移除掉不是发起人或者审批人自选审批人节点
         activityNodes.removeIf(task ->
                 ObjectUtil.notEqual(BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy(), task.getCandidateStrategy()));
         // 2.2 流程发起时要先获取当前流程的预测走向节点,发起时只校验预测的节点发起人自选审批人的审批人和抄送人是否都配置了

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

@@ -552,22 +552,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         // 2.3 调用 BPM complete 去完成任务
         // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用
         if (CollUtil.isNotEmpty(reqVO.getVariables())) {
-            Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
             // 校验传递的参数中是否为下一个将要执行的任务节点
-            validateNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(), bpmnModel, reqVO.getNextAssignees(), instance);
-            // 如果有下一个审批人,则设置到流程变量中
-            // TODO @小北:validateNextAssignees 升级成 validateAndSetNextAssignees,然后里面吧下面这一小段逻辑,抽进去如何?
-            if (CollUtil.isNotEmpty(reqVO.getNextAssignees())) {
-                // 获取实例中的全部节点数据,避免后续节点的审批人被覆盖
-                // TODO @小北:这里有个需要讨论的点,微信哈;
-                // TODO 因为 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES 定位是发起人,那么审批人选择的,放在 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES。目前想到两个方案:
-                // TODO 方案一:增加一个 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES,然后设置到这里面。然后,BpmTaskCandidateStartUserSelectStrategy 也从这里读
-                // TODO 方案二:也是增加一个 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES,根据节点审批人类型,放到 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES、PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES
-                // TODO 方案三:融合成 PROCESS_INSTANCE_VARIABLE_USER_SELECT_ASSIGNEES,不再区分是发起人选择、还是审批人选择。
-                Map<String, List<Long>> hisProcessVariables = FlowableUtils.getStartUserSelectAssignees(instance.getProcessVariables());
-                hisProcessVariables.putAll(reqVO.getNextAssignees());
-                variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, hisProcessVariables);
-            }
+            Map<String, Object> variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(),
+                    bpmnModel, reqVO.getNextAssignees(), instance);
             runtimeService.setVariables(task.getProcessInstanceId(), variables);
             taskService.complete(task.getId(), variables, true);
         } else {
@@ -591,42 +578,59 @@ public class BpmTaskServiceImpl implements BpmTaskService {
      * @param nextAssignees 下一个节点审批人集合(参数)
      * @param processInstance 流程实例
      */
-    private void validateNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel,
+    private Map<String, Object> validateAndSetNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel,
                                        Map<String, List<Long>> nextAssignees, ProcessInstance processInstance) {
+        // 下一个节点参数为空,不做处理,表示流程正常流转,无需选择下一个节点的审判人
+        if (CollUtil.isEmpty(nextAssignees)){
+            return variables;
+        }
         // 1. 获取当前任务节点的信息
         FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey);
         // 2. 获取下一个将要执行的节点集合
         List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables);
-
+        Map<String, List<Long>> processVariables = new HashMap<>();
         // 3. 循环下一个将要执行的节点集合
         for (FlowNode nextFlowNode : nextFlowNodes) {
-            // 3.1 获取下一个将要执行节点的属性(是否为自选审批人等)
-            // TODO @小北:public static Integer parseCandidateStrategy(FlowElement userTask) 使用这个工具方法哈。
-            Map<String, List<ExtensionElement>> extensionElements = nextFlowNode.getExtensionElements();
-            List<ExtensionElement> elements = extensionElements.get(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY);
-            if (CollUtil.isEmpty(elements)) {
-                continue;
+            // 3.1 获取下一个将要执行节点中的审批人策略
+            Integer candidateStrategy = parseCandidateStrategy(nextFlowNode);
+            // 3.2 判断节点是否为执行节点,仅校验节点
+            if (!nextAssignees.containsKey(nextFlowNode.getId())) {
+                throw exception(TASK_TARGET_NODE_NOT_EXISTS, nextFlowNode.getName());
             }
-            // 3.2 获取节点中的审批人策略
-            Integer candidateStrategy = Integer.valueOf(elements.get(0).getElementText());
-            // 3.3 获取流程实例中的发起人自选审批人
-            Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables());
-            List<Long> startUserSelectAssignee = startUserSelectAssignees.get(nextFlowNode.getId());
-            // 3.4 如果节点中的审批人策略为 发起人自选,并且该节点的审批人为空
-            if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy()) && CollUtil.isEmpty(startUserSelectAssignee)) {
-                // 先判断前端传递的参数节点节点是否为将要执行的节点
-                // TODO @小北:!nextAssignees.containsKey(nextFlowNode.getId())、和 CollUtil.isEmpty(nextAssignees.get(nextFlowNode.getId()))) 是不是等价的?
-                if (!nextAssignees.containsKey(nextFlowNode.getId())) {
-                    throw exception(TASK_TARGET_NODE_NOT_EXISTS, nextFlowNode.getName());
+            // 3.3 获取节点中的审批人
+            List<Long> assignees = nextAssignees.get(nextFlowNode.getId());
+            // 3.4 流程变量
+            // 3.5 如果节点中的审批人策略为 发起人自选
+            if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())) {
+                processVariables = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables());
+                if(processVariables == null){
+                    processVariables = new HashMap<>();
                 }
-                // 如果前端传递的节点为空,则抛出异常
-                // TODO @小北:换一个错误码哈。
-                if (CollUtil.isEmpty(nextAssignees.get(nextFlowNode.getId()))) {
+                List<Long> startUserSelectAssignee = processVariables.get(nextFlowNode.getId());
+                // 如果当前节点已经存在审批人,则不允许覆盖
+                if (CollUtil.isNotEmpty(startUserSelectAssignee)) {
+                    continue;
+                }
+                // 如果节点存在,但未配置审批人
+                if (CollUtil.isEmpty(assignees)){
                     throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
                 }
+                // 校验通过的全部节点和审批人
+                processVariables.put(nextFlowNode.getId(), assignees);
+                variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, processVariables);
+            }
+            // 3.6 如果节点中的审批人策略为 审批人,在审批时选择下一个节点的审批人,并且该节点的审批人为空
+            if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())){
+                // 如果节点存在,但未配置审批人
+                if (CollUtil.isEmpty(assignees)) {
+                    throw exception(PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
+                }
+                // 校验通过的全部节点和审批人
+                processVariables.put(nextFlowNode.getId(), assignees);
+                variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES, processVariables);
             }
-            // TODO @小北:加一个“审批人选择”的校验;
         }
+        return variables;
     }
 
     /**