← 返回题目列表

状态模式在订单系统中怎么应用?

高频 中等 第 10 / 25 题 更新于 2026/07/28
状态模式订单状态业务建模状态流转

简化版

订单系统中,状态模式可以把待支付、已支付、已发货、已完成、已取消等状态分别封装成状态类。每个状态类定义当前状态下允许哪些操作,以及操作后迁移到哪个状态,从而避免订单服务里堆满状态判断。

详细版

订单是状态模式的经典场景,因为订单行为强依赖状态。

比如:

  • 待支付:可以支付、取消;
  • 已支付:可以发货、退款;
  • 已发货:可以确认收货;
  • 已完成:可以评价;
  • 已取消:不能支付、不能发货。

如果用状态模式,可以定义 OrderState 接口,再实现 WaitPayStatePaidStateShippedStateFinishedStateCanceledState

订单上下文 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。讲订单例子时一定要补上落库、乐观锁、幂等、状态日志和事务边界,这才是状态模式在真实业务中的完整落地。