feat: 子流程-多实例

This commit is contained in:
Lesan 2025-02-26 15:18:34 +08:00
parent 9c9f2812e9
commit 8a9d64bbaf
8 changed files with 198 additions and 52 deletions

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 子流程多实例来源类型枚举
*
* @author Lesan
*/
@Getter
@AllArgsConstructor
public enum BpmChildProcessMultiInstanceSourceTypeEnum implements ArrayValuable<Integer> {
FIXED_QUANTITY(1, "固定数量"),
DIGITAL_FORM(2, "数字表单"),
MULTI_FORM(3, "多项表单");
private final Integer type;
private final String name;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessMultiInstanceSourceTypeEnum::getType).toArray(Integer[]::new);
public static BpmChildProcessMultiInstanceSourceTypeEnum typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
}
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@ -452,6 +452,9 @@ public class BpmSimpleModelNodeVO {
@Schema(description = "超时设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
private TimeoutSetting timeoutSetting;
@Schema(description = "多实例设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
private MultiInstanceSetting multiInstanceSetting;
@Schema(description = "子流程发起人配置")
@Data
@Valid
@ -490,5 +493,33 @@ public class BpmSimpleModelNodeVO {
}
@Schema(description = "多实例设置")
@Data
@Valid
public static class MultiInstanceSetting {
@Schema(description = "是否开启多实例", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否开启多实例不能为空")
private Boolean enable;
@Schema(description = "是否串行", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否串行不能为空")
private Boolean sequential;
@Schema(description = "完成比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "完成比例不能为空")
private Integer completeRatio;
@Schema(description = "多实例来源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "多实例来源类型不能为空")
@InEnum(BpmChildProcessMultiInstanceSourceTypeEnum.class)
private Integer sourceType;
@Schema(description = "多实例来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "多实例来源不能为空")
private String source;
}
}
}

View File

@ -1,15 +1,21 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import lombok.Setter;
import org.flowable.bpmn.model.Activity;
import org.flowable.bpmn.model.CallActivity;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior;
import java.util.List;
import java.util.Set;
/**
@ -42,27 +48,42 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
*/
@Override
protected int resolveNrOfInstances(DelegateExecution execution) {
// 第一步设置 collectionVariable CollectionVariable
// execution.getVariable() 读取所有任务处理人的 key
super.collectionExpression = null; // collectionExpression collectionVariable 是互斥的
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
// execution.getVariable() 读取当前所有任务处理的人的 key
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
if (execution.getCurrentFlowElement() instanceof UserTask) {
// 第一步设置 collectionVariable CollectionVariable
// execution.getVariable() 读取所有任务处理人的 key
super.collectionExpression = null; // collectionExpression collectionVariable 是互斥的
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
// execution.getVariable() 读取当前所有任务处理的人的 key
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
// 第二步获取任务的所有处理人
@SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素避免自动通过
// 这样保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
// 用途1审批人为空时2审批类型为自动通过自动拒绝时
assigneeUserIds = SetUtils.asSet((Long) null);
// 第二步获取任务的所有处理人
@SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素避免自动通过
// 这样保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
// 用途1审批人为空时2审批类型为自动通过自动拒绝时
assigneeUserIds = SetUtils.asSet((Long) null);
}
execution.setVariableLocal(super.collectionVariable, assigneeUserIds);
}
execution.setVariableLocal(super.collectionVariable, assigneeUserIds);
return assigneeUserIds.size();
}
return assigneeUserIds.size();
if (execution.getCurrentFlowElement() instanceof CallActivity) {
FlowElement flowElement = execution.getCurrentFlowElement();
Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.DIGITAL_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
}
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTI_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
}
}
return super.resolveNrOfInstances(execution);
}
}

View File

@ -2,14 +2,20 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import lombok.Setter;
import org.flowable.bpmn.model.Activity;
import org.flowable.bpmn.model.CallActivity;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
import java.util.List;
import java.util.Set;
/**
@ -35,28 +41,43 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
*/
@Override
protected int resolveNrOfInstances(DelegateExecution execution) {
// 第一步设置 collectionVariable CollectionVariable
// execution.getVariable() 读取所有任务处理人的 key
super.collectionExpression = null; // collectionExpression collectionVariable 是互斥的
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
// execution.getVariable() 读取当前所有任务处理的人的 key
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
if (execution.getCurrentFlowElement() instanceof UserTask) {
// 第一步设置 collectionVariable CollectionVariable
// execution.getVariable() 读取所有任务处理人的 key
super.collectionExpression = null; // collectionExpression collectionVariable 是互斥的
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
// execution.getVariable() 读取当前所有任务处理的人的 key
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
// 第二步获取任务的所有处理人
// 不使用 execution.getVariable 原因目前依次审批任务回退后 collectionVariable 变量没有清理 如果重新进入该任务不会重新分配审批人
@SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class);
if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素避免自动通过
// 这样保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
// 用途1审批人为空时2审批类型为自动通过自动拒绝时
assigneeUserIds = SetUtils.asSet((Long) null);
// 第二步获取任务的所有处理人
// 不使用 execution.getVariable 原因目前依次审批任务回退后 collectionVariable 变量没有清理 如果重新进入该任务不会重新分配审批人
@SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class);
if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素避免自动通过
// 这样保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
// 用途1审批人为空时2审批类型为自动通过自动拒绝时
assigneeUserIds = SetUtils.asSet((Long) null);
}
execution.setVariableLocal(super.collectionVariable, assigneeUserIds);
}
execution.setVariableLocal(super.collectionVariable, assigneeUserIds);
return assigneeUserIds.size();
}
return assigneeUserIds.size();
if (execution.getCurrentFlowElement() instanceof CallActivity) {
FlowElement flowElement = execution.getCurrentFlowElement();
Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.DIGITAL_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
}
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTI_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
}
}
return super.resolveNrOfInstances(execution);
}
}

View File

@ -68,6 +68,11 @@ public interface BpmnModelConstants {
*/
String USER_TASK_APPROVE_METHOD = "approveMethod";
/**
* BPMN Child Process 的扩展属性用于标记多实例来源类型
*/
String CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = "childProcessMultiInstanceSourceType";
/**
* BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限
*/

View File

@ -158,6 +158,17 @@ public class BpmnModelUtils {
return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE));
}
/**
* 解析子流程多实例来源类型
*
* @see BpmChildProcessMultiInstanceSourceTypeEnum
* @param element 任务节点
* @return 多实例来源类型
*/
public static Integer parseMultiInstanceSourceType(FlowElement element) {
return NumberUtils.parseInt(parseExtensionElement(element, BpmnModelConstants.CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE));
}
/**
* 添加任务拒绝处理元素
*

View File

@ -853,18 +853,35 @@ public class SimpleModelUtils {
callActivity.setExecutionListeners(executionListeners);
// 7. 超时设置
if (childProcessSetting.getTimeoutSetting() != null) {
if (childProcessSetting.getTimeoutSetting() != null && Boolean.TRUE.equals(childProcessSetting.getTimeoutSetting().getEnable())) {
BoundaryEvent boundaryEvent = null;
if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
node.getDelaySetting().getDelayTime(), null, null);
} else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
childProcessSetting.getTimeoutSetting().getTimeExpression(), null, null);
} else if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType(),
null, null, node.getDelaySetting().getDelayTime());
null, null, childProcessSetting.getTimeoutSetting().getTimeExpression());
}
flowElements.add(boundaryEvent);
}
// 8. 多实例
if (childProcessSetting.getMultiInstanceSetting() != null && Boolean.TRUE.equals(childProcessSetting.getMultiInstanceSetting().getEnable())) {
MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
multiInstanceCharacteristics.setSequential(childProcessSetting.getMultiInstanceSetting().getSequential());
if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY.getType())) {
multiInstanceCharacteristics.setLoopCardinality(childProcessSetting.getMultiInstanceSetting().getSource());
}
if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.DIGITAL_FORM.getType()) ||
childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTI_FORM.getType())) {
multiInstanceCharacteristics.setInputDataItem(childProcessSetting.getMultiInstanceSetting().getSource());
}
multiInstanceCharacteristics.setCompletionCondition(String.format("${ nrOfCompletedInstances/nrOfInstances >= %s}",
String.format("%.2f", childProcessSetting.getMultiInstanceSetting().getCompleteRatio() / 100D)));
callActivity.setLoopCharacteristics(multiInstanceCharacteristics);
addExtensionElement(callActivity, CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE, childProcessSetting.getMultiInstanceSetting().getSourceType());
}
// 添加节点类型
addNodeType(node.getType(), callActivity);
flowElements.add(callActivity);

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.bpm.service.task.listener;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
@ -9,13 +10,14 @@ import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserEmpt
import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import jakarta.annotation.Resource;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.flowable.engine.impl.el.FixedValue;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Component;
import java.util.List;
@ -37,30 +39,32 @@ public class BpmCallActivityListener implements ExecutionListener {
@Resource
private BpmProcessDefinitionService processDefinitionService;
@Resource
private BpmProcessInstanceService processInstanceService;
@Override
public void notify(DelegateExecution execution) {
String expressionText = listenerConfig.getExpressionText();
Assert.notNull(expressionText, "监听器扩展字段({})不能为空", expressionText);
BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting startUserSetting = JsonUtils.parseObject(
expressionText, BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting.class);
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getRootProcessInstanceId());
// 1. 当发起人来源为主流程发起人时并兜底 startUserSetting 为空时
if (startUserSetting == null
|| startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())) {
ExecutionEntity parent = (ExecutionEntity) execution.getParent();
FlowableUtils.setAuthenticatedUserId(Long.parseLong(parent.getStartUserId()));
FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
return;
}
// 2. 当发起人来源为表单时
if (startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.FROM_FORM.getType())) {
ExecutionEntity parent = (ExecutionEntity) execution.getParent();
String formFieldValue = parent.getVariable(startUserSetting.getFormField(), String.class);
String formFieldValue = MapUtil.getStr(processInstance.getProcessVariables(), startUserSetting.getFormField());
// 2.1 当表单值为空时
if (StrUtil.isEmpty(formFieldValue)) {
// 2.1.1 来自主流程发起人
if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType())) {
FlowableUtils.setAuthenticatedUserId(Long.parseLong(parent.getStartUserId()));
FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
return;
}
// 2.1.2 来自子流程管理员
@ -72,7 +76,7 @@ public class BpmCallActivityListener implements ExecutionListener {
}
// 2.1.3 来自主流程管理员
if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_ADMIN.getType())) {
BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(parent.getProcessDefinitionId());
BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(processInstance.getProcessDefinitionId());
List<Long> managerUserIds = processDefinition.getManagerUserIds();
FlowableUtils.setAuthenticatedUserId(managerUserIds.get(0));
return;
@ -84,7 +88,7 @@ public class BpmCallActivityListener implements ExecutionListener {
} catch (Exception e) {
log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]",
DELEGATE_EXPRESSION, formFieldValue);
FlowableUtils.setAuthenticatedUserId(Long.parseLong(parent.getStartUserId()));
FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
}
}
}