11 KiB
11 KiB
电商订单状态机 (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 |
| 安全性 | 容易遗漏边界情况 | 未定义的转换自动拒绝 |
| 可视化 | 难以从代码推导出状态图 | 可直接导出/打印转换表 |
| 可测试性 | 需要覆盖所有分支组合 | 可针对每条转换单独测试 |
扩展思路
如果想进一步学习,可以尝试:
- 持久化:将状态存入数据库,重启后恢复
- 并发安全:给
fireEvent加锁或使用synchronized - 历史记录:记录每次状态转换的日志(审计追踪)
- 子状态机:比如"退款中"内部还有子流程(审核→打款→到账)
- 超时调度:结合
ScheduledExecutorService实现真实的超时自动取消 - Spring 集成:用 Spring Statemachine 框架做企业级实现