3. 审批流领域建模:典型审批流状态定义

审批流是业务系统中最高频、最核心的状态机场景之一。一个健壮的审批流状态模型,不仅要覆盖正常流转路径,还必须为异常恢复(如节点超时、审批人离职、系统宕机)预留明确的语义锚点。本节定义企业级审批流的六种标准状态,并给出状态间的合法跃迁规则。

3.1 六种核心状态定义

以下六种状态覆盖了从单据创建到最终归档的完整生命周期,每种状态都对应明确的业务含义和系统行为约束:

状态名称 状态码 业务含义 系统行为约束
待提交 DRAFT 单据已创建,但尚未正式发起审批流程 允许编辑、删除;不触发任何通知或超时计时器
审批中 PENDING 单据已提交,正在等待当前审批节点处理 只读(除审批人外);启动超时监控;记录操作日志
已通过 APPROVED 所有审批节点均已完成且结果为通过 触发后置动作(如数据落库、通知下游);不可撤回
已驳回 REJECTED 当前审批节点或终审节点做出驳回决定 根据策略可退回至指定节点或直接终结流程
已撤回 WITHDRAWN 提交人在审批过程中主动撤销申请 仅允许在“审批中”且尚未被处理时执行;撤回后回到“待提交”
已终止 TERMINATED 因异常(超时、审批人不可达、系统错误)强制结束流程 需记录终止原因;支持人工干预恢复或归档
领域建模要点: 在状态机设计中,“已终止”与“已驳回”的关键区别在于:驳回是业务决策,终止是系统级异常保护。终止状态必须携带 terminate_reason 和 terminate_timestamp 字段,用于后续的异常恢复审计。

3.2 状态跃迁图

下图展示了六种状态之间的合法跃迁路径。箭头方向表示状态转移方向,虚线表示仅在特定条件下允许的跃迁:

审批流六种状态跃迁图 审批流标准状态跃迁图 待提交 DRAFT 审批中 PENDING 已通过 APPROVED 已驳回 REJECTED 已撤回 WITHDRAWN 已终止 TERMINATED 提交 全部通过 驳回 撤回(未处理时) 重新编辑 重新提交(若允许) 超时/异常 归档超时 起始状态 流转中 成功终态 业务拒绝 主动撤销 异常终态

3.3 状态跃迁规则矩阵

为便于状态机编码实现,将跃迁规则整理为决策矩阵。行表示当前状态,列表示目标状态,✓ 表示合法跃迁,✗ 表示非法跃迁:

当前状态 \ 目标状态 待提交 审批中 已通过 已驳回 已撤回 已终止
待提交 — ✓ ✗ ✗ ✗ ✗
审批中 ✗ — ✓ ✓ ✓ ✓
已通过 ✗ ✗ — ✗ ✗ ✗
已驳回 ✓ ✗ ✗ — ✗ ✓
已撤回 ✓ ✗ ✗ ✗ — ✗
已终止 ✗ ✗ ✗ ✗ ✗ —
实现提示: 在代码中,建议将跃迁矩阵定义为 Map<State, Set<State>> 或二维布尔数组。每次状态变更前先查表,若跃迁非法则抛出 IllegalTransitionException,避免状态机进入未定义分支。

3.4 异常恢复视角的状态分类

从异常恢复策略的角度,六种状态可归为三类,每类对应不同的恢复处理模式:

  • 可恢复状态(Recoverable): 待提交、审批中。系统故障后,可通过重试、补偿或人工干预恢复到正常流转。
  • 业务终态(Business Final): 已通过、已驳回。流程已产生明确业务结论,通常不需要恢复,但需保证幂等性。
  • 异常终态(Abnormal Final): 已撤回、已终止。流程非正常结束,需记录详细原因,并支持管理员手动重置或归档。
异常恢复视角状态分类 可恢复 待提交 审批中 支持重试/补偿/人工干预 业务终态 已通过 已驳回 需保证幂等性 异常终态 已撤回 已终止 记录原因,支持重置

3.5 状态机代码骨架(Java 枚举实现)

以下代码片段展示了如何用枚举 + 跃迁矩阵实现审批流状态机。该设计可直接嵌入 Spring StateMachine 或自研状态机引擎:

public enum ApprovalState {
    DRAFT,
    PENDING,
    APPROVED,
    REJECTED,
    WITHDRAWN,
    TERMINATED;

    private static final Map<ApprovalState, Set<ApprovalState>> TRANSITIONS = new HashMap<>();

    static {
        TRANSITIONS.put(DRAFT, Set.of(PENDING));
        TRANSITIONS.put(PENDING, Set.of(APPROVED, REJECTED, WITHDRAWN, TERMINATED));
        TRANSITIONS.put(APPROVED, Collections.emptySet());
        TRANSITIONS.put(REJECTED, Set.of(DRAFT, TERMINATED));
        TRANSITIONS.put(WITHDRAWN, Set.of(DRAFT));
        TRANSITIONS.put(TERMINATED, Collections.emptySet());
    }

    public ApprovalState transition(ApprovalState target) {
        if (!TRANSITIONS.getOrDefault(this, Collections.emptySet()).contains(target)) {
            throw new IllegalStateException(
                String.format("非法状态跃迁: %s -> %s", this, target));
        }
        return target;
    }

    public boolean isRecoverable() {
        return this == DRAFT || this == PENDING;
    }

    public boolean isBusinessFinal() {
        return this == APPROVED || this == REJECTED;
    }

    public boolean isAbnormalFinal() {
        return this == WITHDRAWN || this == TERMINATED;
    }
}
避坑指南: 不要将“已驳回”直接视为终态。在实际业务中,驳回后通常允许修改后重新提交(回到待提交),因此跃迁矩阵中必须保留 REJECTED → DRAFT 的路径。如果业务不允许重新提交,则移除该跃迁即可,但不要混淆“驳回”与“终止”的语义。