运行时不是“把 CurrentNodeId 改成下一个节点”。它同时维护实例版本、Execution Token、任务、候选人、FlowState、BusinessStatus、轨迹、Timer、缓存和外部回调。所有业务调用都应从聚焦端口或 Platform Command 进入。
1. 运行时端口
| 端口 | 用例 |
|---|---|
| IWorkflowTransitionFlow | Start、Complete、Reject、Withdraw、Resubmit |
| IWorkflowTaskHandoffFlow | Transfer、Delegate、AddParticipant |
| IWorkflowMigrationFlow | ResetToNode、StartAtNode、TransferApplicant |
| IWorkflowBatchOperation | Terminate、Cancel、Suspend、Resume、Remind、BatchComplete、Priority |
| IWorkflowQuery | ProgressView |
IRuntimeService 仍组合全部方法,但已标为兼容门面。
2. 发起实例
// 操作者与租户都来自服务端认证上下文。Result<IProcessInstance> started = await transitions.StartAsync( definitionKey: "matter-intake-approval", businessKey: matter.MatterNo, businessType: "MatterIntake", starterId: currentUserId, tenantId: currentTenantId, officeId: matter.OfficeId, variables: new Dictionary<string, object?> { // 只传流程路由所需的最小快照,不传完整业务对象。 ["disputeAmount"] = matter.DisputeAmount, ["crossBorder"] = matter.IsCrossBorder }, cancellationToken);当前 Start 接口没有 IdempotencyKey。虽然 Store 提供按 definitionKey+businessKey 查询实例,但 Start 流程没有在创建前显式检查;数据库是否有唯一约束必须另行证明。客户端超时后应先查业务关联,不能直接重试。
3. 完成任务
Complete 先加载 Pending Task,通过 Operate 数据权限,再加载实例与锁定定义。事务中推进实例 Version、完成任务、处理多实例、结束当前节点、移动 Token、更新 FlowState 和轨迹。提交后失效相关待办缓存。
// 完成任务前由引擎再次校验 Pending 状态与 Operate 权限。Result completed = await transitions.CompleteTaskAsync( taskId, currentUserId, comment: "利冲核查通过,同意立案", variables: new Dictionary<string, object?> { ["conflictCleared"] = true, ["engagementLetterSent"] = false }, cancellationToken);
if (completed.IsFailure){ // TaskNotPending 可能表示另一请求已完成;ConcurrencyConflict 表示版本竞争。 return MapWorkflowFailure(completed.Error);}当前 HTTP Request 不带 task version 或幂等键。乐观锁保护数据库,但调用者无法获得同一请求的确定性重放结果。
4. 驳回、撤回与重提
Reject 可显式 targetNodeId,也可按 RollbackRule 解析目标;它取消/重建相关执行与任务并写轨迹。Withdraw 只允许当前申请人语义,Resubmit 在撤回/退回状态恢复流程。
申请人可能通过 IApplicantResolver 动态解析,失败时回退 CurrentApplicantId/StarterId 快照。TransferApplicant 更新当前申请人,后续撤回和通知使用新身份。
5. 转办、委派与加签
Transfer 永久改变任务责任;Delegate 表示临时委派;AddParticipant 增加审批人。这些操作在事务内先推进实例 Version,使纯任务写也参与同一并发边界。
// successorUserId 应先通过同租户目录与在职状态校验。Result transferred = await handoff.TransferTaskAsync( taskId, fromUserId: currentUserId, toUserId: successorUserId, comment: "休假期间转交", cancellationToken);UserTaskConfig 声明 AllowTransfer/AllowDelegate/AllowAddParticipant,但当前运行方法是否强制读取这些字段必须逐项核验;不能只靠前端隐藏按钮。
6. 控制操作
Suspend/Resume、Terminate/Cancel、Reset、Priority 都要求 Manage 数据权限并使用实例 Version。Terminate 与 Cancel 都是终态,但业务含义不同:Terminate 是管理者强制结束,Cancel 是业务取消。
BatchComplete 是特殊管理操作:它原子地把一组 Pending Task 标为 Completed,并推进相应实例 Version,却明确不触发流程推进。不要把它用作普通批量审批,否则实例仍停在原节点。
Remind 解析当前 Pending Assignee 并分发 TaskRemind;通知失败会随命令一起失败回滚(见第 8 节),催办不再可能”悄悄丢失”。
7. 多实例与网关
并行多实例为每位参与人创建任务,CompletionCondition 决定何时收口;顺序多实例逐个创建。ParallelGateway 依赖父子 Execution 判断汇聚,InclusiveGateway 选择满足条件的分支。
必须覆盖:
- 两个审批人同时提交最后一票;
- CompletionCondition 恰好跨阈值;
- 兄弟任务取消与缓存失效;
- 并行分支变量合并;
- 嵌套并行的 parentId 范围;
- 包容网关零条件命中与默认边。
8. 事务与外部副作用
多表写由 IPersistenceStore.BeginTransactionAsync 包裹;引擎把 DB 并发异常规范化。通知与监听器已并入失败链条:平台 INotificationChannel 逐接收人写站内信与 CAP Outbox,任一失败计数后抛出;NotificationDispatcher 记录日志后重抛;EventDispatcher 同样向上传播监听器异常——整个流转命令因此失败回滚。审批成功与”通知事实已落库”现在是同一个命题。
IBusinessIntegrationCallback 由运行时直接调用,并非持久 Outbox。不同用例的调用点可能在事务之后;回调失败可能让请求失败或留下状态已提交的窗口。业务状态同步应具备幂等、重放和对账。
9. 定时器
进入带 Timers 的节点后调度 TimerJob;JobHost 周期扫描 Pending。当前处理顺序是先 Save Fired,再 Dispatch:
var fired = job with { Status = "Fired" };await store.SaveTimerJobAsync(fired, cancellationToken);
// 这里进程崩溃或动作失败,记录不会自动回到 Pending。await DispatchJobAsync(job, cancellationToken);扫描也没有原子 Claim;多实例 JobHost 可能同时读到同一 Pending。必须新增状态 CAS/lease、attempt、nextRetryAt、lastError、死信与恢复 Runbook。
10. 错误处理
Result 错误映射到 ProblemDetails。调用方应区分 NotFound、Forbidden、InvalidState、TaskNotPending、ConcurrencyConflict 和基础设施异常。不要把所有 409 自动重试;并发冲突通常要求重新读取用户意图。
11. 测试清单
- Start 的业务键重复与客户端断线重试;
- 双 ORM 的原子多表写和 rollback;
- 完成/驳回/终止竞争;
- 当前用户、候选人与 Manage 权限;
- 通知/回调失败时数据库事实;
- Timer 重复扫描、崩溃、未知类型、动作失败;
- 多实例和并行网关;
- 缓存失效及多 API 节点一致性。