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 事件驱动状态跃迁图

下图展示了五种事件如何驱动审批流状态机在核心状态之间跃迁:

审批流事件驱动状态跃迁图 DRAFT PENDING_APPROVAL APPROVED REJECTED WITHDRAWN TIMED_OUT SUBMIT APPROVE REJECT WITHDRAW TIMEOUT 图例 初始状态 终态(成功) 终态(失败)

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 建立唯一索引,并将事件持久化与状态更新放在同一个本地事务中,避免“事件已存但状态未更新”的不一致问题。