状态模式在订单系统中怎么应用?
简化版
订单系统中,状态模式可以把待支付、已支付、已发货、已完成、已取消等状态分别封装成状态类。每个状态类定义当前状态下允许哪些操作,以及操作后迁移到哪个状态,从而避免订单服务里堆满状态判断。
详细版
订单是状态模式的经典场景,因为订单行为强依赖状态。
比如:
- 待支付:可以支付、取消;
- 已支付:可以发货、退款;
- 已发货:可以确认收货;
- 已完成:可以评价;
- 已取消:不能支付、不能发货。
如果用状态模式,可以定义 OrderState 接口,再实现 WaitPayState、PaidState、ShippedState、FinishedState、CanceledState。
订单上下文 OrderContext 持有当前状态,业务操作如 pay()、ship()、cancel() 委托给当前状态对象。状态对象执行当前状态下的逻辑,并在符合条件时切换到下一个状态。
真实订单系统还要额外考虑数据库持久化、支付回调幂等、并发状态更新、状态变更日志和异常补偿。
完整版教学
一、订单为什么适合状态模式
订单天然有生命周期:
创建 -> 待支付 -> 已支付 -> 已发货 -> 已完成
也可能有分支:
待支付 -> 已取消
已支付 -> 已退款
不同状态下,操作权限完全不同。
比如“发货”:
- 待支付订单不能发货;
- 已支付订单可以发货;
- 已发货订单不能重复发货;
- 已取消订单不能发货。
这就是典型的状态驱动行为。
二、定义订单状态接口
可以定义:
interface OrderState {
void pay(OrderContext context);
void cancel(OrderContext context);
void ship(OrderContext context);
void confirmReceive(OrderContext context);
}
这些方法代表订单可能收到的操作。
每个具体状态类决定哪些操作合法。
三、待支付状态实现
class WaitPayState implements OrderState {
public void pay(OrderContext context) {
// 记录支付结果
context.changeState(new PaidState());
}
public void cancel(OrderContext context) {
// 释放优惠券、库存预占
context.changeState(new CanceledState());
}
public void ship(OrderContext context) {
throw new RuntimeException("待支付订单不能发货");
}
public void confirmReceive(OrderContext context) {
throw new RuntimeException("待支付订单不能确认收货");
}
}
待支付状态的规则集中在这个类里,不需要订单服务到处判断 status == WAIT_PAY。
四、已支付状态实现
class PaidState implements OrderState {
public void pay(OrderContext context) {
throw new RuntimeException("订单不能重复支付");
}
public void cancel(OrderContext context) {
// 发起退款,再取消
context.changeState(new CanceledState());
}
public void ship(OrderContext context) {
// 创建物流单
context.changeState(new ShippedState());
}
public void confirmReceive(OrderContext context) {
throw new RuntimeException("未发货不能确认收货");
}
}
已支付状态下,发货是合法操作,重复支付是非法操作。
五、订单上下文封装统一入口
class OrderContext {
private OrderState state;
public void pay() {
state.pay(this);
}
public void ship() {
state.ship(this);
}
public void changeState(OrderState nextState) {
this.state = nextState;
// 更新数据库状态、写状态日志
}
}
客户端不关心当前状态,状态对象负责判断当前操作是否允许。
六、真实订单系统不能只靠内存状态对象
订单状态必须落库。
比如订单表里有:
id
status
version
updated_at
状态迁移时通常要做条件更新:
update orders
set status = 'PAID', version = version + 1
where id = ? and status = 'WAIT_PAY' and version = ?
这样可以避免并发请求导致状态错乱。
七、支付回调和状态模式的关系
支付回调可能重复到达,所以状态迁移必须幂等。
如果订单已经是已支付,再收到支付成功回调,不应该重复发消息、重复扣库存。
状态类可以识别重复事件,但真实项目中还需要支付流水唯一索引、幂等表或消息去重。
状态模式负责组织状态行为,不自动解决分布式一致性。
八、支付回调下的幂等与并发
订单版本为 7,两个支付回调同时把 Pending 改 Paid;WHERE version=7 只允许 1 次更新成功,另一次影响 0 行后按已支付幂等处理。
callback x2 -> load Pending(v7) -> CAS update Paid(v8) -> one wins -> publish event once
状态题的正确性落在“当前状态 + 事件 + 守卫条件 → 目标状态”这一条边上,而不是类数量上。每条边都要说明持久化原子性、重复事件结果和副作用触发时机,非法边则必须得到稳定且可观测的拒绝。
九、迁移一致性、幂等与并发验证
| 检查维度 | 应确认的内容 |
|---|---|
| 机制正确性 | 订单状态更新和扣款结果必须围绕业务事实设计,不能仅修改 Java 对象。 |
| 适用边界 | 状态模式组织行为,数据库唯一键、乐观锁和幂等记录保证工程正确性;两层不能互相替代。 |
| 测试证据 | 覆盖正常迁移、重复事件、非法迁移、持久化失败和两个请求竞争同一版本 |
| 工程代价 | 重点评估状态数、迁移边数、一次迁移的数据库竞争,以及新增状态对完整迁移矩阵的影响 |
易错点:订单状态更新和扣款结果必须围绕业务事实设计,不能仅修改 Java 对象。
十、常见误区与追问
- 误区:状态模式能自动防止重复支付回调。 重复与并发需要幂等键、唯一约束或版本控制,模式本身不提供这些机制。
- 误区:把状态码换成状态类就自动消除了所有条件判断。 状态对象只能封装状态相关行为;持久化、并发控制、权限和跨聚合规则仍需显式设计。
- 追问:支付成功事件何时发布? 应在状态持久化成功后可靠发布;强可靠跨进程场景可用本地消息表或 Outbox。
- 追问:非法状态迁移应该静默忽略吗? 不应该;应返回明确业务错误并记录当前状态、事件和聚合标识,重复事件则按幂等规则单独处理。
- 追问:状态模式最容易漏掉哪类测试? 非法迁移、重复事件和并发竞争;它们比单线程成功路径更能证明状态模型可靠。
十一、加强记忆
订单系统使用状态模式,可以把“每个状态下能做什么”封装到状态类中,避免订单服务里堆满 if else。讲订单例子时一定要补上落库、乐观锁、幂等、状态日志和事务边界,这才是状态模式在真实业务中的完整落地。