4、事件驱动架构:审批流中的事件类型
在基于状态机的审批流系统中,事件是驱动状态跃迁的唯一动力。事件驱动架构将审批流的每一次操作抽象为不可变的事件记录,使系统具备异步解耦、可追溯、可重放的核心能力。本章详细定义审批流中的五种标准事件类型:提交事件、审批事件、驳回事件、撤回事件和超时事件,并阐述各自的结构、触发条件与状态影响。
4.1 事件模型总览
每个事件由以下核心字段构成:
| 字段 | 类型 | 说明 |
|---|---|---|
| eventId | UUID | 全局唯一事件标识 |
| eventType | 枚举 | SUBMIT / APPROVE / REJECT / WITHDRAW / TIMEOUT |
| processInstanceId | String | 所属流程实例ID |
| operatorId | String | 操作人标识(系统事件可为空) |
| timestamp | Long | 事件发生时间戳(毫秒) |
| payload | JSON | 事件载荷,包含业务数据与上下文 |
| sourceState | String | 事件发生前的状态机状态 |
| targetState | String | 事件处理后的目标状态 |
设计要点:事件一旦写入事件存储(Event Store),即视为不可变记录。即使后续发生异常回滚,也不应物理删除事件,而是通过补偿事件(Compensating Event)标记状态变更。
4.2 五种事件类型详解
4.2.1 提交事件(SUBMIT)
触发场景:发起人完成表单填写,将审批单推入流程引擎。
- 前置状态:DRAFT(草稿)
- 后置状态:PENDING_APPROVAL(待审批)
- 事件载荷:表单数据、附件列表、发起人信息、优先级
- 异常场景:表单校验失败、重复提交、流程定义不存在
4.2.2 审批事件(APPROVE)
触发场景:当前审批节点操作人点击“同意”。
- 前置状态:PENDING_APPROVAL
- 后置状态:APPROVED(已通过)或流转至下一审批节点
- 事件载荷:审批意见、审批人ID、审批时间、附加签名
- 异常场景:审批人无权限、节点已过期、并行分支未全部到达
4.2.3 驳回事件(REJECT)
触发场景:当前审批节点操作人点击“驳回”。
- 前置状态:PENDING_APPROVAL
- 后置状态:REJECTED(已驳回)或退回至指定前置节点
- 事件载荷:驳回原因、驳回类型(退回发起人/退回上一节点)、是否允许重新提交
- 异常场景:驳回目标节点不存在、驳回后状态机无法回退
4.2.4 撤回事件(WITHDRAW)
触发场景:发起人在审批人尚未处理时主动撤回申请。
- 前置状态:PENDING_APPROVAL
- 后置状态:WITHDRAWN(已撤回)
- 事件载荷:撤回原因、撤回时间
- 约束条件:仅允许在审批人未读取/未操作时撤回;若审批人已处理,则撤回事件应被拒绝
- 异常场景:撤回窗口已关闭、流程已进入终态
4.2.5 超时事件(TIMEOUT)
触发场景:系统定时器检测到审批节点超过规定时限未处理。
- 前置状态:PENDING_APPROVAL
- 后置状态:TIMED_OUT(超时)或自动流转至指定策略(如自动通过/转交他人)
- 事件载荷:超时节点ID、超时阈值、自动执行策略
- 异常场景:定时器重复触发、超时事件与人工事件并发冲突
并发冲突风险:超时事件与审批事件可能在同一毫秒内发生。系统必须通过事件版本号或乐观锁机制确保只有一个事件生效,避免状态机进入歧义状态。
4.3 事件驱动状态跃迁图
下图展示了五种事件如何驱动审批流状态机在核心状态之间跃迁:
4.4 事件处理流程与异常恢复
事件驱动架构下的审批流处理遵循“事件溯源 + 状态机”模式。每个事件被持久化后,状态机根据事件类型和当前状态计算下一个状态。当系统发生故障时,可通过重放事件日志恢复状态机至最新一致状态。
4.5 事件类型对比表
| 事件类型 | 触发角色 | 前置状态 | 后置状态 | 是否可补偿 | 典型异常 |
|---|---|---|---|---|---|
| SUBMIT | 发起人 | DRAFT | PENDING_APPROVAL | 否(可撤回) | 表单校验失败 |
| APPROVE | 审批人 | PENDING_APPROVAL | APPROVED / 下一节点 | 否 | 权限不足 |
| REJECT | 审批人 | PENDING_APPROVAL | REJECTED / 退回 | 否 | 退回目标不存在 |
| WITHDRAW | 发起人 | PENDING_APPROVAL | WITHDRAWN | 否 | 撤回窗口已关闭 |
| TIMEOUT | 系统定时器 | PENDING_APPROVAL | TIMED_OUT / 自动流转 | 否 | 定时器重复触发 |
关键原则:所有事件均为“一次性写入、多次读取”。事件存储应支持按流程实例ID和时间戳范围的高效查询,以支撑状态机重放和审计追溯。
4.6 事件幂等性设计
在分布式环境中,事件可能因网络重试、消息队列重复投递等原因被多次消费。状态机引擎必须实现事件幂等性:
- 事件去重:基于 eventId 进行唯一性校验,已处理的事件直接返回成功。
- 状态校验:处理事件前检查当前状态是否与事件的 sourceState 匹配,不匹配则拒绝。
- 版本号控制:每个流程实例维护一个递增版本号,事件处理时通过乐观锁更新版本号,防止并发覆盖。
// 事件幂等处理伪代码
public EventResult handleEvent(ApprovalEvent event) {
// 1. 去重检查
if (eventStore.exists(event.getEventId())) {
return EventResult.duplicate();
}
// 2. 获取当前流程实例状态
ProcessInstance instance = processRepository.findById(event.getProcessInstanceId());
// 3. 状态校验
if (!instance.getState().equals(event.getSourceState())) {
return EventResult.stateMismatch();
}
// 4. 版本号乐观锁
int updated = processRepository.updateStateWithVersion(
event.getProcessInstanceId(),
event.getTargetState(),
instance.getVersion(),
instance.getVersion() + 1
);
if (updated == 0) {
return EventResult.concurrencyConflict();
}
// 5. 持久化事件
eventStore.save(event);
return EventResult.success();
}
生产建议:在事件存储中为 eventId 建立唯一索引,并将事件持久化与状态更新放在同一个本地事务中,避免“事件已存但状态未更新”的不一致问题。