Files
fsm-demo/README.md
2026-02-14 00:01:04 +08:00

11 KiB
Raw Permalink Blame History

电商订单状态机 (Order FSM Demo)

什么是状态机?

状态机Finite State Machine, FSM是一种数学计算模型。它在任意时刻只能处于一个状态,通过接收事件触发状态转换,从一个状态跳到另一个状态。

用人话说就是:状态机 = 当前状态 + 事件 → 新状态

核心概念

概念 英文 说明 本项目中的例子
状态 (State) State 事物当前所处的情况 待支付、已支付、已发货...
事件 (Event) Event 触发状态变化的信号 支付、发货、确认收货...
转换 (Transition) Transition 从一个状态到另一个状态的跳转规则 待支付 --[支付]--> 已支付
守卫 (Guard) Guard 转换前的条件检查,不满足则拒绝转换 余额是否充足?
动作 (Action) Action 转换发生时执行的业务逻辑 扣减余额、发送通知...
上下文 (Context) Context 承载业务数据的对象 订单信息(金额、商品等)

一图胜千言

                         ┌──────────┐
                    ┌───→│  已取消   │
          取消/超时 │    └──────────┘
                    │
   ┌──────────┐  支付  ┌──────────┐  发货  ┌──────────┐ 确认收货 ┌──────────┐  完成  ┌──────────┐
   │  待支付   │──────→│  已支付   │──────→│  已发货   │────────→│  已收货   │──────→│  已完成   │
   └──────────┘       └──────────┘       └──────────┘         └──────────┘       └──────────┘
                         │                   │
                   申请退款│             申请退款│
                         ▼                   ▼
                      ┌──────────┐
                      │  退款中   │
                      └──────────┘
                       │        │
                 退款通过│  退款拒绝│
                       ▼        ▼
                 ┌──────────┐  恢复为已支付
                 │  已退款   │
                 └──────────┘

项目结构

fsm_demo/
├── src/
│   └── com/example/fsm/
│       ├── core/                          # 状态机核心引擎(通用,可复用)
│       │   ├── Action.java                #   动作接口
│       │   ├── Guard.java                 #   守卫条件接口
│       │   ├── Transition.java            #   转换规则定义
│       │   └── StateMachine.java          #   状态机引擎 + Builder
│       │
│       ├── order/                         # 订单业务实现
│       │   ├── OrderState.java            #   订单状态枚举8种状态
│       │   ├── OrderEvent.java            #   订单事件枚举9种事件
│       │   ├── OrderContext.java           #   订单上下文(业务数据)
│       │   └── OrderStateMachineFactory.java  #   订单状态机工厂(组装规则)
│       │
│       └── OrderFSMDemo.java              # 演示程序入口5个场景
│
├── run.bat                                # Windows 一键运行脚本
└── README.md                              # 本文档

核心引擎 vs 业务实现

本项目刻意将代码分成两层:

┌─────────────────────────────┐
│     order/ (业务层)          │  ← 定义"订单有哪些状态/事件/规则"
│  OrderState, OrderEvent,    │
│  OrderContext,               │
│  OrderStateMachineFactory   │
├─────────────────────────────┤
│     core/ (引擎层)           │  ← 提供"状态机怎么运转"
│  StateMachine, Transition,  │
│  Action, Guard              │
└─────────────────────────────┘

引擎层完全不知道"订单"的存在,它只认识泛型 <S, E, C>。这意味着你可以用同一个引擎去做物流状态机、审批流、游戏AI等任何状态驱动的场景。


快速运行

方式一:使用脚本(推荐)

# Windows
run.bat

方式二:手动编译运行

# 编译
javac -encoding UTF-8 -d out src/com/example/fsm/core/*.java src/com/example/fsm/order/*.java src/com/example/fsm/OrderFSMDemo.java

# 运行
java -Dfile.encoding=UTF-8 -cp out com.example.fsm.OrderFSMDemo

演示场景

程序会依次运行 5 个场景,覆盖电商订单的主要流程:

场景一:正常完整流程 (Happy Path)

待支付 → 已支付 → 已发货 → 已收货 → 已完成

这是最常见的订单生命周期。买家下单、付款、卖家发货、买家收货、完成评价。

场景二:支付超时自动取消

待支付 → 已取消

系统检测到订单超过 30 分钟未支付,自动触发 TIMEOUT 事件取消订单。

场景三:支付后申请退款

待支付 → 已支付 → 退款中 → 已退款

买家付款后反悔,申请退款。卖家审核通过后,系统自动退还金额。

场景四:余额不足 (Guard 守卫演示)

待支付 → (支付失败,状态不变) → 待支付

用户余额 50 元,商品 9999 元。Guard 守卫条件检测到余额不足,拒绝状态转换。状态保持不变。

场景五:非法操作 (状态防护演示)

待支付 → (尝试发货,被拒绝) → 待支付

在未支付状态下直接尝试发货。状态机找不到匹配的转换规则,抛出异常阻止非法操作。


代码详解

1. 状态定义 (OrderState.java)

public enum OrderState {
    WAIT_PAY("待支付", "订单已创建,等待买家付款"),
    PAID("已支付", "买家已完成支付,等待卖家发货"),
    SHIPPED("已发货", "卖家已发货,等待买家确认收货"),
    // ...
}

用枚举定义状态是最佳实践:

  • 类型安全,不可能出现拼写错误
  • IDE 自动补全
  • 可以附加元数据(中文名、描述等)

2. 事件定义 (OrderEvent.java)

public enum OrderEvent {
    PAY("支付", "买家完成付款"),
    SHIP("发货", "卖家确认发货"),
    CANCEL("取消订单", "买家主动取消未支付的订单"),
    // ...
}

3. 转换规则 (Transition.java)

// 一条转换 = 源状态 + 事件 + 目标状态 + (可选)守卫 + (可选)动作
public class Transition<S, E, C> {
    private final S source;       // 从哪个状态
    private final E event;        // 收到什么事件
    private final S target;       // 跳到哪个状态
    private final Guard<C> guard; // 跳之前检查什么条件
    private final Action<C> action; // 跳的时候做什么事
}

4. 状态机引擎 (StateMachine.java)

核心的 fireEvent 方法:

public void fireEvent(E event, C context) {
    // 1. 根据 "当前状态 + 事件" 查找转换规则
    Transition<S, E, C> transition = transitionTable.get(currentState).get(event);

    // 2. 检查守卫条件
    if (transition.getGuard() != null && !transition.getGuard().evaluate(context)) {
        throw new IllegalStateException("守卫条件不满足");
    }

    // 3. 执行动作
    if (transition.getAction() != null) {
        transition.getAction().execute(context);
    }

    // 4. 切换状态
    currentState = transition.getTarget();
}

5. 工厂组装 (OrderStateMachineFactory.java)

用 Builder 模式流畅地注册所有转换规则:

StateMachine.<OrderState, OrderEvent, OrderContext>builder()
    .initialState(OrderState.WAIT_PAY)
    // 待支付 --[支付]--> 已支付(带守卫和动作)
    .addTransition(
        OrderState.WAIT_PAY, OrderEvent.PAY, OrderState.PAID,
        ctx -> ctx.isBalanceSufficient(),  // 守卫:余额充足?
        ctx -> ctx.deductBalance()         // 动作:扣款
    )
    // 已支付 --[发货]--> 已发货
    .addTransition(OrderState.PAID, OrderEvent.SHIP, OrderState.SHIPPED)
    // ...更多规则
    .build();

转换规则一览表

源状态 事件 目标状态 守卫条件 动作
待支付 支付 已支付 余额充足 扣减余额
待支付 取消订单 已取消
待支付 支付超时 已取消
已支付 发货 已发货 记录发货时间
已支付 申请退款 退款中 提交退款申请
已发货 确认收货 已收货
已发货 申请退款 退款中 提交退款申请
已收货 完成 已完成
退款中 退款通过 已退款 退还余额
退款中 退款拒绝 已支付

状态机的优势(为什么不用 if-else

传统写法if-else 地狱)

// 不用状态机的写法 —— 随着状态增多,代码将迅速失控
public void handleEvent(String event) {
    if (state.equals("WAIT_PAY")) {
        if (event.equals("PAY")) {
            if (balance >= amount) {
                state = "PAID";
                deductBalance();
            }
        } else if (event.equals("CANCEL")) {
            state = "CANCELLED";
        } else if (event.equals("TIMEOUT")) {
            state = "CANCELLED";
        }
    } else if (state.equals("PAID")) {
        if (event.equals("SHIP")) {
            state = "SHIPPED";
        } else if (event.equals("REQUEST_REFUND")) {
            state = "REFUNDING";
        }
    }
    // ... 每增加一个状态,嵌套就多一层
}

状态机写法

// 声明式,一目了然
.addTransition(WAIT_PAY, PAY,            PAID,      余额守卫, 扣款动作)
.addTransition(WAIT_PAY, CANCEL,         CANCELLED)
.addTransition(WAIT_PAY, TIMEOUT,        CANCELLED)
.addTransition(PAID,     SHIP,           SHIPPED)
.addTransition(PAID,     REQUEST_REFUND, REFUNDING)

对比总结

维度 if-else 状态机
可读性 嵌套深,难以理清 声明式,一行一规则
可维护性 新增状态需要改多处 新增一行 addTransition
安全性 容易遗漏边界情况 未定义的转换自动拒绝
可视化 难以从代码推导出状态图 可直接导出/打印转换表
可测试性 需要覆盖所有分支组合 可针对每条转换单独测试

扩展思路

如果想进一步学习,可以尝试:

  1. 持久化:将状态存入数据库,重启后恢复
  2. 并发安全:给 fireEvent 加锁或使用 synchronized
  3. 历史记录:记录每次状态转换的日志(审计追踪)
  4. 子状态机:比如"退款中"内部还有子流程(审核→打款→到账)
  5. 超时调度:结合 ScheduledExecutorService 实现真实的超时自动取消
  6. Spring 集成:用 Spring Statemachine 框架做企业级实现