2. 支付系统基础
在进入“支付超时异步回调与最终一致性”的实战之前,必须先建立一套对支付系统运行方式的共同语言。很多线上事故并非代码写错,而是开发者对支付流程的阶段划分、网关职责、状态机边界以及同步/异步通信的语义理解不一致。本节把后续章节反复用到的概念一次性讲清楚。
一、一笔支付到底经历了什么
从用户点击“确认支付”到商户系统确认收款,一笔支付通常跨越四个参与方:用户、商户系统、支付网关(或收单机构)、银行/清算网络。整个流程可以拆成五个阶段:
- 下单与预支付:商户系统创建本地订单,向支付网关发起“预下单”,拿到支付凭证(如 prepay_id、支付链接、二维码)。
- 用户支付:用户在收银台完成密码、指纹或跳转银行页面,资金动作在支付机构侧发生。
- 同步返回:支付完成后,用户浏览器或客户端被跳回商户页面,携带一个同步结果参数。
- 异步通知:支付网关在后台向商户配置的 notify_url 推送支付结果,这是商户系统真正可信的入账依据。
- 对账与结算:T+1 通过账单文件核对交易,处理差错与退款。
关键认知是:同步返回是给用户看的,异步通知才是给系统用的。同步返回可能被用户关闭页面、网络中断、参数篡改而丢失,绝不能作为发货或记账的唯一依据。
二、支付网关的职责与边界
支付网关是商户与银行/支付机构之间的统一入口,它屏蔽了不同渠道的协议差异。它的核心职责包括:
- 协议适配:把商户统一的请求翻译成微信、支付宝、银联各自的报文格式。
- 签名与验签:保证请求和回调的完整性与来源可信。
- 路由与降级:按渠道、费率、可用性选择通道路由。
- 结果通知:负责异步回调的重试与投递。
- 对账文件生成:提供日终账单供商户核对。
三、交易状态机:把“状态”和“事件”分开
支付系统的核心是一张状态机。状态是系统在某一时刻的客观事实,事件是推动状态变化的外部输入。常见状态包括:
| 状态 | 含义 | 可迁移到 |
|---|---|---|
| CREATED | 本地订单已创建,未发起支付 | PAYING、CLOSED |
| PAYING | 已获取支付凭证,等待用户支付 | SUCCESS、FAILED、CLOSED |
| SUCCESS | 支付成功,资金已确认 | REFUNDING |
| FAILED | 支付失败,不可再支付 | — |
| CLOSED | 超时或主动关闭,未支付 | — |
| REFUNDING | 退款处理中 | REFUNDED |
状态机设计有三条铁律:第一,状态只能由事件驱动迁移,禁止直接 UPDATE 状态字段;第二,终态不可逆,SUCCESS 不能被后来的 FAILED 覆盖;第三,迁移必须幂等,同一事件重复到达不产生副作用。下面是一段典型的状态迁移校验逻辑:
public boolean transit(Order order, Event event) {
switch (order.getStatus()) {
case CREATED:
if (event == Event.PAY_INIT) return to(order, PAYING);
if (event == Event.TIMEOUT) return to(order, CLOSED);
break;
case PAYING:
if (event == Event.PAY_SUCCESS) return to(order, SUCCESS);
if (event == Event.PAY_FAIL) return to(order, FAILED);
if (event == Event.TIMEOUT) return to(order, CLOSED);
break;
case SUCCESS:
if (event == Event.REFUND) return to(order, REFUNDING);
break;
default:
return false; // 终态拒绝任何迁移
}
return false;
}
四、同步与异步通信的语义差异
同步通信是“请求—等待—响应”模型,调用方阻塞直到拿到结果,典型如用户支付后跳回商户页面的 return_url。异步通信是“投递—确认”模型,调用方不等待业务处理完成,典型如支付网关向 notify_url 推送结果。
两者的可靠性差异决定了系统设计:同步返回可能因用户关页、网络抖动而丢失,且参数可被伪造;异步通知由网关主动重试,通常按 15s、15s、30s、3m、10m…… 的退避策略投递多次,直到商户返回成功标识。因此异步通知必须满足:
- 验签:用网关公钥验证报文未被篡改。
- 幂等:同一笔通知可能到达多次,处理逻辑要能重复执行而不重复入账。
- 快速应答:先落库再返回成功,避免网关因超时反复重推。
- 可补偿:若本地处理失败,要有主动查询兜底。
五、场景复盘:一次漏单是怎么发生的
某电商在大促期间出现少量“用户已付款但订单仍显示待支付”。排查发现:支付成功后用户立即关闭了页面,同步返回未到达;同时商户的 notify_url 接口因数据库连接池耗尽,处理超时未返回成功标识,网关按策略重试三次后停止推送。由于系统没有主动查询兜底,这笔订单永久停留在 PAYING。
复盘结论有三点:其一,异步通知处理必须与主业务解耦,先落库再异步处理,保证快速应答;其二,必须实现定时主动查询,对 PAYING 超过阈值(如 5 分钟)的订单调用网关查询接口;其三,状态机要允许“查询到成功”这一事件把订单从 PAYING 推进到 SUCCESS,且该事件与异步通知事件共享同一套幂等逻辑。