【代码评审】BPM:下一个审批人

This commit is contained in:
YunaiV 2025-03-01 16:34:30 +08:00
parent 10d7cbb9af
commit fedb9242b5
6 changed files with 62 additions and 80 deletions

View File

@ -58,7 +58,6 @@ 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, "动态表单不存在");

View File

@ -25,6 +25,6 @@ public class BpmTaskApproveReqVO {
private Map<String, Object> variables;
@Schema(description = "下一个节点审批人", example = "{nodeId:[1, 2]}")
private Map<String, List<Long>> nextAssignees;
private Map<String, List<Long>> nextAssignees; // 为什么是 Map而不是 List 因为下一个节点可能是多个例如说并行网关的情况
}

View File

@ -2,24 +2,21 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.d
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
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.BpmnModelUtils;
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.bpmn.model.ServiceTask;
import org.flowable.bpmn.model.Task;
import org.flowable.bpmn.model.UserTask;
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.*;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
/**
* 发起人自选 {@link BpmTaskCandidateUserStrategy} 实现类
@ -53,12 +50,9 @@ 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);
return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
}
@Override
@ -71,33 +65,9 @@ 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);
}
/**
* 获得发起人自选审批人或抄送人的 Task 列表
*
* @param bpmnModel BPMN 模型
* @return Task 列表
*/
public static List<Task> getStartUserSelectTaskList(BpmnModel bpmnModel) {
if (bpmnModel == null) {
return Collections.emptyList();
}
List<Task> tasks = new ArrayList<>();
tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class));
tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, ServiceTask.class));
if (CollUtil.isEmpty(tasks)) {
return Collections.emptyList();
}
tasks.removeIf(task -> ObjectUtil.notEqual(BpmnModelUtils.parseCandidateStrategy(task),
BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy()));
return tasks;
return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
}
}

View File

@ -809,8 +809,7 @@ public class BpmnModelUtils {
if (currentElement instanceof ExclusiveGateway) {
// 查找满足条件的 SequenceFlow 路径
Gateway gateway = (Gateway) currentElement;
// TODO @小北当一个网关节点下存在多个满足的并行节点时只查询一个节点流程流转会存在问题需要优化
// TODO 具体见issuehttps://github.com/YunaiV/ruoyi-vue-pro/issues/761
// TODO @小北当一个网关节点下存在多个满足的并行节点时只查询一个节点流程流转会存在问题需要优化具体见issuehttps://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())));
@ -866,27 +865,27 @@ public class BpmnModelUtils {
* @param bpmnModel BPMN模型
* @param variables 流程变量
*/
@SuppressWarnings("PatternVariableCanBeUsed")
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)){
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){
if (targetElement == null) {
continue;
}
if (targetElement instanceof Gateway gateway) {
// 处理不同类型的网关
// 情况一处理不同类型的网关
if (targetElement instanceof Gateway) {
Gateway gateway = (Gateway) targetElement;
if (gateway instanceof ExclusiveGateway) {
handleExclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
} else if (gateway instanceof InclusiveGateway) {
@ -895,7 +894,7 @@ public class BpmnModelUtils {
handleParallelGateway(gateway, bpmnModel, variables, nextFlowNodes);
}
} else {
// 如果不是网关直接添加到下一个节点列表
// 情况二如果不是网关直接添加到下一个节点列表
nextFlowNodes.add((FlowNode) targetElement);
}
}
@ -903,15 +902,17 @@ public class BpmnModelUtils {
}
/**
* 处理排网关
* 处理排网关
*
* @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中有重复代码是否重构每个网关节点拆分出方法应该比较合理化@芋道
private static void handleExclusiveGateway(Gateway gateway, BpmnModel bpmnModel,
Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
// TODO @小北 这里和 simulateNextFlowElements 中有重复代码是否重构每个网关节点拆分出方法应该比较合理化@芋艿
// TODO @小北ok simulateNextFlowElements 里面处理网关的复用这个方法可以么
SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
&& (evalConditionExpress(variables, flow.getConditionExpression())));
@ -940,7 +941,8 @@ public class BpmnModelUtils {
* @param variables 流程变量
* @param nextFlowNodes 下一个执行的流程节点集合
*/
private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel, Map<String, Object> variables, List<FlowNode> 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()));
@ -960,6 +962,7 @@ public class BpmnModelUtils {
}
});
}
/**
* 处理并行网关
*
@ -968,7 +971,8 @@ public class BpmnModelUtils {
* @param variables 流程变量
* @param nextFlowNodes 下一个执行的流程节点集合
*/
private static void handleParallelGateway(Gateway gateway, BpmnModel bpmnModel, Map<String, Object> variables, List<FlowNode> 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());

View File

@ -175,9 +175,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
}
startUserId = Long.valueOf(historicProcessInstance.getStartUserId());
processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance);
// 如果流程变量不为空则用前端传递的新变量值覆盖历史的流程变量
// 合并 DB 和前端传递的流量变量以前端的为主
Map<String, Object> historicVariables = historicProcessInstance.getProcessVariables();
if (null != processVariables) {
if (CollUtil.isNotEmpty(processVariables)) {
historicVariables.putAll(processVariables);
}
processVariables = historicVariables;

View File

@ -11,13 +11,10 @@ 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;
@ -63,7 +60,6 @@ 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;
@ -535,9 +531,15 @@ public class BpmTaskServiceImpl implements BpmTaskService {
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_ASSIGNEESPROCESS_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);
@ -554,45 +556,52 @@ public class BpmTaskServiceImpl implements BpmTaskService {
/**
* 校验传递的参数中是否为下一个将要执行的任务节点
* 校验选择的下一个节点的审批人是否合法
*
* @param taskDefinitionKey 当前任务节点id
* 1. 是否有漏选没有选择审批人
* 2. 是否有多选非下一个节点
*
* @param taskDefinitionKey 当前任务节点标识
* @param variables 流程变量
* @param bpmnModel 流程模型
* @param nextActivityNodes 下一个节点审批人集合参数
* @param nextAssignees 下一个节点审批人集合参数
* @param processInstance 流程实例
*/
private void validateNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel,
Map<String, List<Long>> nextActivityNodes,ProcessInstance processInstance){
// 1获取当前任务节点的信息
Map<String, List<Long>> nextAssignees, ProcessInstance processInstance) {
// 1. 获取当前任务节点的信息
FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey);
// 2获取下一个将要执行的节点集合
// 2. 获取下一个将要执行的节点集合
List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables);
// 3循环下一个将要执行的节点集合
// 3. 循环下一个将要执行的节点集合
for (FlowNode nextFlowNode : nextFlowNodes) {
// 3.1获取下一个将要执行节点的属性是否为自选审批人等
// 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)){
if (CollUtil.isEmpty(elements)) {
continue;
}
// 3.2获取节点中的审批人策略
// 3.2 获取节点中的审批人策略
Integer candidateStrategy = Integer.valueOf(elements.get(0).getElementText());
// 3.3获取流程实例中的发起人自选审批人
// 3.3 获取流程实例中的发起人自选审批人
Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables());
List<Long> startUserSelectAssignee = startUserSelectAssignees.get(nextFlowNode.getId());
// 3.4如果节点中的审批人策略为 发起人自选并且该节点的审批人为空
// 3.4 如果节点中的审批人策略为 发起人自选并且该节点的审批人为空
if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy()) && CollUtil.isEmpty(startUserSelectAssignee)) {
// 先判断前端传递的参数节点节点是否为将要执行的节点
if (!nextActivityNodes.containsKey(nextFlowNode.getId())){
// 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());
}
// 如果节点存在则获取节点中的审批人
List<Long> nextAssignees = nextActivityNodes.get(nextFlowNode.getId());
// 如果前端传递的节点为空则抛出异常
if (CollUtil.isEmpty(nextAssignees)) {
// TODO @小北换一个错误码哈
if (CollUtil.isEmpty(nextAssignees.get(nextFlowNode.getId()))) {
throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
}
}
// TODO @小北加一个审批人选择的校验
}
}