4. 支付流程编排
支付流程编排的本质,是把一次用户点击“立即支付”拆解为若干可观测、可重试、可对账的阶段,并明确每个阶段的输入、输出、责任边界与失败处理方式。很多线上事故并非支付渠道本身不可用,而是客户端、服务端、渠道回调与订单状态机之间的衔接出现了“半成功”:用户已扣款,订单仍显示待支付;或订单已发货,回调却重复触发导致重复发货。要避免这类问题,必须先把完整链路画清楚,再逐段加固。
一、链路全景:四个阶段与三个关键边界
一条典型的 Android 支付链路可以拆成四个阶段:下单(创建支付单)、调起支付(客户端唤起渠道 SDK)、支付回调(渠道异步通知服务端)、订单状态更新(状态机推进与业务履约)。其中存在三个关键边界:客户端与服务端之间的“下单边界”,客户端与渠道之间的“调起边界”,渠道与服务端之间的“回调边界”。边界处的数据一致性,决定了整条链路的可靠性。
二、下单阶段:先落库,再请求渠道
下单阶段最重要的原则是“先本地落库,再请求渠道”。客户端点击支付时,不应直接把金额、商品名传给渠道,而应先请求业务服务端创建支付单。服务端生成全局唯一的商户订单号 outTradeNo,写入支付单表,初始状态为 CREATED,同时记录金额、币种、商品描述、用户 ID、渠道类型等字段。随后服务端再调用渠道下单接口,拿到 prepayId 或 orderInfo 后回写支付单,状态推进为 PREPAY_READY。
这样做的价值在于:即使渠道下单超时或失败,本地也有一条可追溯的记录,后续可通过定时任务补偿查询。示例参数如下:
outTradeNo: PAY202506120001
amount: 19900 // 单位:分
currency: CNY
channel: WECHAT_APP
subject: 年度会员
status: CREATED → PREPAY_READY
prepayId: wx202506121030xxxx
expireTime: 2025-06-12 11:00:00
注意 outTradeNo 必须由服务端生成,不能由客户端拼接,否则容易被篡改金额或重复下单。同时应设置支付单过期时间,避免长期挂起的待支付单占用资源。
三、调起支付:客户端只做“搬运工”
客户端拿到服务端下发的支付参数后,调用渠道 SDK 唤起收银台。以微信 App 支付为例,需要传入 appId、partnerId、prepayId、packageValue、nonceStr、timeStamp、sign 七个字段。这些字段必须由服务端签名后整体下发,客户端不得自行修改任何一项,否则会触发渠道验签失败。
调起阶段有一个常见误区:把客户端 onResp 返回的“支付成功”当作最终结果直接更新订单。实际上,客户端回调只代表用户完成了收银台操作,并不代表渠道已确认扣款。正确做法是:客户端收到结果后,仅刷新 UI 为“支付处理中”,并轮询或等待服务端订单状态变化。真正的状态推进,必须由服务端接收渠道异步通知后完成。
四、支付回调:验签、幂等、状态机
渠道异步通知到达服务端后,处理顺序应为:验签 → 解析 → 幂等判断 → 状态机推进 → 返回成功应答。验签用于确认通知确实来自渠道;幂等判断用于防止渠道重复通知导致重复履约。常见做法是以 outTradeNo 为唯一键,在支付单表上加唯一索引,或使用 Redis 分布式锁 + 状态判断。
状态机推进必须严格受控。例如支付单状态只允许 PREPAY_READY → PAID → FULFILLED,不允许从 PAID 回退到 CREATED。若收到通知时订单已是 PAID,则直接返回成功应答,不再重复执行业务逻辑。若金额与本地记录不一致,应记录告警并拒绝履约,而不是盲目相信渠道金额。
// 伪代码:回调处理核心逻辑
if (!verifySign(notify)) return FAIL;
order = queryByOutTradeNo(notify.outTradeNo);
if (order == null) return FAIL;
if (order.status == PAID || order.status == FULFILLED) {
return SUCCESS; // 幂等,直接应答
}
if (order.amount != notify.amount) {
alert("amount mismatch"); return FAIL;
}
updateStatus(order, PAID);
asyncFulfill(order); // 发货、加会员、发券
return SUCCESS;
五、订单状态更新与履约
订单状态更新与支付单状态更新应分开建模。支付单关注“钱是否到账”,订单关注“业务是否完成”。支付成功后,通常通过领域事件或消息队列触发履约:发放会员权益、更新订单为已支付、通知仓储发货等。履约动作本身也要幂等,例如“发放会员”应以订单号作为幂等键,避免重复发放。
对于未收到回调的订单,需要兜底机制:定时任务扫描 PREPAY_READY 且超过一定时间的支付单,主动调用渠道查询接口。若渠道返回已支付,则补推状态;若返回未支付且已过期,则关闭支付单。这样即使回调丢失,订单最终也能达到正确状态。
六、可观测性与对账
整条链路应埋点记录:下单耗时、渠道下单结果、调起成功率、回调到达时间、验签失败次数、幂等命中次数、状态推进耗时。对账层面,每日以渠道账单与本地支付单比对,重点核对“渠道已支付但本地未 PAID”和“本地 PAID 但渠道无记录”两类差异。前者通常是回调丢失,后者可能是伪造通知或测试数据污染。
把这条链路编排清楚后,支付就不再是“调个 SDK 就完事”,而是一条具备幂等、补偿、对账能力的可靠数据流。下一阶段无论接入哪种渠道,都可以复用同一套状态机与回调处理框架。