324 lines
11 KiB
Markdown
324 lines
11 KiB
Markdown
# 电商订单状态机 (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等任何状态驱动的场景。
|
||
|
||
---
|
||
|
||
## 快速运行
|
||
|
||
### 方式一:使用脚本(推荐)
|
||
|
||
```bash
|
||
# Windows
|
||
run.bat
|
||
```
|
||
|
||
### 方式二:手动编译运行
|
||
|
||
```bash
|
||
# 编译
|
||
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`)
|
||
|
||
```java
|
||
public enum OrderState {
|
||
WAIT_PAY("待支付", "订单已创建,等待买家付款"),
|
||
PAID("已支付", "买家已完成支付,等待卖家发货"),
|
||
SHIPPED("已发货", "卖家已发货,等待买家确认收货"),
|
||
// ...
|
||
}
|
||
```
|
||
|
||
用枚举定义状态是最佳实践:
|
||
- 类型安全,不可能出现拼写错误
|
||
- IDE 自动补全
|
||
- 可以附加元数据(中文名、描述等)
|
||
|
||
### 2. 事件定义 (`OrderEvent.java`)
|
||
|
||
```java
|
||
public enum OrderEvent {
|
||
PAY("支付", "买家完成付款"),
|
||
SHIP("发货", "卖家确认发货"),
|
||
CANCEL("取消订单", "买家主动取消未支付的订单"),
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 3. 转换规则 (`Transition.java`)
|
||
|
||
```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` 方法:
|
||
|
||
```java
|
||
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 模式流畅地注册所有转换规则:
|
||
|
||
```java
|
||
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 地狱)
|
||
|
||
```java
|
||
// 不用状态机的写法 —— 随着状态增多,代码将迅速失控
|
||
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";
|
||
}
|
||
}
|
||
// ... 每增加一个状态,嵌套就多一层
|
||
}
|
||
```
|
||
|
||
### 状态机写法
|
||
|
||
```java
|
||
// 声明式,一目了然
|
||
.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 框架做企业级实现
|