Skip to content

订单状态机实战:代码校验 + SQL 幂等一次讲清

这篇不是“先写 SQL 再补代码”,而是从设计层面把代码层状态机SQL 幂等更新绑定在一起。

状态流转(业务真实模型)

UNPAID -> PAID -> SHIPPED -> COMPLETED
UNPAID -> CANCELED
PAID   -> REFUNDING -> REFUNDED
SHIPPED-> REFUNDING -> REFUNDED

核心目标:

  1. 代码层禁止非法流转
  2. SQL 层保证幂等与并发安全

一、代码层状态机(先校验)

java
public enum OrderStatus {
    // 未支付:只能去 PAID / CANCELED
    UNPAID(EnumSet.of(PAID, CANCELED)),
    // 已支付:只能去 SHIPPED / REFUNDING
    PAID(EnumSet.of(SHIPPED, REFUNDING)),
    // 已发货:只能去 COMPLETED / REFUNDING
    SHIPPED(EnumSet.of(COMPLETED, REFUNDING)),
    // 退款中:只能去 REFUNDED
    REFUNDING(EnumSet.of(REFUNDED)),
    // 终态
    COMPLETED(EnumSet.noneOf(OrderStatus.class)),
    CANCELED(EnumSet.noneOf(OrderStatus.class)),
    REFUNDED(EnumSet.noneOf(OrderStatus.class));

    // 允许的下一个状态集合
    private final EnumSet<OrderStatus> next;

    OrderStatus(EnumSet<OrderStatus> next) {
        this.next = next;
    }

    // 是否允许流转到目标状态
    public boolean canTransferTo(OrderStatus to) {
        return next.contains(to);
    }

    // 断言:不允许就抛异常
    public void assertCanTransferTo(OrderStatus to) {
        if (!canTransferTo(to)) {
            throw new IllegalStateException("状态不允许流转: " + this + " -> " + to);
        }
    }
}

代码层解决的是:
不允许“跳状态”(例如未支付直接发货)。


二、SQL 层幂等更新(落库安全)

示例:支付成功

sql
UPDATE orders
SET status = 'PAID', pay_time = NOW(), version = version + 1
WHERE id = ?
  AND status = 'UNPAID'
  AND version = ?;

关键点:

  • status = 'UNPAID' 防止乱序
  • version = ? 防止并发覆盖
  • 更新行数为 0 ⇒ 被并发抢先处理或已完成

三、把两者真正结合起来

下面是一个“完整可运行”的服务层写法:

java
public boolean payOrder(Long orderId, Long expectedVersion) {
    // 1) 读当前状态
    Order order = orderMapper.selectById(orderId);
    if (order == null) {
        throw new IllegalArgumentException("订单不存在");
    }

    // 2) 代码层状态机校验
    order.getStatus().assertCanTransferTo(OrderStatus.PAID);

    // 3) SQL 层幂等更新
    int rows = orderMapper.updatePaid(orderId, expectedVersion);
    return rows == 1;
}

对应 SQL(MyBatis / JPA 都适用):

sql
UPDATE orders
SET status = 'PAID', pay_time = NOW(), version = version + 1
WHERE id = ? AND status = 'UNPAID' AND version = ?;

这才是真正意义的“状态机 + SQL”结合:

  • 代码层先挡住非法流转
  • SQL 层再挡住并发与幂等问题
  • 任意一层失败,流程终止

四、其它状态流转写法(可直接复用)

取消订单(未支付才可取消)

sql
UPDATE orders
SET status = 'CANCELED', cancel_time = NOW(), version = version + 1
WHERE id = ? AND status = 'UNPAID' AND version = ?;

发货(已支付才可发货)

sql
UPDATE orders
SET status = 'SHIPPED', ship_time = NOW(), version = version + 1
WHERE id = ? AND status = 'PAID' AND version = ?;

确认收货(已发货才可完成)

sql
UPDATE orders
SET status = 'COMPLETED', finish_time = NOW(), version = version + 1
WHERE id = ? AND status = 'SHIPPED' AND version = ?;

申请退款(已支付或已发货可申请)

sql
UPDATE orders
SET status = 'REFUNDING', refund_apply_time = NOW(), version = version + 1
WHERE id = ? AND status IN ('PAID', 'SHIPPED') AND version = ?;

退款完成(退款中才可完成)

sql
UPDATE orders
SET status = 'REFUNDED', refund_time = NOW(), version = version + 1
WHERE id = ? AND status = 'REFUNDING' AND version = ?;

五、经验总结(真正在项目里好用)

  1. 状态校验写在代码层,SQL 负责幂等与并发
  2. 所有更新都必须带“当前状态”条件
  3. 高并发场景必须带 version 或乐观锁

做到这三条,订单状态机会非常稳。