商城系统开发(七):支付集成与订单状态机

商城系统对接支付是最关键的环节之一。这篇记录商城订单系统的完整设计,重点是订单状态机和与自研支付系统的集成方案。

商城对接自研支付系统

之前已经开发了独立的支付系统(聚合了微信支付和支付宝),商城作为业务方对接。集成方式是标准的API调用模式:

商城下单时调用支付系统的"创建支付单"接口,传入订单号、金额、商品描述等信息,支付系统返回支付参数(微信的prepay_id或支付宝的form表单)。用户完成支付后,支付系统通过异步回调通知商城。

几个关键设计点:

幂等性:商城用订单号作为支付系统的外部交易号。同一个订单号重复请求创建支付单,支付系统返回同一个支付单,不会重复创建。这样商城侧重试就不会出问题。

金额校验:支付回调中的金额必须和订单金额比对。虽然支付系统是自己的,但防御性编程不能省。金额不一致直接告警,人工介入。

签名验证:回调请求用RSA签名验证,防止伪造。支付系统和商城各持一对密钥。

超时处理:支付单创建后有过期时间(通常30分钟)。支付系统在过期后会关闭支付单并回调通知商城。

订单状态机设计

商城订单的完整生命周期涉及多个状态。我设计的状态流转如下:

核心状态:

  • 待支付(PENDING_PAYMENT):订单创建成功,等待用户支付
  • 已支付(PAID):支付成功确认
  • 待发货(PENDING_SHIPMENT):等同于已支付,运营可以处理发货
  • 已发货(SHIPPED):商家已发货,物流运输中
  • 已完成(COMPLETED):用户确认收货,订单完结
  • 已取消(CANCELLED):订单被取消(用户主动或超时)
  • 退款中(REFUNDING):用户申请退款,处理中
  • 已退款(REFUNDED):退款完成

状态流转规则:

正常流程:待支付 → 已支付/待发货 → 已发货 → 已完成

取消流程:

  • 待支付 → 已取消(用户主动取消或超时自动取消)

退款流程:

  • 已支付/待发货 → 退款中 → 已退款(未发货退款,全额退)
  • 已发货 → 退款中 → 已退款(需要退货退款流程)
  • 已完成 → 退款中 → 已退款(售后退款,有时间限制)

非法流转拒绝:已取消的订单不能再支付,已完成的不能再发货。状态机要严格校验。

状态机实现

状态机的核心是定义合法的状态转换表,每次状态变更都校验合法性。

实现思路:用一个map定义每个状态允许转换到哪些状态,以及转换时需要执行的前置检查和后置动作。

状态转换表的结构大致是:

  • 待支付 → 可以转到:已支付(触发条件:收到支付回调)、已取消(触发条件:用户取消或超时)
  • 已支付 → 可以转到:待发货(自动流转)、退款中(用户申请退款)、已取消(特殊情况,管理员操作)
  • 待发货 → 可以转到:已发货(商家发货操作)、退款中(用户申请退款)
  • 已发货 → 可以转到:已完成(用户确认收货或超时自动确认)、退款中(用户申请售后)
  • 退款中 → 可以转到:已退款(退款成功)、原状态(退款拒绝,回退)
  • 已完成、已取消、已退款 → 终态,一般不再流转

每次状态变更时的处理步骤:

  1. 加锁(数据库行锁或分布式锁)
  2. 查询当前订单状态
  3. 校验目标状态是否在允许的转换列表中
  4. 执行前置动作(如退款前检查是否已发货)
  5. 更新订单状态,记录状态变更日志
  6. 执行后置动作(如支付成功后扣减库存、发送通知)
  7. 释放锁

状态变更日志非常重要。每次流转都记录:订单ID、原状态、新状态、操作人(用户/系统/管理员)、操作时间、备注。出了问题可以追溯完整的状态变迁历史。

超时自动取消:延迟队列

用户下单后不支付的情况很常见,需要在超时后自动取消订单并释放库存。

方案选择:

方案一:定时扫描。每分钟扫一次数据库,找出所有超时的待支付订单。简单但有延迟(最长一个扫描周期)且数据库压力大。

方案二:延迟队列。下单时投一条延迟消息到队列,到期后触发取消逻辑。精确且无数据库压力。

我用的是方案二,基于Redis的sorted set实现简易延迟队列:

订单创建时,将订单号作为member、过期时间戳作为score放入sorted set。后台有一个消费协程不断用ZRANGEBYSCORE查询到期的订单,查到就触发取消流程。

取消流程:

  1. 从延迟队列移除(ZREM,原子操作,防止重复消费)
  2. 查询订单当前状态,如果已经支付了就跳过(支付回调可能在取消之前到达)
  3. 如果仍然是待支付,执行取消:更新状态、释放库存、释放优惠券
  4. 关闭支付系统中的支付单

竞态处理:支付回调和超时取消可能同时发生。解决方案是在状态变更时用乐观锁(UPDATE ... WHERE status = 'PENDING_PAYMENT'),只有一个操作能成功。支付回调如果发现订单已取消,需要触发退款。

另外还有一个边界情况:用户在第29分钟支付成功,支付系统回调在第31分钟才到达商城(网络延迟)。这时订单可能已经被超时取消了。处理方式是在收到支付回调时检查订单状态,如果已取消,直接调支付系统退款接口原路退回。

库存扣减策略

订单状态机和库存扣减紧密相关。两种常见策略:

下单扣减:创建订单时立即扣减库存。优点是不会超卖。缺点是用户不付款也占着库存(恶意锁库存攻击)。需要配合超时释放。

支付扣减:支付成功后才扣减库存。优点是库存利用率高。缺点是下单后可能支付时发现没库存了,体验差。

我选择的是下单扣减。理由是商城的SKU库存本身就不大,超卖带来的客服成本和用户体验损失比暂时锁定库存更严重。配合30分钟超时释放,库存锁定问题可控。

具体实现:用Redis做库存预扣减(DECR,高性能),数据库做最终扣减(在订单创建事务中)。Redis扣减成功但数据库失败时需要补偿回Redis。

关键流程串联

一次完整的下单-支付流程:

  1. 用户提交订单:校验商品、计算价格、锁定优惠券、Redis预扣库存
  2. 创建订单:数据库事务——插入订单、订单商品行、扣减数据库库存、投延迟取消消息
  3. 请求支付:调用支付系统创建支付单,返回支付参数给前端
  4. 用户支付:前端唤起微信/支付宝支付
  5. 支付回调:收到支付系统回调,验签、校验金额、更新订单状态为已支付
  6. 后续:核销优惠券、发通知、更新销量统计

任何一步失败都要有补偿机制。第1步库存不足直接拒绝。第2步事务失败释放Redis库存和优惠券。第3步支付系统异常允许重试。第5步回调失败支付系统会重发。

监控与告警

支付相关的监控必须到位:

  • 订单创建到支付成功的时间分布(过长可能是支付页面有问题)
  • 支付回调延迟监控(超过1分钟告警)
  • 订单金额与支付金额不一致告警
  • 超时取消率(过高说明流程有问题或定价问题)
  • 退款率监控
  • 库存扣减与释放的对账(每天跑一次对账任务)

日志要详细记录每一步的请求和响应,特别是和支付系统交互的部分。出了资金问题能快速定位。

总结

订单状态机是商城的核心骨架,设计时要把所有可能的状态流转都想清楚,特别是异常流程(超时、退款、并发竞态)。和支付系统的集成重点在幂等、校验和补偿。做好这些,支付环节就不容易出问题。