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

张开发
2026/4/6 23:37:43 15 分钟阅读

分享文章

订单状态机实战:代码校验 + SQL 幂等一次讲清
这篇不是“先写 SQL 再补代码”而是从设计层面把代码层状态机和SQL 幂等更新绑定在一起。状态流转业务真实模型UNPAID - PAID - SHIPPED - COMPLETED UNPAID - CANCELED PAID - REFUNDING - REFUNDED SHIPPED- REFUNDING - REFUNDED核心目标代码层禁止非法流转SQL 层保证幂等与并发安全一、代码层状态机先校验publicenumOrderStatus{// 未支付只能去 PAID / CANCELEDUNPAID(EnumSet.of(PAID,CANCELED)),// 已支付只能去 SHIPPED / REFUNDINGPAID(EnumSet.of(SHIPPED,REFUNDING)),// 已发货只能去 COMPLETED / REFUNDINGSHIPPED(EnumSet.of(COMPLETED,REFUNDING)),// 退款中只能去 REFUNDEDREFUNDING(EnumSet.of(REFUNDED)),// 终态COMPLETED(EnumSet.noneOf(OrderStatus.class)),CANCELED(EnumSet.noneOf(OrderStatus.class)),REFUNDED(EnumSet.noneOf(OrderStatus.class));// 允许的下一个状态集合privatefinalEnumSetOrderStatusnext;OrderStatus(EnumSetOrderStatusnext){this.nextnext;}// 是否允许流转到目标状态publicbooleancanTransferTo(OrderStatusto){returnnext.contains(to);}// 断言不允许就抛异常publicvoidassertCanTransferTo(OrderStatusto){if(!canTransferTo(to)){thrownewIllegalStateException(状态不允许流转: this - to);}}}代码层解决的是不允许“跳状态”例如未支付直接发货。二、SQL 层幂等更新落库安全示例支付成功UPDATEordersSETstatusPAID,pay_timeNOW(),versionversion1WHEREid?ANDstatusUNPAIDANDversion?;关键点status UNPAID防止乱序version ?防止并发覆盖更新行数为 0 ⇒ 被并发抢先处理或已完成三、把两者真正结合起来下面是一个“完整可运行”的服务层写法publicbooleanpayOrder(LongorderId,LongexpectedVersion){// 1) 读当前状态OrderorderorderMapper.selectById(orderId);if(ordernull){thrownewIllegalArgumentException(订单不存在);}// 2) 代码层状态机校验order.getStatus().assertCanTransferTo(OrderStatus.PAID);// 3) SQL 层幂等更新introwsorderMapper.updatePaid(orderId,expectedVersion);returnrows1;}对应 SQLMyBatis / JPA 都适用UPDATEordersSETstatusPAID,pay_timeNOW(),versionversion1WHEREid?ANDstatusUNPAIDANDversion?;这才是真正意义的“状态机 SQL”结合代码层先挡住非法流转SQL 层再挡住并发与幂等问题任意一层失败流程终止四、其它状态流转写法可直接复用取消订单未支付才可取消UPDATEordersSETstatusCANCELED,cancel_timeNOW(),versionversion1WHEREid?ANDstatusUNPAIDANDversion?;发货已支付才可发货UPDATEordersSETstatusSHIPPED,ship_timeNOW(),versionversion1WHEREid?ANDstatusPAIDANDversion?;确认收货已发货才可完成UPDATEordersSETstatusCOMPLETED,finish_timeNOW(),versionversion1WHEREid?ANDstatusSHIPPEDANDversion?;申请退款已支付或已发货可申请UPDATEordersSETstatusREFUNDING,refund_apply_timeNOW(),versionversion1WHEREid?ANDstatusIN(PAID,SHIPPED)ANDversion?;退款完成退款中才可完成UPDATEordersSETstatusREFUNDED,refund_timeNOW(),versionversion1WHEREid?ANDstatusREFUNDINGANDversion?;五、经验总结真正在项目里好用状态校验写在代码层SQL 负责幂等与并发所有更新都必须带“当前状态”条件高并发场景必须带version或乐观锁做到这三条订单状态机会非常稳。

更多文章