订单状态机实战:代码校验 + SQL 幂等一次讲清
这篇不是“先写 SQL 再补代码”,而是从设计层面把代码层状态机和SQL 幂等更新绑定在一起。
状态流转(业务真实模型)
UNPAID -> PAID -> SHIPPED -> COMPLETED
UNPAID -> CANCELED
PAID -> REFUNDING -> REFUNDED
SHIPPED-> REFUNDING -> REFUNDED核心目标:
- 代码层禁止非法流转
- 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 = ?;五、经验总结(真正在项目里好用)
- 状态校验写在代码层,SQL 负责幂等与并发
- 所有更新都必须带“当前状态”条件
- 高并发场景必须带
version或乐观锁
做到这三条,订单状态机会非常稳。