Commit 10d7cbb9af1d71a60df59672eb8a0bc19d5076b7

Authored by 芋道源码
Committed by Gitee
2 parents 79d5c5e2 ff555b51

!1267 feat: 审批通过时,校验节点是否为下一个执行节点

Merge pull request !1267 from SamllNorth_Lee/fix/bpm
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 {
58 58 ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!");
59 59 ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!");
60 60 ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!");
  61 + ErrorCode TASK_START_USER_SELECT_NODE_NOT_EXISTS = new ErrorCode(1_009_004_007, "({})不是下一个执行的流程节点!");
61 62  
62 63 // ========== 动态表单模块 1-009-010-000 ==========
63 64 ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在");
... ...
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;
4 4 import jakarta.validation.constraints.NotEmpty;
5 5 import lombok.Data;
6 6  
  7 +import java.util.List;
7 8 import java.util.Map;
8 9  
9 10 @Schema(description = "管理后台 - 通过流程任务的 Request VO")
... ... @@ -23,4 +24,7 @@ public class BpmTaskApproveReqVO {
23 24 @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
24 25 private Map<String, Object> variables;
25 26  
  27 + @Schema(description = "下一个节点审批人", example = "{nodeId:[1, 2]}")
  28 + private Map<String, List<Long>> nextAssignees;
  29 +
26 30 }
... ...
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
53 53 Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance);
54 54 Assert.notNull(startUserSelectAssignees, "流程实例({}) 的发起人自选审批人不能为空",
55 55 execution.getProcessInstanceId());
56   - // 获得审批人
  56 + // 获得审批人,如果不存在,则直接返回空,fix: 用于节点预测时,如果该节点不存在发起人自选审批人,类型转换异常
57 57 List<Long> assignees = startUserSelectAssignees.get(execution.getCurrentActivityId());
  58 + if (CollUtil.isEmpty(assignees)){
  59 + return Sets.newLinkedHashSet();
  60 + }
58 61 return new LinkedHashSet<>(assignees);
59 62 }
60 63  
... ... @@ -68,8 +71,11 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand
68 71 if (startUserSelectAssignees == null) {
69 72 return Sets.newLinkedHashSet();
70 73 }
71   - // 获得审批人
  74 + // 获得审批人,如果不存在,则直接返回空,fix: 用于节点预测时,如果该节点不存在发起人自选审批人,类型转换异常
72 75 List<Long> assignees = startUserSelectAssignees.get(activityId);
  76 + if (CollUtil.isEmpty(assignees)){
  77 + return Sets.newLinkedHashSet();
  78 + }
73 79 return new LinkedHashSet<>(assignees);
74 80 }
75 81  
... ...
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 {
809 809 if (currentElement instanceof ExclusiveGateway) {
810 810 // 查找满足条件的 SequenceFlow 路径
811 811 Gateway gateway = (Gateway) currentElement;
  812 + // TODO @小北:当一个网关节点下存在多个满足的并行节点时,只查询一个节点流程流转会存在问题,需要优化,
  813 + // TODO 具体见issue:https://github.com/YunaiV/ruoyi-vue-pro/issues/761
812 814 SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
813 815 flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
814 816 && (evalConditionExpress(variables, flow.getConditionExpression())));
... ... @@ -858,6 +860,125 @@ public class BpmnModelUtils {
858 860 }
859 861  
860 862 /**
  863 + * 根据当前节点,获取下一个节点
  864 + *
  865 + * @param currentElement 当前节点
  866 + * @param bpmnModel BPMN模型
  867 + * @param variables 流程变量
  868 + */
  869 + public static List<FlowNode> getNextFlowNodes(FlowElement currentElement, BpmnModel bpmnModel,
  870 + Map<String, Object> variables){
  871 + // 下一个执行的流程节点集合
  872 + List<FlowNode> nextFlowNodes = new ArrayList<>();
  873 + // 当前执行节点的基本属性
  874 + FlowNode currentNode = (FlowNode) currentElement;
  875 + // 获取当前节点的关联节点
  876 + List<SequenceFlow> outgoingFlows = currentNode.getOutgoingFlows();
  877 + if (CollUtil.isEmpty(outgoingFlows)){
  878 + log.warn("[getNextFlowNodes][当前节点({}) 的 outgoingFlows 为空]", currentNode.getId());
  879 + return nextFlowNodes;
  880 + }
  881 + // 遍历每个出口流
  882 + for (SequenceFlow outgoingFlow : outgoingFlows) {
  883 + // 获取目标节点的基本属性
  884 + FlowElement targetElement = bpmnModel.getFlowElement(outgoingFlow.getTargetRef());
  885 + if (targetElement == null){
  886 + continue;
  887 + }
  888 + if (targetElement instanceof Gateway gateway) {
  889 + // 处理不同类型的网关
  890 + if (gateway instanceof ExclusiveGateway) {
  891 + handleExclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
  892 + } else if (gateway instanceof InclusiveGateway) {
  893 + handleInclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
  894 + } else if (gateway instanceof ParallelGateway) {
  895 + handleParallelGateway(gateway, bpmnModel, variables, nextFlowNodes);
  896 + }
  897 + } else {
  898 + // 如果不是网关,直接添加到下一个节点列表
  899 + nextFlowNodes.add((FlowNode) targetElement);
  900 + }
  901 + }
  902 + return nextFlowNodes;
  903 + }
  904 +
  905 + /**
  906 + * 处理排他网关
  907 + *
  908 + * @param gateway 排他网关
  909 + * @param bpmnModel BPMN模型
  910 + * @param variables 流程变量
  911 + * @param nextFlowNodes 下一个执行的流程节点集合
  912 + */
  913 + private static void handleExclusiveGateway(Gateway gateway, BpmnModel bpmnModel, Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
  914 + // TODO @小北: 这里和simulateNextFlowElements中有重复代码,是否重构??每个网关节点拆分出方法应该比较合理化,@芋道
  915 + SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
  916 + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
  917 + && (evalConditionExpress(variables, flow.getConditionExpression())));
  918 + if (matchSequenceFlow == null) {
  919 + matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
  920 + flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
  921 + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
  922 + if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) {
  923 + matchSequenceFlow = gateway.getOutgoingFlows().get(0);
  924 + }
  925 + }
  926 + // 遍历满足条件的 SequenceFlow 路径
  927 + if (matchSequenceFlow != null) {
  928 + FlowElement targetElement = bpmnModel.getFlowElement(matchSequenceFlow.getTargetRef());
  929 + if (targetElement instanceof FlowNode) {
  930 + nextFlowNodes.add((FlowNode) targetElement);
  931 + }
  932 + }
  933 + }
  934 +
  935 + /**
  936 + * 处理包容网关
  937 + *
  938 + * @param gateway 排他网关
  939 + * @param bpmnModel BPMN模型
  940 + * @param variables 流程变量
  941 + * @param nextFlowNodes 下一个执行的流程节点集合
  942 + */
  943 + private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel, Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
  944 + Collection<SequenceFlow> matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
  945 + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
  946 + && evalConditionExpress(variables, flow.getConditionExpression()));
  947 + if (CollUtil.isEmpty(matchSequenceFlows)) {
  948 + matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
  949 + flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
  950 + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
  951 + if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) {
  952 + matchSequenceFlows = gateway.getOutgoingFlows();
  953 + }
  954 + }
  955 + // 遍历满足条件的 SequenceFlow 路径,获取目标节点
  956 + matchSequenceFlows.forEach(flow -> {
  957 + FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
  958 + if (targetElement instanceof FlowNode) {
  959 + nextFlowNodes.add((FlowNode) targetElement);
  960 + }
  961 + });
  962 + }
  963 + /**
  964 + * 处理并行网关
  965 + *
  966 + * @param gateway 排他网关
  967 + * @param bpmnModel BPMN模型
  968 + * @param variables 流程变量
  969 + * @param nextFlowNodes 下一个执行的流程节点集合
  970 + */
  971 + private static void handleParallelGateway(Gateway gateway, BpmnModel bpmnModel, Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
  972 + // 并行网关,遍历所有出口路径,获取目标节点
  973 + gateway.getOutgoingFlows().forEach(flow -> {
  974 + FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
  975 + if (targetElement instanceof FlowNode) {
  976 + nextFlowNodes.add((FlowNode) targetElement);
  977 + }
  978 + });
  979 + }
  980 +
  981 + /**
861 982 * 计算条件表达式是否为 true 满足条件
862 983 *
863 984 * @param variables 流程实例
... ...
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
175 175 }
176 176 startUserId = Long.valueOf(historicProcessInstance.getStartUserId());
177 177 processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance);
178   - processVariables = historicProcessInstance.getProcessVariables();
  178 + // 如果流程变量不为空,则用前端传递的新变量值覆盖历史的流程变量
  179 + Map<String, Object> historicVariables = historicProcessInstance.getProcessVariables();
  180 + if (null != processVariables) {
  181 + historicVariables.putAll(processVariables);
  182 + }
  183 + processVariables = historicVariables;
179 184 }
180 185 // 1.3 读取其它相关数据
181 186 ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
... ...
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;
11 11 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
12 12 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
13 13 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
  14 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailReqVO;
  15 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
14 16 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
15 17 import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
16 18 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
17 19 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
  20 +import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
18 21 import cn.iocoder.yudao.module.bpm.enums.definition.*;
19 22 import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
20 23 import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
21 24 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
22 25 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  26 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
  27 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
23 28 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
24 29 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
25 30 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
... ... @@ -58,6 +63,7 @@ import org.springframework.transaction.support.TransactionSynchronization;
58 63 import org.springframework.transaction.support.TransactionSynchronizationManager;
59 64  
60 65 import java.util.*;
  66 +import java.util.stream.Collectors;
61 67 import java.util.stream.Stream;
62 68  
63 69 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -527,7 +533,15 @@ public class BpmTaskServiceImpl implements BpmTaskService {
527 533 // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用
528 534 if (CollUtil.isNotEmpty(reqVO.getVariables())) {
529 535 Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
530   - // 修改表单的值需要存储到 ProcessInstance 变量
  536 + // 校验传递的参数中是否为下一个将要执行的任务节点
  537 + validateNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(), bpmnModel, reqVO.getNextAssignees(), instance);
  538 + // 下个节点审批人如果不存在,则由前端传递
  539 + if (CollUtil.isNotEmpty(reqVO.getNextAssignees())) {
  540 + // 获取实例中的全部节点数据,避免后续节点的审批人被覆盖
  541 + Map<String, List<Long>> hisProcessVariables = FlowableUtils.getStartUserSelectAssignees(instance.getProcessVariables());
  542 + hisProcessVariables.putAll(reqVO.getNextAssignees());
  543 + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, hisProcessVariables);
  544 + }
531 545 runtimeService.setVariables(task.getProcessInstanceId(), variables);
532 546 taskService.complete(task.getId(), variables, true);
533 547 } else {
... ... @@ -538,6 +552,50 @@ public class BpmTaskServiceImpl implements BpmTaskService {
538 552 handleParentTaskIfSign(task.getParentTaskId());
539 553 }
540 554  
  555 +
  556 + /**
  557 + * 校验传递的参数中是否为下一个将要执行的任务节点
  558 + *
  559 + * @param taskDefinitionKey 当前任务节点id
  560 + * @param variables 流程变量
  561 + * @param bpmnModel 流程模型
  562 + * @param nextActivityNodes 下一个节点审批人集合(参数)
  563 + */
  564 + private void validateNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel,
  565 + Map<String, List<Long>> nextActivityNodes,ProcessInstance processInstance){
  566 + // 1、获取当前任务节点的信息
  567 + FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey);
  568 + // 2、获取下一个将要执行的节点集合
  569 + List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables);
  570 + // 3、循环下一个将要执行的节点集合
  571 + for (FlowNode nextFlowNode : nextFlowNodes) {
  572 + // 3.1、获取下一个将要执行节点的属性(是否为自选审批人等)
  573 + Map<String, List<ExtensionElement>> extensionElements = nextFlowNode.getExtensionElements();
  574 + List<ExtensionElement> elements = extensionElements.get(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY);
  575 + if (CollUtil.isEmpty(elements)){
  576 + continue;
  577 + }
  578 + // 3.2、获取节点中的审批人策略
  579 + Integer candidateStrategy = Integer.valueOf(elements.get(0).getElementText());
  580 + // 3.3、获取流程实例中的发起人自选审批人
  581 + Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables());
  582 + List<Long> startUserSelectAssignee = startUserSelectAssignees.get(nextFlowNode.getId());
  583 + // 3.4、如果节点中的审批人策略为 发起人自选,并且该节点的审批人为空
  584 + if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy()) && CollUtil.isEmpty(startUserSelectAssignee)) {
  585 + // 先判断前端传递的参数节点节点是否为将要执行的节点
  586 + if (!nextActivityNodes.containsKey(nextFlowNode.getId())){
  587 + throw exception(TASK_TARGET_NODE_NOT_EXISTS, nextFlowNode.getName());
  588 + }
  589 + // 如果节点存在,则获取节点中的审批人
  590 + List<Long> nextAssignees = nextActivityNodes.get(nextFlowNode.getId());
  591 + // 如果前端传递的节点为空,则抛出异常
  592 + if (CollUtil.isEmpty(nextAssignees)) {
  593 + throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
  594 + }
  595 + }
  596 + }
  597 + }
  598 +
541 599 /**
542 600 * 审批通过存在“后加签”的任务。
543 601 * <p>
... ...