--- tags: [pattern/fsm, workflow-engine, order-lifecycle, state-machine-pattern] create time: 2026-08-08 15:00 update time: 2026-08-08 15:00 --- # 有限状态机在业务中的应用 ## 概述 FSM 在工程中最大的价值是将"散落的状态判断"收拢到"显式的状态定义"。本文讨论三种典型的业务场景:审批流、订单生命周期和资源交付,每种场景展示其状态图和关键注意事项。 ## 审批流 ### 典型流程 任务从提交到最终结果需经过多级审批,每个审批人可以批准或驳回。 ```mermaid stateDiagram-v2 [*] --> Draft Draft --> Submitted: 提交申请 Submitted --> ManagerReview: 主管审核 ManagerReview --> Approved: 批准 ManagerReview --> Rejected: 驳回 ManagerReview --> ManagerRevise: 修改后重提 Approved --> FinanceReview: 进入财务审核 FinanceReview --> FinalApproved: 财务批准 FinanceReview --> Rejected: 财务驳回 Rejected --> [*] FinalApproved --> [*] ManagerRevise --> Draft: 退回草稿 ``` ### 关键注意事项 | 注意点 | 说明 | |--------|------| | 权限校验 | 每个审批节点需要确认审批人是否有该角色的审批权限 | | 驳回的语义 | "驳回"可以是终止(回终态)也可以是退回上一步(回到前置状态),需要在模型中明确区分 | | 超期自动处理 | 审批超过 N 天未操作应触发超时事件,默认通过或升级给上级 | | 审计追踪 | 每次状态转换记录 `operator`、`timestamp`、`comment`,不可删除 | > [!TIP] > 面试常考点:如果审批链动态变化(比如 A 请假了由 B 代审),这不再是静态 FSM,而是需要引入**策略模式 + 规则引擎**。硬编码审批人在生产环境中是不可接受的。 ## 订单生命周期 ### 状态图 订单是最经典的 FSM 应用场景。合法转移必须严格受控,防止"跳状态"。 ```mermaid stateDiagram-v2 [*] --> PendingPayment PendingPayment --> Paid: 用户支付 PendingPayment --> Cancelled: 超时取消 Paid --> Shipped: 仓库发货 Paid --> Refunding: 申请退款 Shipped --> Completed: 用户确认收货 Shipped --> Refunding: 申请退款 Completed --> [*] Refunding --> Refunded: 退款完成 Refunding --> Paid: 退款拒绝 Cancelled --> [*] Refunded --> [*] ``` ### 非法转移示例 ``` Paid -> Cancelled ❌ 已支付的订单不能直接取消 Completed -> Shipped ❌ 已完成不能倒退回已发货 PendingPayment -> Shipped ❌ 未付款不能直接发货 ``` ### 代码示例 ```go func (m *OrderManager) Transition(ctx context.Context, orderID string, event string) error { // 1. 获取当前状态 order, _ := m.repo.FindByID(ctx, orderID) // 2. 查表确认转换是否合法 handler, ok := m.transitions[order.Status][event] if !ok { return fmt.Errorf("invalid transition: %s -> [%s]", order.Status, event) } // 3. 执行转换 if err := handler(ctx, order); err != nil { return err } return m.repo.Save(ctx, order) } ``` ## 资源交付八阶段(七牛云存储场景) 这是一个更复杂的 FSM 应用。假设我们正在实现一个对象存储服务,资源从创建到销毁经历以下八个阶段。 ```mermaid stateDiagram-v2 [*] --> BucketCreated BucketCreated --> FileUploaded: 上传文件 FileUploaded --> CDNPrefetch: CDN预热 CDNPrefetch --> AccessAuthorized: 设置访问策略 AccessAuthorized --> TrafficTracked: 采集流量统计 TrafficTracked --> LifecycleConfigured: 配置生命周期 LifecycleConfigured --> CleanupPending: 满足清理条件 CleanupPending --> [*]: 物理删除 ``` ### 各阶段职责 | 阶段 | 核心操作 | 失败回退策略 | |------|---------|-------------| | BucketCreated | 创建存储空间,分配地域和冗余策略 | 幂等重试,已有 bucket 视为成功 | | FileUploaded | PUT 对象请求,校验 ETag 一致性 | 断点续传 + 分片合并 | | CDNPrefetch | 向 CDN 节点下发缓存指令 | 异步队列,不阻塞主链路 | | AccessAuthorized | 签发临时签名 URL 或配置 ACL | 与文件上传绑定,原子操作 | | TrafficTracked | 持续指标上报至时序数据库 | 本地 buffer,批量异步发送 | | LifecycleConfigured | 设置过期规则(如 90 天自动删除) | API 级别幂等更新 | | CleanupPending | 定时任务扫描到期对象,标记为删除中 | 二次确认后执行,可撤销 | | 物理删除 | 实际调用存储 API 删除对象 | 事务性删除,失败告警 | ### 关键设计决策 > [!WARNING] > 资源删除必须是**两阶段**的:先标记 `CleanupPending`,再执行 `物理删除`。这避免误删且支持回滚。跳过这个阶段是数据丢失的头号原因。 > [!NOTE] > 为什么"流量统计"被建模为状态而不是事件?因为流量统计是一个**持续的监控行为**,不是一个瞬时动作。在实际工程中,它可能通过侧车(sidecar)或事件总线持续运行,不受单用户的控制流影响。FSM 在这里只记录"是否已进入此阶段"。 ## 对比总结 | 维度 | 审批流 | 订单生命周期 | 资源交付 | |------|--------|-------------|---------| | 状态数量 | 5-8 | 6-8 | 8 | | 循环转移 | 有(退回修改) | 部分(退款→重新支付) | 无(线性流程) | | 并发安全要求 | 中等(单人审批) | **极高**(多人同时操作) | 高(异步任务冲突) | | 持久化策略 | 每步写 DB | 每步写 DB + 事件日志 | 最终一致 + 补偿机制 | ## 关联笔记 - [[有限状态机核心概念]]