Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

支付订单与状态机

“支付业务的核心是‘绝不能丢钱,也绝不能多扣钱’。网络可能是不可靠的,但我们的系统必须是可靠的。”

面试策略: 在这部分重点体现你对分布式系统异常情况的理解。突出幂等性、重试、轮询、超时取消,以及状态机控制这五个关键武器。

一、客户端支付全链路流程

一次完整的第三方支付(如微信/支付宝)交互绝不是客户端拿着金额去请求 SDK 这么简单,它涉及三方交互:

  1. 客户端发起订单: 客户端请求自己服务器,传递商品信息。
  2. 服务端生成预支付单: 业务服务器调用微信/支付宝生成预支付交易单,将包含签名等核心信息的 PayInfo 返给客户端。
  3. 客户端拉起收银台: 客户端使用 SDK,传入 PayInfo 唤起支付 App 完成支付。
  4. 客户端获取同步结果: 支付 SDK 回调给客户端支付结果(成功/取消/失败)。此结果仅供 UI 展示参考,不能作为最终发货依据
  5. 服务端接收异步通知: 微信/支付宝服务器将真实的支付成功通知发给你的业务服务器。
  6. 客户端轮询/长连同步真实结果: 客户端主动拉取或接收服务器推送,确认支付最终状态,展示成功页。

二、订单状态机设计

复杂业务中,订单绝不能只有“成功“和“失败“。必须用严谨的状态机约束状态流转,防止非法倒流(如“已取消“的订单被发货)。

           [待支付]
          /    |   \
 (超时未付)   (支付)  (用户主动取消)
    /          |        \
[已取消]    [支付中]     [已取消]
               |
      (服务端接收回调成功)
               |
           [已支付/待发货]
               |
           [已发货] → [已完成]
  • 核心原则: 状态只能单向推进,或根据特定规则流转。客户端 UI 根据状态机的当前状态来渲染按钮(如:待支付展示“去支付“,已取消展示“重新购买“)。

三、网络异常与重试机制 (幂等性)

移动端面临弱网、断网、重切等各种网络异常。当发出“确认购买“请求,但遇到了网络超时,此时钱扣了吗?

  • 幂等性 (Idempotency): 无论接口被调用多少次,产生的业务结果应该和调用一次相同。
  • 防重点击机制:
    • 前端 UI 防止连点(按钮变灰/Debounce拦截)。
    • 核心防线在服务端: 客户端生成全局唯一的单号或携带防重 Token,服务端依赖数据库唯一索引或 Redis 分布式锁拦截重复请求。客户端重试时必须带上同样的 ID。

四、支付结果确认与轮询机制

如第一节所述,客户端 SDK 返回成功,不代表真的成功了(可能是网络劫持伪造的响应)。

  • 确认机制: 支付完成后,客户端展示“支付确认中“的加载框。
  • 轮询 (Polling):
    • 客户端定时向业务服务器发起请求查询订单真实状态(比如:延时 1s、2s、4s 递增查询,最多查询 5 次)。
  • 兜底策略: 如果轮询一直未确认,不能告诉用户“支付失败“,应该提示“结果确认中,请稍后查看订单列表“。服务端会在后续收到异步通知时更正订单状态。

五、超时取消与库存回退

  • 当订单处于“待支付“状态,业务逻辑往往已经预占了商品的库存。
  • 如果用户迟迟不付,必须有超时取消机制(例如 15 分钟未支付自动关闭),释放占用的库存。
  • 客户端倒计时: 倒计时的计算一定要以服务端返回的时间戳为基准,切勿使用客户端本地的 System.currentTimeMillis(),因为用户可以随便修改手机时间。

六、安全边界与反作弊

  • 金额不可信: 客户端绝不能自己提交 amount=100 给支付 SDK。金额必须由业务服务端通过商品 ID 和促销规则计算生成,客户端只拿组装好的签名串。
  • 拦截抓包与篡改: 防止用户抓包拦截服务端的预支付单,修改成别人的订单号或极小金额。这里结合第 20 篇提到的签名、HTTPS 双向校验和风控逻辑发挥。

高频面试题

Q1: 支付完 SDK 告诉客户端成功了,此时可以立刻更新本地状态为“已支付”并给用户发货吗? 绝对不可以。客户端环境不可控,SDK 返回的结果可能被篡改(如破解包或劫持回调)。发货的唯一凭证是业务服务器收到微信/支付宝的官方异步回调校验成功。客户端结果只用于触发向服务端查询的动作。

Q2: 弱网下用户点击“立即支付”由于没有响应,狂点了三下,怎么保证不产生三个订单?

  1. 客户端防重: 按钮点击后 disable,通过 RxBinding 或协程防抖。
  2. 状态机控制: 本地记录正在请求中,不响应二次点击。
  3. 服务端唯一防重: 客户端生成或服务端提前下发的 UUID 作为本次交易的凭据(幂等 Key),服务端收到多次相同 Key 的请求时,只处理一次,其余的直接返回旧订单信息。

Q3: 如果支付完成后回到 App,网络断了,无法轮询服务端拿到结果,应该怎么处理? 展示“订单处理中”或“网络异常,稍后请在订单列表查看”。绝不能显示“支付失败”(万一钱扣了会引发严重客诉),也绝不能显示“支付成功”(万一真没成功则产生资损)。等待网络恢复后,用户进入订单列表时再次向服务器同步最新状态。

易错点 / 追问

  • 倒计时依赖本地时间: 利用修改手机系统时间可以无限延长支付时间或卡出倒计时负数 bug。
  • 本地订单状态与服务端不一致: 客户端由于进程被杀等原因错过状态流转,重入时一定要从服务器重新拉取当前最新状态机节点。
  • 支付 SDK 冲突: 集成多方支付 SDK 导致依赖库冲突(通常通过 exclude 或使用精简版 SDK 解决)。