Flowable 驳回到上一节点:普通节点、多实例和并行网关处理

“驳回到上一节点”听起来只有一个目标,真正落到 Flowable 运行时却可能对应三种完全不同的动作:移动一个 Execution、结束整个多实例活动,或者收拢并行网关产生的多条 Execution。若不先识别当前执行树结构,就把所有场景统一写成 moveActivityIdTo,普通流程可能正常,多实例会一次移动多个任务,并行网关则可能留下孤立令牌或制造重复路径。

本文以截至 2026 年 8 月 2 日最新正式版 Flowable 8.0.0 为基准,结合 ChangeActivityStateBuilder、AbstractDynamicStateManager,以及官方 ChangeStateTest、ChangeStateForMultiInstanceTest 和 ChangeStateForGatewaysTest,说明三类场景应该怎样定位上一节点、选择迁移 API、处理剩余任务并验证迁移结果。

一、核心结论与问题边界

一句话回答:先识别执行结构,再决定移动一个还是一组 Execution

可以先记住三个默认策略:

  • 普通单实例任务:取当前活动 task 的 executionId,精确移动到目标 activityId;
  • 多实例任务:先定义“驳回一个实例”还是“结束整组会签”,再决定移动单个 Execution 或整个多实例活动;
  • 并行网关分支:分支内回退只移动本分支,整体退回网关之前则必须收拢全部相关 Execution。

因此,“上一节点”不是一个纯历史查询问题,而是“目标节点解析 + 当前执行树分类 + 作用域迁移策略”的组合问题。

截至 2026 年 8 月,最新 Flowable 代码提供了哪些能力

Flowable 8.0.0 发布于 2026 年 2 月 27 日。与本文直接相关的一项修复是“将并行多实例活动移动到单一活动时的问题”,这再次说明多实例与多 Execution 合并是状态迁移的核心边界,而不是普通 taskId 更新。

ChangeActivityStateBuilder 暴露的主要迁移方法包括:

API 运行时含义 典型用途
moveExecutionToActivityId 一个 Execution 移到一个活动 普通任务、精确分支回退
moveActivityIdTo 当前 activityId 对应活动迁到目标 单活动回退、整组多实例迁移
moveExecutionsToSingleActivityId 多个 Execution 收拢到一个活动 并行分支整体退回或越过汇聚
moveSingleExecutionToActivityIds 一个 Execution 拆到多个活动 明确需要重新建立多条路径
moveActivityIdsToSingleActivityId 多个 activityId 收拢到一个活动 已知多个活动节点整体迁移

ChangeActivityStateBuilder 只是通用状态改变接口。Flowable 不知道“上一审批节点”的业务定义,也不会自动决定会签剩余任务和并行分支应该怎样处理。

“上一节点”至少有四种业务口径

同一个按钮可能被不同产品理解为:

  1. BPMN 模型中当前节点的直接前驱;
  2. 当前流程实例实际经过的上一用户任务;
  3. 当前审批人在本分支看到的上一审批人;
  4. 允许选择的任意已办节点。

直接前驱可以从 BPMN incoming Sequence Flow 解析,但排他网关、循环和条件路径下,它未必实际执行过。上一已办节点可以从历史轨迹寻找,却必须排除网关、服务任务、迁移删除任务和其他并行分支。上一审批人还要结合轮次、会签参与人和业务操作记录。

推荐把“驳回上一节点”定义为:当前运行分支内、同一允许作用域中,最近一次正常到达且具备驳回资格的用户任务节点。若产品要支持“模型直接前驱”或“任意已办节点”,应拆成不同操作入口。

二、关键概念与能力差异

源码为什么证明 activityId 可能对应多个 Execution

Flowable 8.0.0 的 AbstractDynamicStateManager.resolveActiveExecutions 会查询流程实例全部子 Execution,再筛选 currentActivityId 等于指定 activityId 的记录。也就是说,按 activityId 迁移不是天然只命中一个运行实例。

源码随后检查 Execution 是否为 multiInstanceRoot,或是否处于多实例内部;如果是,会按父 Execution 分组构造迁移容器。若不在多实例内,才从匹配结果中取一个活动 Execution。由此可以得到一个重要结论:

  • 普通节点中 activityId 通常只有一个活动 Execution;
  • 并行多实例中,同一个 activityId 会对应多个任务与子 Execution;
  • 嵌套多实例、子流程或网关时,还要按父作用域分组;
  • 要精确控制某一任务路径,应优先以当前 task.executionId 定位。

这不是 API 命名差异,而是迁移范围的差异。

在这里插入图片描述

图 1:先检查运行时结构,再决定移动单个 Execution、整个多实例活动或并行分支集合。

普通节点:最稳妥的是按当前 executionId 精确回退

顺序流程 A 申请 → B 部门经理 → C 财务经理,当前 C 需要驳回到 B。普通单实例场景通常只有一个活动任务和一个叶子 Execution,可以使用:

  • runtimeService.createChangeActivityStateBuilder()
  • .processInstanceId(processInstanceId)
  • .moveExecutionToActivityId(currentTask.getExecutionId(), "taskB")
  • .changeState()

Flowable 官方 ChangeStateTest 同时验证了 moveActivityIdTo("secondTask", "firstTask") 和 moveExecutionToActivityId(task.getExecutionId(), "firstTask")。迁移后当前活动触发 ACTIVITY_CANCELLED,目标活动触发 ACTIVITY_STARTED,查询到的新任务 taskDefinitionKey 变为 firstTask。

企业实现中更建议保留 processInstanceId 参与业务校验,并以 taskId 查询出的当前 executionId 作为精确迁移对象。驳回完成后,B 上会创建新的任务实例,不会复活此前完成的 B1。

普通节点还要处理边界定时器和事件订阅

如果当前 User Task 挂有 Boundary Timer,迁移离开该节点时不仅要结束任务,还要取消对应 Timer Job;重新进入带定时器的目标节点时,又要创建新的定时作业。Flowable 官方测试明确断言了 JOB_CANCELED、ACTIVITY_CANCELLED、ACTIVITY_STARTED 和 TIMER_SCHEDULED 等事件顺序。

因此不要直接修改 ACT_RU_EXECUTION.ACT_ID_。Change State 的价值不仅是改变活动位置,还包括删除关联任务、作业、事件订阅和子 Execution,再按目标 FlowElement 的行为重新启动。

普通节点迁移后至少验证:当前活动任务只有一条;旧 taskId 已结束;目标 taskId 为新值;目标节点边界定时器已重新调度;业务单据状态与新节点一致。

在这里插入图片描述

图 2:普通节点回退移动当前叶子 Execution,旧任务取消,目标节点产生新一轮任务及关联作业。

三、模型架构与运行机制

多实例节点:首先确认驳回发生在节点内还是节点之后

多实例有两个常见位置:

  • 当前正在会签,A、B、C 三个人各有一条任务,其中一人点击驳回;
  • 会签已经整体结束,后续节点需要驳回并重新启动上一会签节点。

前者必须决定剩余会签任务怎样处理;后者必须决定是否按原参与人快照重新创建整组任务。不能仅看到 taskDefinitionKey 相同,就把一条历史会签任务当成普通上一节点。

多实例 Activity Body 通常包含 multiInstanceRoot、多个子 Execution、nrOfInstances、nrOfActiveInstances、nrOfCompletedInstances 和 loopCounter 等状态。驳回策略必须维护这些计数和作用域关系。

会签进行中:驳回一个人和驳回整组不是一回事

假设并行会签节点有 task-A、task-B、task-C,B 点击“驳回上一节点”。平台通常有三种产品语义:

产品语义 推荐处理 风险提示
B 个人退出会签 精确处理 B 的 execution 完成条件和计票规则必须允许
一人驳回即整组退回 结束整个多实例作用域,再回到前置节点 其他任务统一取消并保留意见
先记录反对票,节点完成后决定 正常完成 B,按 completionCondition 收敛 不立即执行状态迁移

中国式 OA 最常见的是“一人驳回即整组退回”。这时不应只移动 B 的 execution,否则 A、C 仍处于活动状态,多实例父执行还在等待计数收敛。平台应把整个会签 activityId 作为迁移范围,或显式收集同一多实例父作用域下的全部活动 Execution,再进入前置节点。

如果产品允许“仅 B 回退”,必须说明 B 回退后是否仍属于原多实例、A/C 结束后谁负责汇聚,以及 B 再次回来如何计算 nrOfCompletedInstances。这通常不是标准会签的自然语义,测试成本很高。

四、核心场景与处理策略

从后续节点驳回到上一会签节点:会重新创建整组实例

流程为申请 → 并行会签 → 总经理,当前总经理驳回到会签节点。此时当前只有总经理的一个 Execution,目标却是多实例 Activity。迁移进入目标节点后,Flowable 会根据目标活动的 MultiInstanceLoopCharacteristics 创建多实例根和内部实例。

Flowable 官方多实例测试显示:从 beforeMultiInstance 移到 parallelTasks 后,会形成一个多实例根和三个并行 Execution,并产生三条 active task;从 parallelTasks1 移动到 parallelTasks2 时,同样会在目标多实例活动形成三个任务。

企业系统必须把参与人集合作为上一轮快照还是重新计算写清楚:

  • 原人重审:使用上一轮参与人快照,保证审计可解释;
  • 按当前组织重算:适用于人员变动,但要记录人员差异;
  • 指定部分人员重审:生成新的参与人集合并标记新轮次。

不要依赖历史 taskId 重建会签人。应在业务表保存 activityId、roundNo、participantSnapshot、decision 和 sourceTaskId。

Flowable 官方测试怎样迁出并行多实例

ChangeStateForMultiInstanceTest 中,一个并行多实例包含三个活动任务。完成其中一个后,仍有两个活动任务;随后调用 moveActivityIdTo("parallelTasks", "nextTask"),测试断言子 Execution 收缩为一个,且只剩 nextTask。

这个行为适合“结束整个会签并进入单一目标”的场景,也说明 activityId 级迁移处理的不是点击按钮的那一个 task,而是该活动作用域中的活动实例集合。Flowable 8.0.0 发布说明中对“并行多实例活动迁到单一活动”的修复,更值得在升级后做专项回归。

在这里插入图片描述

图 3:会签中的个人退回、整组退回和记票后收敛是三种不同产品语义。

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

并行网关:先区分分支内回退和整体退回

流程在并行网关后产生法务、财务两条分支。财务审批人点击驳回时,可以有两种完全不同的目标:

  • 退回财务分支内部的上一个节点:只移动财务 execution,法务分支继续;
  • 退回并行网关之前的申请节点:必须取消或收拢法务、财务以及可能等待在 Join 的全部相关 execution。

分支内回退可用 moveExecutionToActivityId 精确定位当前分支。整体退回则不能只移动财务 execution 到网关之前,否则法务令牌仍可能继续进入 Join,而申请人重提后网关又会产生新令牌,最终出现重复任务或错误汇聚。

整体退回网关之前:使用多 Execution 收拢语义

整体退回可采用以下处理顺序:

在这里插入图片描述

图 4:整体驳回先收拢并行作用域中的全部活动与等待 Execution,再回到 Fork 前的单一节点。

  1. 根据并发父作用域识别本次并行拆分产生的全部活动 Execution;
  2. 纳入仍在用户任务上的分支,以及已经等待在汇聚网关上的 Execution;
  3. 统一校验这些 Execution 属于同一并行作用域;
  4. 调用 moveExecutionsToSingleActivityId(executionIds, beforeForkActivityId);
  5. 断言迁移后只存在目标任务和预期数量的子 Execution。

Flowable 官方 ChangeStateForGatewaysTest 有两类直接证据:两条分支活动时,把两个 executionId 收拢到 taskAfter,最终只剩一个 Execution;把多个 Execution 移入 parallelJoin 时,引擎让每个传入 Execution 自然进入汇聚网关,然后继续到 taskAfter。

用于“整体向前越过分支”的测试,同样证明了“多分支整体退回到单一前置节点”应使用一组 Execution 的收拢语义。这是基于官方 API 和测试拓扑作出的工程推论,目标 activityId 需要替换为实际的网关前用户任务。

六、工程实现与系统集成

一条分支已经到 Join,整体驳回该怎样处理

最容易漏掉的状态是:法务已经完成,法务 Execution 正等待在 Parallel Join;财务仍有活动任务。此时待办查询只有财务一条,但执行树仍有两个分支 Execution。

整体驳回不能只查询 active Task 的 executionId。应同时查询并发作用域下的活动 Execution,把财务分支和 Join 等待分支一起纳入迁移集合。Flowable 官方网关测试也覆盖了“一条分支已在 synchronizing gateway,另一条分支仍活动”的组合,并将两条 Execution 一起移动到单一活动。

因此,运行时识别应以 Execution 树为主,待办列表只能作为用户任务入口,不能代表完整令牌集合。

为什么不能随便把一个 Execution 移到 Parallel Join

AbstractDynamicStateManager 源码特别说明:并行网关 Join 需要每个传入 Execution 自然进入网关,因为汇聚逻辑要检查 Execution 数量。当目标 FlowElement 是 Gateway 且迁入多个 Execution 时,源码会为其创建相应子 Execution,让它们分别进入网关。

若只把一条分支直接移到 Join,而其他必达分支已经被取消或根本不存在,Join 可能持续等待;若把网关前的一个普通 Execution 直接移到 Join,又可能绕过 Split 产生错误的到达数。

推荐原则是:

  • 分支内驳回,不跨越 Fork 和 Join;
  • 整体驳回网关之前,收拢完整并行作用域;
  • 移入 Join 时,显式提供所有应该到达的 Execution;
  • 从 Join 之后回到网关之前,可以移动当前单 Execution,随后让流程按正常 Sequence Flow 再次经过 Split。

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

三类场景的统一决策算法

平台可以在执行驳回前完成五步分类:

  1. 用 taskId 查询活动任务并取得 executionId、activityId、processInstanceId;
  2. 向上遍历 Execution 父链,识别 multiInstanceRoot、并发父作用域和子流程边界;
  3. 结合 BPMN 模型与业务轨迹解析可驳回目标;
  4. 根据迁移范围选择单 Execution、activityId 集合或 Execution 集合 API;
  5. 迁移后重新查询执行树、任务、作业和业务投影进行断言。
当前结构 默认范围 推荐 API
普通单任务 当前叶子 Execution moveExecutionToActivityId
并行会签整体驳回 多实例活动作用域 moveActivityIdTo 或明确的 Execution 集合
并行网关分支内回退 当前分支 Execution moveExecutionToActivityId
并行网关整体退回 同一并发作用域全部 Execution moveExecutionsToSingleActivityId

API 只是最后一步。真正决定正确性的,是前面三步对目标、作用域和执行集合的识别。

事务、幂等和业务状态怎样一起处理

驳回服务应在同一业务事务边界内完成:当前任务活动校验、操作权限校验、幂等键占位、审批操作记录、引擎状态迁移和业务状态更新。若引擎与业务服务跨库,使用 Outbox 与可重试事件保证最终一致。

迁移后不要只检查“出现目标待办”,还应断言:

  • 普通节点只有一个目标活动任务;
  • 整组会签驳回后没有遗留原会签活动任务;
  • 并行整体驳回后没有遗留分支或 Join Execution;
  • 目标任务使用新 taskId,并带有新轮次;
  • 旧任务结束原因、审批意见和业务操作类型可以对应;
  • 定时器、事件订阅和异步作业数量符合目标节点模型。

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

低代码平台应该怎样把复杂度挡在按钮之外

云程低代码开发平台这类工作流产品,可以在设计阶段把技术迁移约束转成业务配置:普通审批默认“退回上一已办节点”;会签节点必须配置“一人驳回整组退回”或“记票后结束”;并行网关必须配置“仅本分支”或“整体退回 Fork 前”。
在这里插入图片描述

发布校验器应检查跨作用域目标、Parallel Join 令牌完整性、多实例参与人快照和完成条件。运行时页面只展示模型与权限允许的目标,不能让前端任意提交 activityId。

这样业务人员看到的仍是一个清晰按钮,复杂的 Execution 集合计算、作用域迁移和迁移后断言由平台统一完成。

上线前必须覆盖的测试矩阵

  • 普通 C→B 回退,验证旧任务取消、新任务创建和事件顺序;
  • 带 Boundary Timer 的任务离开和重新进入,验证作业取消与重建;
  • 会签三人中一人驳回整组,验证其他两条任务全部结束;
  • 会签完成后从后续节点回到会签,验证参与人快照和新轮次;
  • 并行多实例完成一人后整体迁出,验证只剩单一目标任务;
  • 并行网关分支内回退,验证另一分支不受影响;
  • 两条分支都活动时整体退回 Fork 前,验证 Execution 收拢;
  • 一条分支已到 Join、一条仍活动时整体退回,验证等待令牌也被处理;
  • 从 Join 后节点退回 Fork 前,再次提交后验证分支只创建一次;
  • 重复驳回请求、任务并发完成和乐观锁冲突均保持幂等;
  • Flowable 8 升级后回归并行多实例迁到单一活动;
  • 迁移失败时业务状态、审批操作记录和待办投影可以恢复。
posted @ 2026-09-13 09:39  大龄码农有梦想  阅读(37)  评论(0)    收藏  举报