# 电商订单状态机 (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 │ └─────────────────────────────┘ ``` **引擎层**完全不知道"订单"的存在,它只认识泛型 ``。这意味着你可以用同一个引擎去做物流状态机、审批流、游戏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 { private final S source; // 从哪个状态 private final E event; // 收到什么事件 private final S target; // 跳到哪个状态 private final Guard guard; // 跳之前检查什么条件 private final Action action; // 跳的时候做什么事 } ``` ### 4. 状态机引擎 (`StateMachine.java`) 核心的 `fireEvent` 方法: ```java public void fireEvent(E event, C context) { // 1. 根据 "当前状态 + 事件" 查找转换规则 Transition 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.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 框架做企业级实现