Flowable会签实现:多实例任务的底层原理

Flowable 会签的技术基础是 BPMN 多实例活动:引擎进入节点时解析人员集合,为集合中的每个元素创建一个活动实例,并用多实例根执行统一维护总数、活动数和完成数。并行会签一次创建多个子执行,顺序会签始终只保持一个活动实例;每完成一个任务,引擎都会更新计数、判断 completionCondition,满足条件后结束整个多实例活动。

但"多实例任务"不等于"完整会签"。Flowable 只负责重复执行、运行时任务和结束条件,赞成票、反对票、弃权、权重、动态加签、审批意见和责任证据仍应由审批领域服务定义。理解这一边界,才能避免把 nrOfCompletedInstances 错当成赞成票数,也能解释为什么并发会签最容易在计票、监听器和动态参与者上出错。

一、核心结论与问题边界

一句话理解 Flowable 多实例任务

多实例活动可以理解为工作流引擎里的 for each:对人员集合中的每个元素执行一次 User Task。它可以并行执行,也可以顺序执行;可以等待全部实例完成,也可以用 completionCondition 提前结束。

概念 Flowable 负责什么 会签平台还要负责什么
人员集合 读取 collection 并逐项绑定 组织解析、去重、回避和人员快照
执行方式 并行或顺序创建活动实例 会签类型、轮次和前后加签语义
完成判断 计算实例完成数并执行表达式 赞成、反对、弃权、权重和一票否决
运行时任务 创建、完成和删除 User Task 意见、签名、附件、责任链和通知
历史 保存任务与活动历史 可解释的审批记录和合规证据

因此,多实例是会签的执行容器,不是会签的业务规则引擎。

源码里的核心设计:外层行为包装内层任务

Flowable 当前源码中的 MultiInstanceActivityBehavior 是一个抽象行为,直接子类是 ParallelMultiInstanceBehavior 和 SequentialMultiInstanceBehavior。它并不重新实现 User Task,而是包装原活动的 ActivityBehavior:外层负责循环、执行树、计数和结束条件,内层仍按普通用户任务逻辑创建待办、等待用户完成。

这个设计带来两个重要结论:

  • 多实例特性可以附加到 User Task、Service Task、SubProcess、Call Activity 等活动上;
  • 每个实例仍是一次真实活动执行,不是一张任务表里简单复制几行数据。

官方文档也明确说明,Gateway 和 Event 不能直接变成多实例。原因是多实例针对"可执行活动"进行重复,而网关负责路由、事件负责捕获或抛出流程事件。

一张图看懂多实例执行树

图 1:多实例根执行保存整体计数,子执行保存 loopCounter 和当前集合元素;并行模式下多个子任务同时存在。

流程令牌到达会签节点后,会形成一个多实例根执行。nrOfInstances、nrOfActiveInstances 和 nrOfCompletedInstances 属于整体循环语义;每个子执行拥有自己的 loopCounter,以及由 elementVariable 指定的当前人员,例如 assignee。

并行三人会签通常会看到一个多实例根执行、三个子执行和三个运行时任务。任务完成后,运行时任务记录消失,活动及任务历史进入历史数据;只有整个多实例满足结束条件,令牌才会离开会签节点。

二、关键概念与能力差异

BPMN 配置怎样变成运行时实例

多实例 User Task 的核心配置可以归纳为下面几项:

配置 典型值 运行时含义
isSequential false 或 true false 并行,true 顺序
collection assigneeList 参与者集合或返回集合的表达式
elementVariable assignee 当前集合元素的局部变量名
assignee ${assignee} 用当前元素分配每个任务
indexVariable loopCounter 当前实例下标,可自定义变量名
completionCondition 布尔表达式 每个实例结束后判断是否结束整体活动

进入活动时,引擎解析 loopCardinality 或 collection。官方文档说明,实例数量在进入活动时计算一次;如果 collection 是 assigneeList,初始集合有三人,nrOfInstances 就从三开始。后续修改普通流程变量里的列表,并不会自动生成第四个执行实例。

这也是"修改人员列表"和"动态加签"不能混为一谈的原因。真正的运行中加签需要调用受支持的多实例运行时 API,或通过审批领域服务建立新的会签轮次。

并行会签和顺序会签的底层差异

并行多实例会先创建全部子执行,再逐个调度原活动行为。Flowable 源码特别把"创建全部执行"和"执行原行为"分成两个循环,避免无等待活动执行过快时对子执行进行裁剪,造成集合尚未完整创建就被提前结束。

顺序多实例只创建一个子执行。当前实例完成后,引擎增加 loopCounter 和 nrOfCompletedInstances,清理上一实例的局部变量,再让同一个执行进入下一轮。对普通 User Task 来说,表现为上一人的任务完成后才创建下一人的任务。

对比项 并行多实例 顺序多实例
初始子执行 按集合大小创建多个 只创建一个
活动任务数 多个任务可同时存在 同一时刻通常只有一个
nrOfActiveInstances 随完成逐步减少 通常保持为 1
办理顺序 无天然先后 按集合迭代顺序
主要风险 并发更新、提前结束、剩余任务清理 人员顺序、跳过规则、单点阻塞

如果业务要求"同一批人全部可见、同时表态",使用并行;如果要求"部门经理处理后才到分管领导",使用顺序。不要用任务创建时间碰巧先后来模拟顺序规则。

三、模型架构与运行机制

四个变量分别代表什么

Flowable 多实例最常见的四个变量是:

  • nrOfInstances:进入多实例时确定的实例总数;
  • nrOfActiveInstances:当前仍处于活动状态的实例数;
  • nrOfCompletedInstances:已经完成的实例数;
  • loopCounter:当前子实例在集合中的下标,默认从 0 开始。

前三个变量描述整个多实例,loopCounter 描述单个实例。集合元素变量也应放在子执行局部作用域,否则并行实例会互相覆盖 assignee,最终可能出现多张任务指向同一个人。

最容易犯的错误是把 nrOfCompletedInstances 理解成"同意人数"。它只表示有多少实例结束。某个用户点击驳回并完成任务,完成数同样会增加。审批结果必须另存为结构化决定,再由会签策略统计 approvedCount、rejectedCount 和 abstainedCount。

一个任务完成后,引擎内部发生什么

图 2:TaskService 完成一个任务后,内层活动离开,外层多实例行为更新计数并判断是否继续或结束。

以并行会签为例,一次任务提交大致经历以下步骤:

  1. 业务服务校验办理人、任务版本和会签策略;
  2. 保存本人的结构化决定与审批意见;
  3. 调用 TaskService.complete 完成当前 User Task;
  4. User Task 的内层行为请求离开活动;
  5. ParallelMultiInstanceBehavior 使当前子执行失活并更新完成计数;
  6. 引擎执行活动结束监听器和变量聚合;
  7. 引擎计算 completionCondition;
  8. 条件未满足则继续等待,满足则结束多实例并沿顺序流前进。

Flowable 当前并行实现还会锁定首个父 scope,避免多个子实例同时完成时都认为自己是"最后一个"。当前主分支对并行循环计数采用专门的变量类型,同时保留对旧运行中实例的兼容逻辑。这说明并发正确性属于引擎内部实现细节,应用不应直接修改计数变量或 ACT_RU_EXECUTION。

四、核心场景与处理策略

completionCondition 应该怎样写

completionCondition 会在每个实例结束后求值,结果必须是布尔值。它适合表达"什么时候结束整个多实例",但表达式引用什么数据,决定了会签语义是否正确。

会签规则 推荐判断 不应使用的判断
全员处理 nrOfCompletedInstances 等于 nrOfInstances 只判断当前任务已完成
任一同意 approvedCount 大于等于 1 nrOfCompletedInstances 大于等于 1
60% 同意 approvedCount 乘 100 大于等于 nrOfInstances 乘 60 完成数除以总数大于等于 0.6
一票否决 rejectedCount 大于等于 1 任意任务完成即否决
过半且无否决 approvedCount 过半且 rejectedCount 为 0 只看赞成票,不处理否决票

计票变量必须并发安全。简单在监听器里读取 approvedCount、加一再写回,两个事务可能读到同一个旧值并覆盖彼此。更可靠的做法是保存一人一票的业务记录,以唯一键防止重复表态,再在同一锁或同一事务边界内计算聚合结果。

提前结束时,剩余任务去了哪里

completionCondition 一旦满足,Flowable 会结束多实例活动并清理仍在运行的子执行。对用户界面而言,其他人的运行时待办会消失;对审计而言,这些任务不能被误写成"已同意"或"正常完成"。

业务系统应为剩余参与者记录明确原因,例如:达到通过比例、触发一票否决、管理员终止或边界事件中断。历史任务的结束时间只能说明任务生命周期结束,不能单独证明办理人作出了什么决定。

提前结束还会影响催办、移动端缓存和待办索引。推荐在事务提交后发布"会签整体结束"事件,由投影服务撤销未办提醒、更新工作台并保留取消原因,而不是只依赖前端下一次刷新发现任务不见了。

五、数据、规则与状态设计

执行表、任务表和历史表怎样分工

多实例运行时通常会涉及 ACT_RU_EXECUTION、ACT_RU_TASK 和运行时变量;完成后,活动与任务轨迹进入历史表。理解表的职责有助于排查问题,但业务代码应优先使用 RuntimeService、TaskService 和 HistoryService 查询,不应直接更新引擎表。

  • ACT_RU_EXECUTION 体现流程实例、scope、多实例根和子执行关系;
  • ACT_RU_TASK 保存当前仍需人工处理的 User Task;
  • 运行时变量保存集合、计数、局部人员变量和业务上下文;
  • 历史活动记录可观察每个活动实例的开始、结束和删除原因;
  • 历史任务记录可观察办理人、任务生命周期和完成时间。

排查"三人会签为什么只有两张任务"时,应先看人员集合快照,再看执行树和局部 assignee,最后看是否有 completionCondition、边界事件或监听器提前结束。只查任务表容易把执行树问题误判成任务创建问题。

赞成、反对和弃权应该存在哪里

推荐把审批决定存入独立业务记录,而不是只写一个全局流程变量。每条记录至少包含 processInstanceId、taskId、activityId、roundNo、userId、decision、comment、operationId 和完成时间,并对 taskId 或"轮次加人员"建立唯一约束。

这样做有三个好处:

  • 审批决定与任务完成解耦,能够解释每一票;
  • 并行提交可以通过唯一键和事务锁避免重复计票;
  • 动态加签、撤回意见和多轮会签可以保留版本,而不是覆盖一个变量。

流程变量适合保存轻量、需要驱动流程的聚合结果,例如 approvedCount、rejectedCount 和 meetingResult;原始意见、附件、签名和人员快照应由业务库承担。

六、工程实现与系统集成

动态加签为什么比增加一个任务复杂

Flowable RuntimeService 提供 addMultiInstanceExecution 和 deleteMultiInstanceExecution 等公开能力,可以向正在运行的多实例父执行增加或删除实例。但 API 解决的是执行树变更,不会替企业定义会签规则。

运行中加签至少要决定:

  • 新人员是否计入 nrOfInstances 和通过比例分母;
  • 当前已经满足的比例是否需要重新计算;
  • 新任务属于原轮次还是新轮次;
  • 被删除实例算弃权、取消还是管理员调整;
  • 加签人与被加签人的责任关系怎样记录;
  • 重复请求和并发完成同时发生时怎样保证幂等。

因此,加签入口必须由领域命令服务统一封装。不要让前端直接拿 parentExecutionId 调用引擎 API,更不要手工插入任务和执行记录。

监听器为什么经常执行两遍

多实例活动上的 execution listener 与普通活动不同。官方文档说明,进入多实例整体时会触发一次 start,此时 loopCounter 尚未设置;每个实际实例开始时还会再触发 start,此时 loopCounter 有值。end 事件同理:每个实例结束触发一次,整体结束还会再触发一次。

如果监听器不区分"整体事件"和"实例事件",就可能重复发消息、重复写审批记录或重复更新业务状态。判断时应结合 loopCounter、execution 是否为 multi-instance root、事件类型和 operationId,而不是看到 activityId 相同就认为是同一次回调。

Task listener 则面向每张具体用户任务,更适合记录任务创建、指派和完成事件;跨系统通知仍建议通过事务 Outbox 在提交后异步发送,避免监听器远程调用拖长引擎事务。

七、安全、性能与治理要求

边界事件和超时会怎样影响会签

多实例活动仍然是普通 BPMN 活动,因此可以挂载边界事件。官方文档说明,中断型边界事件被捕获时,所有仍活动的实例都会被销毁。例如会签节点设置三天超时,定时器触发后可以统一转入升级审批,而不是让每个人各自进入一条升级分支。

若业务要求"每个人分别超时",边界定时器应放在内部任务语义上并明确是否中断单个实例。两种模型的影响范围完全不同:一个中断整个会签,一个只处理当前参与者。

超时处理也必须保存原因。取消任务、自动同意、自动弃权和转交上级是四种不同的审批语义,不能只用任务已结束来表示。

一套可靠的会签领域架构

图 3:审批领域服务负责人员、投票和审计,Flowable 多实例负责执行树、任务和流程推进。

推荐把会签分成五个职责:

  1. Participant Resolver:按部门、岗位、逐级主管等规则生成并冻结人员快照;
  2. Vote Service:保存赞成、反对、弃权、权重和一票否决等结构化决定;
  3. Counter Policy:根据当前轮次与人员快照计算是否达到结束条件;
  4. Flowable Adapter:完成任务、增加或删除多实例执行,并隔离版本差异;
  5. Audit and Projection:生成审批时间线、待办投影、取消原因和通知事件。

Flowable 只接收经过校验的命令。所有 PC、移动端、开放 API 和管理员入口都经过同一个 operationId 幂等检查,避免同一人双击、重试或多端提交产生两次投票。

八、平台落地、测试与选型

低代码平台应该封装哪些配置

云程低代码开发平台可以在 BPMN.js 属性面板中把底层多实例配置包装成业务可理解的会签选项:并行或顺序、人员来源、全员或比例通过、一票否决、超时策略、是否允许加签、人员快照时机和剩余任务处理方式。

设计器发布时应把这些选项转换为版本化节点策略,并校验 collection、elementVariable、assignee 和 completionCondition 是否一致。运行时仍由统一会签服务完成计票和审计,避免每条流程复制监听器脚本。

相关推荐
玹外之音9 天前
别做"AI 翻译"了,做"AI 本地化":跨境电商 Listing 系统的工程实践
aigc·工作流引擎·aiops
梦想很大很大9 天前
Workrun 进度更新:从理念到解决具体问题
agent·workflow·工作流引擎
愚农搬码17 天前
BPMN.js 自定义工具栏、节点图标和右键菜单
工作流引擎
大龄码农有梦想18 天前
仿钉钉流程模型如何转换成 BPMN 2.0?
工作流引擎·流程引擎·bpmn.js·oa·流程审批·仿钉钉流程设计器·bpmn流程设计器
愚农搬码22 天前
Camunda 7 与 Camunda 8 不是升级关系:架构差异与选型边界
工作流引擎
天丁o1 个月前
我把一套 Flowable + Spring Boot + Vue 的 BPM 流程引擎整理开源了:从流程设计器到待办闭环
spring boot·vue·工作流引擎·flowable·bpm流程引擎·流程审批
昭阳1 个月前
我的AI工作流整理【个人向】
人工智能·工作流引擎
Yao8061 个月前
Warm-Flow工作流引擎入门:比Flowable轻量在哪
工作流引擎