# scaffold 项目之工作流的会签、或签、依次审批
# 简介
可以看 scaffold 项目之工作流 文章
# 开始使用
支持多人审批,包括:
- 会签(并行会签):同一个审批节点设置多个人(如 A、B、C 三人,三人会同时收到待办任务),需全部同意之后,审批才可到下一审批节点
- 或签(并行或签):同一个审批节点设置多个人,任意一个人处理后,就能进入下一个节点
- 依次审批(顺序会签):同一个审批节点设置多个人(如 A、B、C 三人),三人按顺序依次收到待办,即 A 先审批,A 提交后 B 才能审批,需全部同意之后,审批才可到下一审批节点
# 多人审批
# 会签(并行会签)
在流程设计中,选择【任务节点】 然后在选择【多人审批方式】
会签(可同时审批,至少 % 人必须审批通过)
# 或签(并行或签)
在流程设计中,选择【任务节点】 然后在选择【多人审批方式】
或签(可同时审批,有一人通过即可)
因此,会签和或签的差异,就在于完成条件的不同。
# 随机挑选一人审批
在流程设计中,选择【任务节点】 然后在选择【多人审批方式】
就是随机选中指定一名人员审批
# 依次审批
在流程设计中,选择【任务节点】 然后在选择【多人审批方式】
依次审批配置:顺序多重事件,实现多个人按顺序审批
因此,依次审批和会签的差异,就在于是否并行审批。
按照这个思路,实现 “票签”,是不是很简单?!
友情提示:什么是 “票签”?
指同一个审批节点设置多个人,如 A、B、C 三人,当通过比例大于 50% 就能进入下一个节点。
# 实现原理
在 [《选择审批人、发起人自选》] 小节中,我们看到使用 BpmUserTaskActivityBehavior 实现了审批任务的审批人分配。实际上,还有两个 Behavior 类,如下所示:
package cn.tzzfj.scaffold.module.bpm.framework.flowable.core.behavior; | |
import cn.tzzfj.scaffold.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; | |
import lombok.Setter; | |
import org.flowable.bpmn.model.Activity; | |
import org.flowable.bpmn.model.UserTask; | |
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; | |
import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; | |
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; | |
import org.flowable.engine.impl.bpmn.behavior.UserTaskActivityBehavior; | |
import org.flowable.engine.impl.bpmn.parser.factory.DefaultActivityBehaviorFactory; | |
/** | |
* <p> Project: scaffold - BpmActivityBehaviorFactory </p> | |
* | |
* 自定义的 ActivityBehaviorFactory 实现类,目的如下: | |
* | |
* 1. 自定义 {@link #createUserTaskActivityBehavior (UserTask)}:实现自定义的流程任务的 assignee 负责人的分配 | |
* | |
* @author Tz | |
* @date 2025/10/25 15:26 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Setter | |
public class BpmActivityBehaviorFactory extends DefaultActivityBehaviorFactory { | |
private BpmTaskCandidateInvoker taskCandidateInvoker; | |
@Override | |
public UserTaskActivityBehavior createUserTaskActivityBehavior(UserTask userTask) { | |
return new BpmUserTaskActivityBehavior(userTask) | |
.setTaskCandidateInvoker(taskCandidateInvoker); | |
} | |
@Override | |
public ParallelMultiInstanceBehavior createParallelMultiInstanceBehavior(Activity activity, | |
AbstractBpmnActivityBehavior behavior) { | |
return new BpmParallelMultiInstanceBehavior(activity, behavior) | |
.setTaskCandidateInvoker(taskCandidateInvoker); | |
} | |
@Override | |
public SequentialMultiInstanceBehavior createSequentialMultiInstanceBehavior(Activity activity, | |
AbstractBpmnActivityBehavior behavior) { | |
return new BpmSequentialMultiInstanceBehavior(activity, behavior) | |
.setTaskCandidateInvoker(taskCandidateInvoker); | |
} | |
} |
- [BpmParallelMultiInstanceBehavior] 并行 + 多实例(单节点多任务)的 Behavior 类
- [BpmSequentialMultiInstanceBehavior] 顺序 + 多实例(单节点多任务)的 Behavior 类
# 并行 BpmParallelMultiInstanceBehavior
① BpmParallelMultiInstanceBehavior 实现 Flowable ParallelMultiInstanceBehavior 类,实现单节点多任务的审批人 “计算”。如下所示:
package cn.tzzfj.scaffold.module.bpm.framework.flowable.core.behavior; | |
/** | |
* <p> Project: scaffold - BpmParallelMultiInstanceBehavior </p> | |
* | |
* 自定义的【并行】的【多个】流程任务的 assignee 负责人的分配 | |
* | |
* 第一步,基于分配规则,计算出分配任务的【多个】候选人们。 | |
* | |
* 第二步,将【多个】任务候选人们,设置到 DelegateExecution 的 collectionVariable 变量中,以便 BpmUserTaskActivityBehavior 使用它 | |
* | |
* @author Tz | |
* @date 2025/10/25 15:26 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Setter | |
public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehavior { | |
private BpmTaskCandidateInvoker taskCandidateInvoker; | |
public BpmParallelMultiInstanceBehavior(Activity activity, | |
AbstractBpmnActivityBehavior innerActivityBehavior) { | |
super(activity, innerActivityBehavior); | |
} | |
/** | |
* 重写该方法,主要实现两个功能: | |
* 1. 忽略原有的 collectionVariable、collectionElementVariable 表达式,而是采用自己定义的 | |
* 2. 获得任务的处理人,并设置到 collectionVariable 中,用于 BpmUserTaskActivityBehavior 从中可以获取任务的处理人 | |
* | |
* 注意,多个任务实例,每个任务实例对应一个处理人,所以返回的数量就是任务处理人的数量 | |
* | |
* @param execution 执行任务 | |
* @return 数量 | |
*/ | |
@Override | |
protected int resolveNrOfInstances(DelegateExecution execution) { | |
// 情况一:UserTask 节点 | |
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); | |
} | |
execution.setVariableLocal(super.collectionVariable, assigneeUserIds); | |
} | |
return assigneeUserIds.size(); | |
} | |
// 情况二:CallActivity 节点 | |
if (execution.getCurrentFlowElement() instanceof CallActivity) { | |
FlowElement flowElement = execution.getCurrentFlowElement(); | |
Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement); | |
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { | |
return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class); | |
} | |
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { | |
return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size(); | |
} | |
} | |
return super.resolveNrOfInstances(execution); | |
} | |
} |
② BpmUserTaskActivityBehavior,判断是多实例的情况,则复用 BpmParallelMultiInstanceBehavior “计算” 结果,直接设置审批人。如下所示:
package cn.tzzfj.scaffold.module.bpm.framework.flowable.core.behavior; | |
/** | |
* <p> Project: scaffold - BpmUserTaskActivityBehavior </p> | |
* | |
* 自定义的【单个】流程任务的 assignee 负责人的分配 | |
* | |
* 第一步,基于分配规则,计算出分配任务的【单个】候选人。如果找不到,则直接报业务异常,不继续执行后续的流程; | |
* | |
* 第二步,随机选择一个候选人,则选择作为 assignee 负责人。 | |
* | |
* @author Tz | |
* @date 2025/10/25 15:26 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Slf4j | |
public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior { | |
@Setter | |
private BpmTaskCandidateInvoker taskCandidateInvoker; | |
public BpmUserTaskActivityBehavior(UserTask userTask) { | |
super(userTask); | |
} | |
@Override | |
@Transactional(rollbackFor = Exception.class) | |
protected void handleAssignments(TaskService taskService, String assignee, String owner, | |
List<String> candidateUsers, List<String> candidateGroups, TaskEntity task, ExpressionManager expressionManager, | |
DelegateExecution execution, ProcessEngineConfigurationImpl processEngineConfiguration) { | |
// 第一步,获得任务的候选用户 | |
Long assigneeUserId = calculateTaskCandidateUsers(execution); | |
// 第二步,设置作为负责人 | |
if (assigneeUserId != null) { | |
TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId)); | |
} | |
} | |
private Long calculateTaskCandidateUsers(DelegateExecution execution) { | |
// 情况一,如果是多实例的任务,例如说会签、或签等情况,则从 Variable 中获取。 | |
// 顺序审批可见 BpmSequentialMultiInstanceBehavior,并发审批可见 BpmSequentialMultiInstanceBehavior | |
if (super.multiInstanceActivityBehavior != null) { | |
return execution.getVariable(super.multiInstanceActivityBehavior.getCollectionElementVariable(), Long.class); | |
} | |
// 情况二,如果非多实例的任务,则计算任务处理人 | |
// 第一步,先计算可处理该任务的处理人们 | |
Set<Long> candidateUserIds = taskCandidateInvoker.calculateUsersByTask(execution); | |
if (CollUtil.isEmpty(candidateUserIds)) { | |
return null; | |
} | |
// 第二步,后随机选择一个任务的处理人 | |
// 疑问:为什么一定要选择一个任务处理人? | |
// 解答:项目对 bpm 的任务是责任到人,所以每个任务有且仅有一个处理人。 | |
// 如果希望一个任务可以同时被多个人处理,可以考虑使用 BpmParallelMultiInstanceBehavior 实现的会签 or 或签。 | |
int index = RandomUtil.randomInt(candidateUserIds.size()); | |
return CollUtil.get(candidateUserIds, index); | |
} | |
@Override | |
protected void handleCategory(CreateUserTaskBeforeContext beforeContext, ExpressionManager expressionManager, | |
TaskEntity task, DelegateExecution execution) { | |
ProcessDefinitionEntity processDefinitionEntity = CommandContextUtil.getProcessDefinitionEntityManager().findById(execution.getProcessDefinitionId()); | |
if (processDefinitionEntity == null) { | |
log.warn("[handleCategory][任务编号({}) 找不到流程定义({})]", task.getId(), execution.getProcessDefinitionId()); | |
return; | |
} | |
task.setCategory(processDefinitionEntity.getCategory()); | |
} | |
} |
所以,先是 BpmParallelMultiInstanceBehavior 计算审批任务数量 + 审批人列表,然后 BpmUserTaskActivityBehavior 直接设置审批人。
# 顺序 BpmSequentialMultiInstanceBehavior
① BpmSequentialMultiInstanceBehavior 实现 Flowable SequentialMultiInstanceBehavior 类,实现单节点多任务的审批人 “计算”。如下所示:
package cn.tzzfj.scaffold.module.bpm.framework.flowable.core.behavior; | |
/** | |
* <p> Project: scaffold - BpmSequentialMultiInstanceBehavior </p> | |
* | |
* 自定义的【串行】的【多个】流程任务的 assignee 负责人的分配 | |
* | |
* 本质上,实现和 {@link BpmParallelMultiInstanceBehavior} 一样,只是继承的类不一样 | |
* | |
* @author Tz | |
* @date 2025/10/25 15:26 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Setter | |
public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceBehavior { | |
private BpmTaskCandidateInvoker taskCandidateInvoker; | |
public BpmSequentialMultiInstanceBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) { | |
super(activity, innerActivityBehavior); | |
} | |
/** | |
* 逻辑和 {@link BpmParallelMultiInstanceBehavior#resolveNrOfInstances (DelegateExecution)} 类似 | |
* | |
* 差异的点:是在【第二步】的时候,需要返回 LinkedHashSet 集合!因为它需要有序! | |
*/ | |
@Override | |
protected int resolveNrOfInstances(DelegateExecution execution) { | |
// 情况一:UserTask 节点 | |
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.setVariableLocal(super.collectionVariable, assigneeUserIds); | |
} | |
return assigneeUserIds.size(); | |
} | |
// 情况二:CallActivity 节点 | |
if (execution.getCurrentFlowElement() instanceof CallActivity) { | |
FlowElement flowElement = execution.getCurrentFlowElement(); | |
Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement); | |
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { | |
return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class); | |
} | |
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { | |
return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size(); | |
} | |
} | |
return super.resolveNrOfInstances(execution); | |
} | |
@Override | |
protected void executeOriginalBehavior(DelegateExecution execution, ExecutionEntity multiInstanceRootExecution, int loopCounter) { | |
// 参见 https://t.zsxq.com/53Meo 情况 | |
if (execution.getCurrentFlowElement() instanceof CallActivity | |
|| execution.getCurrentFlowElement() instanceof SubProcess) { | |
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter); | |
return; | |
} | |
// 参见 https://gitee.com/zhijiantianya/scaffold-cloud/issues/IC239F | |
super.collectionExpression = null; | |
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); | |
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); | |
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter); | |
} | |
} |
② 还是使用 BpmUserTaskActivityBehavior,逻辑是一模一样的。
所以,BpmSequentialMultiInstanceBehavior 和 BpmParallelMultiInstanceBehavior 基本是一致的,差异只是前者返回的是 LinkedHashSet 有序集合。最终,还是交给 Flowable 到底是一次性创建多个审批任务,还是按照顺序创建多个审批任务。