Pārlūkot izejas kodu

!1267 feat: 审批通过时,校验节点是否为下一个执行节点
Merge pull request !1267 from SamllNorth_Lee/fix/bpm

芋道源码 5 mēneši atpakaļ
vecāks
revīzija
10d7cbb9af

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

@@ -58,6 +58,7 @@ public interface ErrorCodeConstants {
     ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!");
     ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!");
     ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!");
+    ErrorCode TASK_START_USER_SELECT_NODE_NOT_EXISTS = new ErrorCode(1_009_004_007, "({})不是下一个执行的流程节点!");
 
     // ========== 动态表单模块 1-009-010-000 ==========
     ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在");

+ 4 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotEmpty;
 import lombok.Data;
 
+import java.util.List;
 import java.util.Map;
 
 @Schema(description = "管理后台 - 通过流程任务的 Request VO")
@@ -23,4 +24,7 @@ public class BpmTaskApproveReqVO {
     @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
     private Map<String, Object> variables;
 
+    @Schema(description = "下一个节点审批人", example = "{nodeId:[1, 2]}")
+    private Map<String, List<Long>> nextAssignees;
+
 }

+ 8 - 2
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java

@@ -53,8 +53,11 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand
         Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance);
         Assert.notNull(startUserSelectAssignees, "流程实例({}) 的发起人自选审批人不能为空",
                 execution.getProcessInstanceId());
-        // 获得审批人
+        // 获得审批人,如果不存在,则直接返回空,fix: 用于节点预测时,如果该节点不存在发起人自选审批人,类型转换异常
         List<Long> assignees = startUserSelectAssignees.get(execution.getCurrentActivityId());
+        if (CollUtil.isEmpty(assignees)){
+            return Sets.newLinkedHashSet();
+        }
         return new LinkedHashSet<>(assignees);
     }
 
@@ -68,8 +71,11 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand
         if (startUserSelectAssignees == null) {
             return Sets.newLinkedHashSet();
         }
-        // 获得审批人
+        // 获得审批人,如果不存在,则直接返回空,fix: 用于节点预测时,如果该节点不存在发起人自选审批人,类型转换异常
         List<Long> assignees = startUserSelectAssignees.get(activityId);
+        if (CollUtil.isEmpty(assignees)){
+            return Sets.newLinkedHashSet();
+        }
         return new LinkedHashSet<>(assignees);
     }
 

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

@@ -809,6 +809,8 @@ public class BpmnModelUtils {
         if (currentElement instanceof ExclusiveGateway) {
             // 查找满足条件的 SequenceFlow 路径
             Gateway gateway = (Gateway) currentElement;
+            // TODO @小北:当一个网关节点下存在多个满足的并行节点时,只查询一个节点流程流转会存在问题,需要优化,
+            // 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())));
@@ -857,6 +859,125 @@ public class BpmnModelUtils {
         }
     }
 
+    /**
+     * 根据当前节点,获取下一个节点
+     *
+     * @param currentElement 当前节点
+     * @param bpmnModel  BPMN模型
+     * @param variables 流程变量
+     */
+    public static List<FlowNode> getNextFlowNodes(FlowElement currentElement, BpmnModel bpmnModel,
+                                                  Map<String, Object> variables){
+        // 下一个执行的流程节点集合
+        List<FlowNode> nextFlowNodes = new ArrayList<>();
+        // 当前执行节点的基本属性
+        FlowNode currentNode = (FlowNode) currentElement;
+        // 获取当前节点的关联节点
+        List<SequenceFlow> outgoingFlows = currentNode.getOutgoingFlows();
+        if (CollUtil.isEmpty(outgoingFlows)){
+            log.warn("[getNextFlowNodes][当前节点({}) 的 outgoingFlows 为空]", currentNode.getId());
+            return nextFlowNodes;
+        }
+        // 遍历每个出口流
+        for (SequenceFlow outgoingFlow : outgoingFlows) {
+            // 获取目标节点的基本属性
+            FlowElement targetElement = bpmnModel.getFlowElement(outgoingFlow.getTargetRef());
+            if (targetElement == null){
+                continue;
+            }
+            if (targetElement instanceof Gateway gateway) {
+                // 处理不同类型的网关
+                if (gateway instanceof ExclusiveGateway) {
+                    handleExclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
+                } else if (gateway instanceof InclusiveGateway) {
+                    handleInclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
+                } else if (gateway instanceof ParallelGateway) {
+                    handleParallelGateway(gateway, bpmnModel, variables, nextFlowNodes);
+                }
+            } else {
+                // 如果不是网关,直接添加到下一个节点列表
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        }
+        return nextFlowNodes;
+    }
+
+    /**
+     * 处理排他网关
+     *
+     * @param gateway 排他网关
+     * @param bpmnModel BPMN模型
+     * @param variables 流程变量
+     * @param nextFlowNodes 下一个执行的流程节点集合
+     */
+    private static void handleExclusiveGateway(Gateway gateway, BpmnModel bpmnModel, Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
+       // TODO @小北: 这里和simulateNextFlowElements中有重复代码,是否重构??每个网关节点拆分出方法应该比较合理化,@芋道
+        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 路径
+        if (matchSequenceFlow != null) {
+            FlowElement targetElement = bpmnModel.getFlowElement(matchSequenceFlow.getTargetRef());
+            if (targetElement instanceof FlowNode) {
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        }
+    }
+
+    /**
+     * 处理包容网关
+     *
+     * @param gateway 排他网关
+     * @param bpmnModel BPMN模型
+     * @param variables 流程变量
+     * @param nextFlowNodes 下一个执行的流程节点集合
+     */
+    private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel, Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
+        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();
+            }
+        }
+        // 遍历满足条件的 SequenceFlow 路径,获取目标节点
+        matchSequenceFlows.forEach(flow -> {
+            FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
+            if (targetElement instanceof FlowNode) {
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        });
+    }
+    /**
+     * 处理并行网关
+     *
+     * @param gateway 排他网关
+     * @param bpmnModel BPMN模型
+     * @param variables 流程变量
+     * @param nextFlowNodes 下一个执行的流程节点集合
+     */
+    private static void handleParallelGateway(Gateway gateway, BpmnModel bpmnModel, Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
+        // 并行网关,遍历所有出口路径,获取目标节点
+        gateway.getOutgoingFlows().forEach(flow -> {
+            FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
+            if (targetElement instanceof FlowNode) {
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        });
+    }
+
     /**
      * 计算条件表达式是否为 true 满足条件
      *

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

@@ -175,7 +175,12 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             }
             startUserId = Long.valueOf(historicProcessInstance.getStartUserId());
             processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance);
-            processVariables = historicProcessInstance.getProcessVariables();
+            // 如果流程变量不为空,则用前端传递的新变量值覆盖历史的流程变量
+            Map<String, Object> historicVariables = historicProcessInstance.getProcessVariables();
+            if (null != processVariables) {
+                historicVariables.putAll(processVariables);
+            }
+            processVariables = historicVariables;
         }
         // 1.3 读取其它相关数据
         ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(

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

@@ -11,15 +11,20 @@ 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.framework.web.core.util.WebFrameworkUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
 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.definition.BpmFormDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.bpm.enums.definition.*;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
@@ -58,6 +63,7 @@ import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import java.util.*;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -527,7 +533,15 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用
         if (CollUtil.isNotEmpty(reqVO.getVariables())) {
             Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
-            // 修改表单的值需要存储到 ProcessInstance 变量
+            // 校验传递的参数中是否为下一个将要执行的任务节点
+            validateNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(), bpmnModel, reqVO.getNextAssignees(), instance);
+            // 下个节点审批人如果不存在,则由前端传递
+            if (CollUtil.isNotEmpty(reqVO.getNextAssignees())) {
+                // 获取实例中的全部节点数据,避免后续节点的审批人被覆盖
+                Map<String, List<Long>> hisProcessVariables = FlowableUtils.getStartUserSelectAssignees(instance.getProcessVariables());
+                hisProcessVariables.putAll(reqVO.getNextAssignees());
+                variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, hisProcessVariables);
+            }
             runtimeService.setVariables(task.getProcessInstanceId(), variables);
             taskService.complete(task.getId(), variables, true);
         } else {
@@ -538,6 +552,50 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         handleParentTaskIfSign(task.getParentTaskId());
     }
 
+
+    /**
+     * 校验传递的参数中是否为下一个将要执行的任务节点
+     *
+     * @param taskDefinitionKey 当前任务节点id
+     * @param variables 流程变量
+     * @param bpmnModel 流程模型
+     * @param nextActivityNodes 下一个节点审批人集合(参数)
+     */
+    private void validateNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel,
+                                                Map<String, List<Long>> nextActivityNodes,ProcessInstance processInstance){
+        // 1、获取当前任务节点的信息
+        FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey);
+        // 2、获取下一个将要执行的节点集合
+        List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables);
+        // 3、循环下一个将要执行的节点集合
+        for (FlowNode nextFlowNode : nextFlowNodes) {
+            // 3.1、获取下一个将要执行节点的属性(是否为自选审批人等)
+            Map<String, List<ExtensionElement>> extensionElements = nextFlowNode.getExtensionElements();
+            List<ExtensionElement> elements = extensionElements.get(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY);
+            if (CollUtil.isEmpty(elements)){
+                continue;
+            }
+            // 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)) {
+                // 先判断前端传递的参数节点节点是否为将要执行的节点
+                if (!nextActivityNodes.containsKey(nextFlowNode.getId())){
+                    throw exception(TASK_TARGET_NODE_NOT_EXISTS, nextFlowNode.getName());
+                }
+                // 如果节点存在,则获取节点中的审批人
+                List<Long> nextAssignees = nextActivityNodes.get(nextFlowNode.getId());
+                // 如果前端传递的节点为空,则抛出异常
+                if (CollUtil.isEmpty(nextAssignees)) {
+                    throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
+                }
+            }
+        }
+    }
+
     /**
      * 审批通过存在“后加签”的任务。
      * <p>