commit cbd4a33eaf33bfa1a3bdaf421ebd8007ab63b665 Author: meowrain Date: Sat Feb 14 00:01:04 2026 +0800 all diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..ad8adf8 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(javac:*)", + "Bash(java:*)" + ] + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..21bbd22 --- /dev/null +++ b/README.md @@ -0,0 +1,323 @@ +# 电商订单状态机 (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 框架做企业级实现 diff --git a/out/com/example/fsm/OrderFSMDemo.class b/out/com/example/fsm/OrderFSMDemo.class new file mode 100644 index 0000000..1892a55 Binary files /dev/null and b/out/com/example/fsm/OrderFSMDemo.class differ diff --git a/out/com/example/fsm/core/Action.class b/out/com/example/fsm/core/Action.class new file mode 100644 index 0000000..1021b6d Binary files /dev/null and b/out/com/example/fsm/core/Action.class differ diff --git a/out/com/example/fsm/core/Guard.class b/out/com/example/fsm/core/Guard.class new file mode 100644 index 0000000..43fd7cf Binary files /dev/null and b/out/com/example/fsm/core/Guard.class differ diff --git a/out/com/example/fsm/core/StateMachine$Builder.class b/out/com/example/fsm/core/StateMachine$Builder.class new file mode 100644 index 0000000..d0faa80 Binary files /dev/null and b/out/com/example/fsm/core/StateMachine$Builder.class differ diff --git a/out/com/example/fsm/core/StateMachine$StateChangeListener.class b/out/com/example/fsm/core/StateMachine$StateChangeListener.class new file mode 100644 index 0000000..990dc23 Binary files /dev/null and b/out/com/example/fsm/core/StateMachine$StateChangeListener.class differ diff --git a/out/com/example/fsm/core/StateMachine.class b/out/com/example/fsm/core/StateMachine.class new file mode 100644 index 0000000..ede2981 Binary files /dev/null and b/out/com/example/fsm/core/StateMachine.class differ diff --git a/out/com/example/fsm/core/Transition.class b/out/com/example/fsm/core/Transition.class new file mode 100644 index 0000000..4f6340a Binary files /dev/null and b/out/com/example/fsm/core/Transition.class differ diff --git a/out/com/example/fsm/order/OrderContext.class b/out/com/example/fsm/order/OrderContext.class new file mode 100644 index 0000000..8c131bf Binary files /dev/null and b/out/com/example/fsm/order/OrderContext.class differ diff --git a/out/com/example/fsm/order/OrderEvent.class b/out/com/example/fsm/order/OrderEvent.class new file mode 100644 index 0000000..6faf225 Binary files /dev/null and b/out/com/example/fsm/order/OrderEvent.class differ diff --git a/out/com/example/fsm/order/OrderState.class b/out/com/example/fsm/order/OrderState.class new file mode 100644 index 0000000..cf9d5db Binary files /dev/null and b/out/com/example/fsm/order/OrderState.class differ diff --git a/out/com/example/fsm/order/OrderStateMachineFactory.class b/out/com/example/fsm/order/OrderStateMachineFactory.class new file mode 100644 index 0000000..20f3088 Binary files /dev/null and b/out/com/example/fsm/order/OrderStateMachineFactory.class differ diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..58249a1 --- /dev/null +++ b/run.bat @@ -0,0 +1,25 @@ +@echo off +chcp 65001 >nul 2>&1 +echo. +echo ======================================== +echo 编译并运行 订单状态机演示程序 +echo ======================================== +echo. + +if not exist out mkdir out + +echo [1/2] 编译中... +javac -encoding UTF-8 -d out src\com\example\fsm\core\*.java src\com\example\fsm\order\*.java src\com\example\fsm\OrderFSMDemo.java + +if %errorlevel% neq 0 ( + echo 编译失败!请检查 Java 环境。 + pause + exit /b 1 +) + +echo [2/2] 运行中... +echo. +java -Dfile.encoding=UTF-8 -cp out com.example.fsm.OrderFSMDemo + +echo. +pause diff --git a/src/com/example/fsm/OrderFSMDemo.java b/src/com/example/fsm/OrderFSMDemo.java new file mode 100644 index 0000000..8f48761 --- /dev/null +++ b/src/com/example/fsm/OrderFSMDemo.java @@ -0,0 +1,214 @@ +package com.example.fsm; + +import com.example.fsm.core.StateMachine; +import com.example.fsm.order.OrderContext; +import com.example.fsm.order.OrderEvent; +import com.example.fsm.order.OrderState; +import com.example.fsm.order.OrderStateMachineFactory; + +import java.math.BigDecimal; + +/** + * 订单状态机演示程序 + * + * 本程序演示三个典型场景: + * 场景一:正常下单 → 支付 → 发货 → 收货 → 完成(Happy Path) + * 场景二:下单 → 支付超时自动取消 + * 场景三:下单 → 支付 → 申请退款 → 退款成功 + * 场景四:余额不足支付失败(演示守卫条件) + * 场景五:非法操作(演示状态机的防护能力) + */ +public class OrderFSMDemo { + + public static void main(String[] args) { + + System.out.println("╔══════════════════════════════════════════════════════╗"); + System.out.println("║ 电商订单状态机演示程序 ║"); + System.out.println("╚══════════════════════════════════════════════════════╝"); + System.out.println(); + + scenario1_happyPath(); + scenario2_timeout(); + scenario3_refund(); + scenario4_insufficientBalance(); + scenario5_illegalTransition(); + } + + /** + * 场景一:正常完整流程(Happy Path) + * + * 待支付 → 已支付 → 已发货 → 已收货 → 已完成 + */ + private static void scenario1_happyPath() { + printScenarioHeader("场景一:正常完整流程(Happy Path)"); + + // 1. 创建状态机 + StateMachine fsm = OrderStateMachineFactory.create(); + + // 2. 注册监听器 —— 每次状态变化都会被记录 + fsm.addListener((from, event, to, ctx) -> + System.out.println(" [状态变更] " + from + " --[" + event + "]--> " + to) + ); + + // 3. 打印转换表(方便理解) + fsm.printTransitionTable(); + + // 4. 创建订单上下文 + OrderContext order = new OrderContext("ORD-2024-001", "Java编程思想(第4版)", new BigDecimal("99.00"), new BigDecimal("500.00")); + System.out.println("创建订单:" + order); + printState(fsm); + + // 5. 依次触发事件,推动状态流转 + System.out.println("\n--- 买家点击[立即支付] ---"); + fsm.fireEvent(OrderEvent.PAY, order); + printState(fsm); + + System.out.println("\n--- 卖家点击[确认发货] ---"); + fsm.fireEvent(OrderEvent.SHIP, order); + printState(fsm); + + System.out.println("\n--- 买家点击[确认收货] ---"); + fsm.fireEvent(OrderEvent.RECEIVE, order); + printState(fsm); + + System.out.println("\n--- 买家完成评价 ---"); + fsm.fireEvent(OrderEvent.COMPLETE, order); + printState(fsm); + + System.out.println(); + } + + /** + * 场景二:支付超时自动取消 + * + * 待支付 → 已取消 + */ + private static void scenario2_timeout() { + printScenarioHeader("场景二:支付超时自动取消"); + + StateMachine fsm = OrderStateMachineFactory.create(); + fsm.addListener((from, event, to, ctx) -> + System.out.println(" [状态变更] " + from + " --[" + event + "]--> " + to) + ); + + OrderContext order = new OrderContext("ORD-2024-002", "设计模式:可复用面向对象软件的基础", new BigDecimal("68.00"), new BigDecimal("500.00")); + System.out.println("创建订单:" + order); + printState(fsm); + + System.out.println("\n--- 30分钟后,系统触发超时事件 ---"); + fsm.fireEvent(OrderEvent.TIMEOUT, order); + printState(fsm); + + System.out.println(); + } + + /** + * 场景三:申请退款 + * + * 待支付 → 已支付 → 退款中 → 已退款 + */ + private static void scenario3_refund() { + printScenarioHeader("场景三:支付后申请退款"); + + StateMachine fsm = OrderStateMachineFactory.create(); + fsm.addListener((from, event, to, ctx) -> + System.out.println(" [状态变更] " + from + " --[" + event + "]--> " + to) + ); + + OrderContext order = new OrderContext("ORD-2024-003", "深入理解Java虚拟机(第3版)", new BigDecimal("129.00"), new BigDecimal("500.00")); + System.out.println("创建订单:" + order); + printState(fsm); + + System.out.println("\n--- 买家支付 ---"); + fsm.fireEvent(OrderEvent.PAY, order); + printState(fsm); + + System.out.println("\n--- 买家申请退款 ---"); + order.setRefundReason("买错了,想要第2版"); + fsm.fireEvent(OrderEvent.REQUEST_REFUND, order); + printState(fsm); + + System.out.println("\n--- 卖家审核通过退款 ---"); + fsm.fireEvent(OrderEvent.REFUND_APPROVE, order); + printState(fsm); + + System.out.println(); + } + + /** + * 场景四:余额不足,支付失败 + * + * 演示 Guard(守卫条件)的作用 + */ + private static void scenario4_insufficientBalance() { + printScenarioHeader("场景四:余额不足,支付被守卫条件拦截"); + + StateMachine fsm = OrderStateMachineFactory.create(); + + // 注意余额只有 50 元,但商品需要 999 元 + OrderContext order = new OrderContext("ORD-2024-004", "MacBook Pro 14寸", new BigDecimal("9999.00"), new BigDecimal("50.00")); + System.out.println("创建订单:" + order); + printState(fsm); + + System.out.println("\n--- 买家尝试支付(余额不足)---"); + try { + fsm.fireEvent(OrderEvent.PAY, order); + } catch (IllegalStateException e) { + System.out.println(" [拦截] " + e.getMessage()); + } + // 状态应该还是"待支付",没有被错误地改变 + printState(fsm); + + System.out.println(); + } + + /** + * 场景五:非法状态转换 + * + * 演示状态机的"防护能力"——不允许不合法的操作 + */ + private static void scenario5_illegalTransition() { + printScenarioHeader("场景五:非法操作(在待支付状态下尝试发货)"); + + StateMachine fsm = OrderStateMachineFactory.create(); + + OrderContext order = new OrderContext("ORD-2024-005", "算法导论(第3版)", new BigDecimal("128.00"), new BigDecimal("500.00")); + System.out.println("创建订单:" + order); + printState(fsm); + + System.out.println("\n--- 尝试在未支付时直接发货 ---"); + try { + fsm.fireEvent(OrderEvent.SHIP, order); // 这是非法的! + } catch (IllegalStateException e) { + System.out.println(" [拦截] " + e.getMessage()); + } + printState(fsm); + + System.out.println("\n说明:状态机保证了业务流程的正确性,不可能跳过支付直接发货。"); + System.out.println("\n当前状态下可执行的事件:" + fsm.getAvailableEvents()); + System.out.println(); + } + + // ===== 辅助方法 ===== + + private static void printState(StateMachine fsm) { + System.out.println(" ★ 当前状态:" + fsm.getCurrentState()); + } + + private static void printScenarioHeader(String title) { + System.out.println("┌─────────────────────────────────────────────────┐"); + System.out.printf("│ %s%s│%n", title, " ".repeat(Math.max(0, 47 - title.length() - getChineseCount(title)))); + System.out.println("└─────────────────────────────────────────────────┘"); + } + + /** 计算字符串中中文字符的数量(中文占两个宽度,需要额外减去) */ + private static int getChineseCount(String str) { + int count = 0; + for (char c : str.toCharArray()) { + if (Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN) { + count++; + } + } + return count; + } +} diff --git a/src/com/example/fsm/core/Action.java b/src/com/example/fsm/core/Action.java new file mode 100644 index 0000000..fb76cd7 --- /dev/null +++ b/src/com/example/fsm/core/Action.java @@ -0,0 +1,20 @@ +package com.example.fsm.core; + +/** + * 动作接口 —— 状态转换时要执行的业务逻辑 + * + * 例如:支付成功后扣减库存、发货后发送短信通知等。 + * 使用泛型 C 代表上下文(Context),这样不同业务可以传入自己的数据对象。 + * + * @param 上下文类型,承载业务数据(如订单信息) + */ +@FunctionalInterface +public interface Action { + + /** + * 执行动作 + * + * @param context 当前业务上下文 + */ + void execute(C context); +} diff --git a/src/com/example/fsm/core/Guard.java b/src/com/example/fsm/core/Guard.java new file mode 100644 index 0000000..f729046 --- /dev/null +++ b/src/com/example/fsm/core/Guard.java @@ -0,0 +1,25 @@ +package com.example.fsm.core; + +/** + * 守卫条件接口 —— 判断某个状态转换是否允许发生 + * + * 举个例子: + * "待支付 -> 已支付" 这个转换,需要守卫条件检查余额是否充足。 + * 如果余额不足,即使触发了"支付"事件,转换也不会发生。 + * + * 守卫条件是状态机中非常重要的概念,它让转换不再是"无脑跳转", + * 而是可以根据业务规则动态决定是否放行。 + * + * @param 上下文类型 + */ +@FunctionalInterface +public interface Guard { + + /** + * 判断是否满足转换条件 + * + * @param context 当前业务上下文 + * @return true=允许转换, false=阻止转换 + */ + boolean evaluate(C context); +} diff --git a/src/com/example/fsm/core/StateMachine.java b/src/com/example/fsm/core/StateMachine.java new file mode 100644 index 0000000..3777995 --- /dev/null +++ b/src/com/example/fsm/core/StateMachine.java @@ -0,0 +1,223 @@ +package com.example.fsm.core; + +import java.util.*; + +/** + * 状态机核心引擎 + * + * 这是整个状态机的"大脑"。它做三件事: + * 1. 维护当前状态 + * 2. 存储所有合法的转换规则 + * 3. 接收事件,查找匹配的转换,执行状态跳转 + * + * 工作流程(当外部触发一个事件时): + * ┌─────────────────────────────────────────────────────┐ + * │ 1. 根据 "当前状态 + 事件" 查找转换规则 │ + * │ 2. 如果找到了转换规则: │ + * │ a. 检查守卫条件(Guard),不满足则拒绝 │ + * │ b. 执行转换动作(Action) │ + * │ c. 将当前状态切换为目标状态 │ + * │ 3. 如果没找到:抛出异常,说明这个事件在当前状态下 │ + * │ 是非法的 │ + * └─────────────────────────────────────────────────────┘ + * + * 用法:通过 Builder 构建(见下方内部类) + * + * @param 状态类型 + * @param 事件类型 + * @param 上下文类型 + */ +public class StateMachine, E extends Enum, C> { + + /** 当前状态 */ + private S currentState; + + /** + * 转换规则表 + * + * 结构:Map< 源状态, Map< 事件, 转换规则 > > + * + * 这是一个两级 Map: + * 第一级 key = 源状态 → 得到该状态下所有可能的转换 + * 第二级 key = 事件 → 得到具体的转换规则 + * + * 例如: + * transitionTable.get(待支付).get(支付事件) → Transition{待支付→已支付} + */ + private final Map>> transitionTable; + + /** 状态变更监听器列表(观察者模式) */ + private final List> listeners; + + /** + * 私有构造 —— 只能通过 Builder 创建 + */ + private StateMachine(S initialState, Map>> transitionTable) { + this.currentState = initialState; + this.transitionTable = transitionTable; + this.listeners = new ArrayList<>(); + } + + /** + * 获取当前状态 + */ + public S getCurrentState() { + return currentState; + } + + /** + * 触发事件 —— 这是状态机对外的核心方法 + * + * @param event 要触发的事件 + * @param context 业务上下文 + * @throws IllegalStateException 如果当前状态下该事件不合法 + */ + public void fireEvent(E event, C context) { + // 第一步:根据当前状态查找该状态下的所有转换 + Map> stateTransitions = transitionTable.get(currentState); + + if (stateTransitions == null || !stateTransitions.containsKey(event)) { + throw new IllegalStateException( + String.format("非法的状态转换!当前状态=[%s],触发事件=[%s],没有匹配的转换规则。", currentState, event) + ); + } + + // 第二步:取出对应的转换规则 + Transition transition = stateTransitions.get(event); + + // 第三步:检查守卫条件 + if (transition.getGuard() != null && !transition.getGuard().evaluate(context)) { + throw new IllegalStateException( + String.format("转换被守卫条件拒绝!转换=[%s]", transition) + ); + } + + // 记录旧状态(用于通知监听器) + S oldState = currentState; + + // 第四步:执行转换动作(业务逻辑) + if (transition.getAction() != null) { + transition.getAction().execute(context); + } + + // 第五步:切换状态 + currentState = transition.getTarget(); + + // 第六步:通知所有监听器 + for (StateChangeListener listener : listeners) { + listener.onStateChanged(oldState, event, currentState, context); + } + } + + /** + * 添加状态变更监听器 + */ + public void addListener(StateChangeListener listener) { + listeners.add(listener); + } + + /** + * 获取当前状态下可以触发的所有事件(用于 UI 展示"下一步可以做什么") + */ + public Set getAvailableEvents() { + Map> stateTransitions = transitionTable.get(currentState); + if (stateTransitions == null) { + return Collections.emptySet(); + } + return Collections.unmodifiableSet(stateTransitions.keySet()); + } + + /** + * 打印所有转换规则(调试用) + */ + public void printTransitionTable() { + System.out.println("========== 状态转换表 =========="); + for (Map.Entry>> entry : transitionTable.entrySet()) { + for (Transition t : entry.getValue().values()) { + System.out.println(" " + t); + } + } + System.out.println("================================"); + } + + // ========================================================================= + // 状态变更监听器接口 + // ========================================================================= + + /** + * 状态变更监听器 —— 当状态发生变化时收到通知 + * + * 典型用途:记录日志、发送通知、更新数据库等 + */ + @FunctionalInterface + public interface StateChangeListener { + void onStateChanged(S fromState, E event, S toState, C context); + } + + // ========================================================================= + // Builder 构建器 —— 用于流畅地创建状态机 + // ========================================================================= + + /** + * 状态机构建器 + * + * 使用方式: + * StateMachine fsm = StateMachine + * .builder() + * .initialState(MyState.INIT) + * .addTransition(...) + * .addTransition(...) + * .build(); + */ + public static , E extends Enum, C> Builder builder() { + return new Builder<>(); + } + + public static class Builder, E extends Enum, C> { + + private S initialState; + private final Map>> transitionTable = new HashMap<>(); + + /** + * 设置初始状态 + */ + public Builder initialState(S state) { + this.initialState = state; + return this; + } + + /** + * 添加一条转换规则(完整版:含守卫和动作) + */ + public Builder addTransition(S source, E event, S target, Guard guard, Action action) { + transitionTable + .computeIfAbsent(source, k -> new HashMap<>()) + .put(event, new Transition<>(source, event, target, guard, action)); + return this; + } + + /** + * 添加一条转换规则(简化版:只有动作,没有守卫) + */ + public Builder addTransition(S source, E event, S target, Action action) { + return addTransition(source, event, target, null, action); + } + + /** + * 添加一条转换规则(最简版:纯状态跳转) + */ + public Builder addTransition(S source, E event, S target) { + return addTransition(source, event, target, null, null); + } + + /** + * 构建状态机实例 + */ + public StateMachine build() { + if (initialState == null) { + throw new IllegalStateException("必须设置初始状态!请调用 initialState()"); + } + return new StateMachine<>(initialState, transitionTable); + } + } +} diff --git a/src/com/example/fsm/core/Transition.java b/src/com/example/fsm/core/Transition.java new file mode 100644 index 0000000..cb15ab9 --- /dev/null +++ b/src/com/example/fsm/core/Transition.java @@ -0,0 +1,74 @@ +package com.example.fsm.core; + +/** + * 状态转换定义 —— 描述"从哪个状态,经过什么事件,到达哪个状态" + * + * 这是状态机最核心的数据结构。每一条 Transition 就是状态图里的一条"箭头"。 + * + * 一条完整的转换包含五个要素: + * 1. source —— 源状态(箭头的起点) + * 2. event —— 触发事件(箭头上的标签) + * 3. target —— 目标状态(箭头的终点) + * 4. guard —— 守卫条件(可选,转换前的检查) + * 5. action —— 转换动作(可选,转换时执行的业务逻辑) + * + * 示例: + * source=待支付, event=支付, target=已支付, guard=余额充足?, action=扣款 + * + * @param 状态类型(通常是枚举) + * @param 事件类型(通常是枚举) + * @param 上下文类型(业务数据) + */ +public class Transition, E extends Enum, C> { + + private final S source; // 源状态 + private final E event; // 触发事件 + private final S target; // 目标状态 + private final Guard guard; // 守卫条件(可为 null,表示无条件放行) + private final Action action; // 转换动作(可为 null,表示无副作用) + + /** + * 完整构造:指定所有要素 + */ + public Transition(S source, E event, S target, Guard guard, Action action) { + this.source = source; + this.event = event; + this.target = target; + this.guard = guard; + this.action = action; + } + + /** + * 简化构造:没有守卫条件和动作(纯粹的状态跳转) + */ + public Transition(S source, E event, S target) { + this(source, event, target, null, null); + } + + // ===== Getter 方法 ===== + + public S getSource() { + return source; + } + + public E getEvent() { + return event; + } + + public S getTarget() { + return target; + } + + public Guard getGuard() { + return guard; + } + + public Action getAction() { + return action; + } + + @Override + public String toString() { + return source + " --[" + event + "]--> " + target; + } +} diff --git a/src/com/example/fsm/order/OrderContext.java b/src/com/example/fsm/order/OrderContext.java new file mode 100644 index 0000000..dff15ea --- /dev/null +++ b/src/com/example/fsm/order/OrderContext.java @@ -0,0 +1,103 @@ +package com.example.fsm.order; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 订单上下文 —— 承载订单的业务数据 + * + * 在状态机模式中,Context(上下文)用于在状态转换过程中传递业务数据。 + * Guard(守卫)可以读取它来判断条件,Action(动作)可以修改它来更新业务状态。 + * + * 这里模拟了一个简化版的电商订单,包含核心字段。 + */ +public class OrderContext { + + private String orderId; // 订单号 + private String productName; // 商品名称 + private BigDecimal amount; // 订单金额 + private BigDecimal userBalance; // 用户余额(用于演示守卫条件) + private String refundReason; // 退款原因 + private LocalDateTime createTime; // 创建时间 + private LocalDateTime payTime; // 支付时间 + private LocalDateTime shipTime; // 发货时间 + + /** + * 创建一个模拟订单 + */ + public OrderContext(String orderId, String productName, BigDecimal amount, BigDecimal userBalance) { + this.orderId = orderId; + this.productName = productName; + this.amount = amount; + this.userBalance = userBalance; + this.createTime = LocalDateTime.now(); + } + + // ===== 业务方法 ===== + + /** + * 扣减用户余额(支付时调用) + */ + public void deductBalance() { + this.userBalance = this.userBalance.subtract(this.amount); + this.payTime = LocalDateTime.now(); + } + + /** + * 退还用户余额(退款时调用) + */ + public void refundBalance() { + this.userBalance = this.userBalance.add(this.amount); + } + + /** + * 记录发货时间 + */ + public void markShipped() { + this.shipTime = LocalDateTime.now(); + } + + /** + * 判断余额是否充足 + */ + public boolean isBalanceSufficient() { + return userBalance.compareTo(amount) >= 0; + } + + // ===== Getter / Setter ===== + + public String getOrderId() { + return orderId; + } + + public String getProductName() { + return productName; + } + + public BigDecimal getAmount() { + return amount; + } + + public BigDecimal getUserBalance() { + return userBalance; + } + + public String getRefundReason() { + return refundReason; + } + + public void setRefundReason(String refundReason) { + this.refundReason = refundReason; + } + + @Override + public String toString() { + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return String.format( + "订单[%s] 商品=%s 金额=%.2f 用户余额=%.2f 创建时间=%s", + orderId, productName, amount, userBalance, + createTime != null ? createTime.format(fmt) : "无" + ); + } +} diff --git a/src/com/example/fsm/order/OrderEvent.java b/src/com/example/fsm/order/OrderEvent.java new file mode 100644 index 0000000..485b909 --- /dev/null +++ b/src/com/example/fsm/order/OrderEvent.java @@ -0,0 +1,48 @@ +package com.example.fsm.order; + +/** + * 订单事件枚举 + * + * 事件 = 触发状态转换的"动作"或"信号" + * + * 在状态机中,状态不会自己变化,必须由事件来驱动。 + * 比如:用户点击"付款"按钮 → 系统发出 PAY 事件 → 状态机将订单从"待支付"转为"已支付" + * + * 每个事件都有明确的触发者(买家/卖家/系统)和触发场景。 + */ +public enum OrderEvent { + + // ===== 正向流程事件 ===== + PAY("支付", "买家完成付款"), + SHIP("发货", "卖家确认发货"), + RECEIVE("确认收货", "买家确认收到商品"), + COMPLETE("完成", "买家完成评价,或系统自动完成"), + + // ===== 逆向流程事件 ===== + CANCEL("取消订单", "买家主动取消未支付的订单"), + TIMEOUT("支付超时", "系统检测到订单超过支付时限,自动取消"), + REQUEST_REFUND("申请退款", "买家发起退款申请"), + REFUND_APPROVE("退款通过", "卖家审核通过退款申请"), + REFUND_REJECT("退款拒绝", "卖家驳回退款申请"); + + private final String displayName; // 中文显示名 + private final String description; // 事件说明 + + OrderEvent(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return displayName + "(" + name() + ")"; + } +} diff --git a/src/com/example/fsm/order/OrderState.java b/src/com/example/fsm/order/OrderState.java new file mode 100644 index 0000000..071a754 --- /dev/null +++ b/src/com/example/fsm/order/OrderState.java @@ -0,0 +1,61 @@ +package com.example.fsm.order; + +/** + * 订单状态枚举 + * + * 对应电商系统中一个订单从创建到结束的所有可能状态。 + * + * 状态流转全景图: + * + * ┌──────────┐ + * ┌───→│ 已取消 │ + * 取消/超时 │ └──────────┘ + * │ + * ┌──────────┐ 支付 ┌──────────┐ 发货 ┌──────────┐ 确认收货 ┌──────────┐ 完成 ┌──────────┐ + * │ 待支付 │──────→│ 已支付 │──────→│ 已发货 │─────────→│ 已收货 │──────→│ 已完成 │ + * └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ + * │ │ + * 申请退款│ 申请退款│ + * ▼ ▼ + * ┌──────────┐ + * │ 退款中 │ + * └──────────┘ + * │ │ + * 退款通过│ 退款拒绝│ + * ▼ ▼ + * ┌──────────┐ 回到之前的状态 + * │ 已退款 │ (已支付/已发货) + * └──────────┘ + */ +public enum OrderState { + + WAIT_PAY("待支付", "订单已创建,等待买家付款"), + PAID("已支付", "买家已完成支付,等待卖家发货"), + SHIPPED("已发货", "卖家已发货,等待买家确认收货"), + RECEIVED("已收货", "买家已确认收货,等待评价"), + COMPLETED("已完成", "订单已完成(已评价或超时自动完成)"), + CANCELLED("已取消", "订单已取消(买家主动取消或支付超时)"), + REFUNDING("退款中", "买家申请退款,等待卖家审核"), + REFUNDED("已退款", "退款已完成,资金已退回买家账户"); + + private final String displayName; // 中文显示名 + private final String description; // 状态说明 + + OrderState(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return displayName + "(" + name() + ")"; + } +} diff --git a/src/com/example/fsm/order/OrderStateMachineFactory.java b/src/com/example/fsm/order/OrderStateMachineFactory.java new file mode 100644 index 0000000..520a5e7 --- /dev/null +++ b/src/com/example/fsm/order/OrderStateMachineFactory.java @@ -0,0 +1,141 @@ +package com.example.fsm.order; + +import com.example.fsm.core.StateMachine; + +/** + * 订单状态机工厂 —— 负责组装订单状态机的所有转换规则 + * + * 为什么要单独一个工厂类? + * 状态机的"骨架"(StateMachine)是通用的,不关心具体业务。 + * 而"转换规则"是订单业务特有的。把规则的组装抽到工厂类里, + * 实现了 "状态机引擎" 和 "业务规则" 的分离。 + * + * 如果将来要做"物流状态机"或"审批状态机",只需要写新的工厂类, + * 核心引擎完全复用。 + */ +public class OrderStateMachineFactory { + + /** + * 创建并返回一个配置好所有转换规则的订单状态机 + * + * 下面每一条 addTransition 都对应状态图上的一条箭头。 + * 阅读时建议对照 OrderState 中的状态流转图。 + */ + public static StateMachine create() { + + return StateMachine.builder() + + // ============================== + // 初始状态:待支付 + // ============================== + .initialState(OrderState.WAIT_PAY) + + // ------------------------------ + // 正向流程 + // ------------------------------ + + // 待支付 --[支付]--> 已支付 + // 守卫条件:用户余额 >= 订单金额 + // 动作:扣减余额 + .addTransition( + OrderState.WAIT_PAY, // 源状态 + OrderEvent.PAY, // 触发事件 + OrderState.PAID, // 目标状态 + ctx -> ctx.isBalanceSufficient(), // 守卫:余额充足才能支付 + ctx -> { // 动作:扣款 + ctx.deductBalance(); + System.out.println(" [动作] 扣款成功!扣除 " + ctx.getAmount() + " 元,剩余余额 " + ctx.getUserBalance() + " 元"); + } + ) + + // 已支付 --[发货]--> 已发货 + // 动作:记录发货时间 + .addTransition( + OrderState.PAID, + OrderEvent.SHIP, + OrderState.SHIPPED, + ctx -> { + ctx.markShipped(); + System.out.println(" [动作] 商品已发出!请注意查收。"); + } + ) + + // 已发货 --[确认收货]--> 已收货 + .addTransition( + OrderState.SHIPPED, + OrderEvent.RECEIVE, + OrderState.RECEIVED, + ctx -> System.out.println(" [动作] 买家已确认收货。") + ) + + // 已收货 --[完成]--> 已完成 + .addTransition( + OrderState.RECEIVED, + OrderEvent.COMPLETE, + OrderState.COMPLETED, + ctx -> System.out.println(" [动作] 订单完成!感谢您的购买。") + ) + + // ------------------------------ + // 取消/超时流程 + // ------------------------------ + + // 待支付 --[取消]--> 已取消 + .addTransition( + OrderState.WAIT_PAY, + OrderEvent.CANCEL, + OrderState.CANCELLED, + ctx -> System.out.println(" [动作] 订单已取消。") + ) + + // 待支付 --[超时]--> 已取消 + .addTransition( + OrderState.WAIT_PAY, + OrderEvent.TIMEOUT, + OrderState.CANCELLED, + ctx -> System.out.println(" [动作] 支付超时,订单自动取消。") + ) + + // ------------------------------ + // 退款流程 + // ------------------------------ + + // 已支付 --[申请退款]--> 退款中 + .addTransition( + OrderState.PAID, + OrderEvent.REQUEST_REFUND, + OrderState.REFUNDING, + ctx -> System.out.println(" [动作] 退款申请已提交,等待卖家审核。原因:" + ctx.getRefundReason()) + ) + + // 已发货 --[申请退款]--> 退款中 + .addTransition( + OrderState.SHIPPED, + OrderEvent.REQUEST_REFUND, + OrderState.REFUNDING, + ctx -> System.out.println(" [动作] 退款申请已提交(商品在途),等待卖家审核。原因:" + ctx.getRefundReason()) + ) + + // 退款中 --[退款通过]--> 已退款 + // 动作:退还用户余额 + .addTransition( + OrderState.REFUNDING, + OrderEvent.REFUND_APPROVE, + OrderState.REFUNDED, + ctx -> { + ctx.refundBalance(); + System.out.println(" [动作] 退款成功!已退回 " + ctx.getAmount() + " 元,当前余额 " + ctx.getUserBalance() + " 元"); + } + ) + + // 退款中 --[退款拒绝]--> 已支付(退回到支付后的状态,简化处理) + .addTransition( + OrderState.REFUNDING, + OrderEvent.REFUND_REJECT, + OrderState.PAID, + ctx -> System.out.println(" [动作] 退款申请被驳回,订单恢复为已支付状态。") + ) + + .build(); + } +}