商城系统对接支付是最关键的环节之一。这篇记录商城订单系统的完整设计,重点是订单状态机和与自研支付系统的集成方案。
商城对接自研支付系统
之前已经开发了独立的支付系统(聚合了微信支付和支付宝),商城作为业务方对接。集成方式是标准的API调用模式:
商城下单时调用支付系统的"创建支付单"接口,传入订单号、金额、商品描述等信息,支付系统返回支付参数(微信的prepay_id或支付宝的form表单)。用户完成支付后,支付系统通过异步回调通知商城。
几个关键设计点:
幂等性:商城用订单号作为支付系统的外部交易号。同一个订单号重复请求创建支付单,支付系统返回同一个支付单,不会重复创建。这样商城侧重试就不会出问题。
金额校验:支付回调中的金额必须和订单金额比对。虽然支付系统是自己的,但防御性编程不能省。金额不一致直接告警,人工介入。
签名验证:回调请求用RSA签名验证,防止伪造。支付系统和商城各持一对密钥。
超时处理:支付单创建后有过期时间(通常30分钟)。支付系统在过期后会关闭支付单并回调通知商城。
订单状态机设计
商城订单的完整生命周期涉及多个状态。我设计的状态流转如下:
核心状态:
- 待支付(PENDING_PAYMENT):订单创建成功,等待用户支付
- 已支付(PAID):支付成功确认
- 待发货(PENDING_SHIPMENT):等同于已支付,运营可以处理发货
- 已发货(SHIPPED):商家已发货,物流运输中
- 已完成(COMPLETED):用户确认收货,订单完结
- 已取消(CANCELLED):订单被取消(用户主动或超时)
- 退款中(REFUNDING):用户申请退款,处理中
- 已退款(REFUNDED):退款完成
状态流转规则:
正常流程:待支付 → 已支付/待发货 → 已发货 → 已完成
取消流程:
- 待支付 → 已取消(用户主动取消或超时自动取消)
退款流程:
- 已支付/待发货 → 退款中 → 已退款(未发货退款,全额退)
- 已发货 → 退款中 → 已退款(需要退货退款流程)
- 已完成 → 退款中 → 已退款(售后退款,有时间限制)
非法流转拒绝:已取消的订单不能再支付,已完成的不能再发货。状态机要严格校验。
状态机实现
状态机的核心是定义合法的状态转换表,每次状态变更都校验合法性。
实现思路:用一个map定义每个状态允许转换到哪些状态,以及转换时需要执行的前置检查和后置动作。
状态转换表的结构大致是:
- 待支付 → 可以转到:已支付(触发条件:收到支付回调)、已取消(触发条件:用户取消或超时)
- 已支付 → 可以转到:待发货(自动流转)、退款中(用户申请退款)、已取消(特殊情况,管理员操作)
- 待发货 → 可以转到:已发货(商家发货操作)、退款中(用户申请退款)
- 已发货 → 可以转到:已完成(用户确认收货或超时自动确认)、退款中(用户申请售后)
- 退款中 → 可以转到:已退款(退款成功)、原状态(退款拒绝,回退)
- 已完成、已取消、已退款 → 终态,一般不再流转
每次状态变更时的处理步骤:
- 加锁(数据库行锁或分布式锁)
- 查询当前订单状态
- 校验目标状态是否在允许的转换列表中
- 执行前置动作(如退款前检查是否已发货)
- 更新订单状态,记录状态变更日志
- 执行后置动作(如支付成功后扣减库存、发送通知)
- 释放锁
状态变更日志非常重要。每次流转都记录:订单ID、原状态、新状态、操作人(用户/系统/管理员)、操作时间、备注。出了问题可以追溯完整的状态变迁历史。
超时自动取消:延迟队列
用户下单后不支付的情况很常见,需要在超时后自动取消订单并释放库存。
方案选择:
方案一:定时扫描。每分钟扫一次数据库,找出所有超时的待支付订单。简单但有延迟(最长一个扫描周期)且数据库压力大。
方案二:延迟队列。下单时投一条延迟消息到队列,到期后触发取消逻辑。精确且无数据库压力。
我用的是方案二,基于Redis的sorted set实现简易延迟队列:
订单创建时,将订单号作为member、过期时间戳作为score放入sorted set。后台有一个消费协程不断用ZRANGEBYSCORE查询到期的订单,查到就触发取消流程。
取消流程:
- 从延迟队列移除(ZREM,原子操作,防止重复消费)
- 查询订单当前状态,如果已经支付了就跳过(支付回调可能在取消之前到达)
- 如果仍然是待支付,执行取消:更新状态、释放库存、释放优惠券
- 关闭支付系统中的支付单
竞态处理:支付回调和超时取消可能同时发生。解决方案是在状态变更时用乐观锁(UPDATE ... WHERE status = 'PENDING_PAYMENT'),只有一个操作能成功。支付回调如果发现订单已取消,需要触发退款。
另外还有一个边界情况:用户在第29分钟支付成功,支付系统回调在第31分钟才到达商城(网络延迟)。这时订单可能已经被超时取消了。处理方式是在收到支付回调时检查订单状态,如果已取消,直接调支付系统退款接口原路退回。
库存扣减策略
订单状态机和库存扣减紧密相关。两种常见策略:
下单扣减:创建订单时立即扣减库存。优点是不会超卖。缺点是用户不付款也占着库存(恶意锁库存攻击)。需要配合超时释放。
支付扣减:支付成功后才扣减库存。优点是库存利用率高。缺点是下单后可能支付时发现没库存了,体验差。
我选择的是下单扣减。理由是商城的SKU库存本身就不大,超卖带来的客服成本和用户体验损失比暂时锁定库存更严重。配合30分钟超时释放,库存锁定问题可控。
具体实现:用Redis做库存预扣减(DECR,高性能),数据库做最终扣减(在订单创建事务中)。Redis扣减成功但数据库失败时需要补偿回Redis。
关键流程串联
一次完整的下单-支付流程:
- 用户提交订单:校验商品、计算价格、锁定优惠券、Redis预扣库存
- 创建订单:数据库事务——插入订单、订单商品行、扣减数据库库存、投延迟取消消息
- 请求支付:调用支付系统创建支付单,返回支付参数给前端
- 用户支付:前端唤起微信/支付宝支付
- 支付回调:收到支付系统回调,验签、校验金额、更新订单状态为已支付
- 后续:核销优惠券、发通知、更新销量统计
任何一步失败都要有补偿机制。第1步库存不足直接拒绝。第2步事务失败释放Redis库存和优惠券。第3步支付系统异常允许重试。第5步回调失败支付系统会重发。
监控与告警
支付相关的监控必须到位:
- 订单创建到支付成功的时间分布(过长可能是支付页面有问题)
- 支付回调延迟监控(超过1分钟告警)
- 订单金额与支付金额不一致告警
- 超时取消率(过高说明流程有问题或定价问题)
- 退款率监控
- 库存扣减与释放的对账(每天跑一次对账任务)
日志要详细记录每一步的请求和响应,特别是和支付系统交互的部分。出了资金问题能快速定位。
总结
订单状态机是商城的核心骨架,设计时要把所有可能的状态流转都想清楚,特别是异常流程(超时、退款、并发竞态)。和支付系统的集成重点在幂等、校验和补偿。做好这些,支付环节就不容易出问题。