2. 支付系统基础

在进入“支付超时异步回调与最终一致性”的实战之前,必须先建立一套对支付系统运行方式的共同语言。很多线上事故并非代码写错,而是开发者对支付流程的阶段划分、网关职责、状态机边界以及同步/异步通信的语义理解不一致。本节把后续章节反复用到的概念一次性讲清楚。

一、一笔支付到底经历了什么

从用户点击“确认支付”到商户系统确认收款,一笔支付通常跨越四个参与方:用户、商户系统、支付网关(或收单机构)、银行/清算网络。整个流程可以拆成五个阶段:

  1. 下单与预支付:商户系统创建本地订单,向支付网关发起“预下单”,拿到支付凭证(如 prepay_id、支付链接、二维码)。
  2. 用户支付:用户在收银台完成密码、指纹或跳转银行页面,资金动作在支付机构侧发生。
  3. 同步返回:支付完成后,用户浏览器或客户端被跳回商户页面,携带一个同步结果参数。
  4. 异步通知:支付网关在后台向商户配置的 notify_url 推送支付结果,这是商户系统真正可信的入账依据。
  5. 对账与结算: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…… 的退避策略投递多次,直到商户返回成功标识。因此异步通知必须满足:

  • 验签:用网关公钥验证报文未被篡改。
  • 幂等:同一笔通知可能到达多次,处理逻辑要能重复执行而不重复入账。
  • 快速应答:先落库再返回成功,避免网关因超时反复重推。
  • 可补偿:若本地处理失败,要有主动查询兜底。
常见误区:把同步返回当作支付成功依据直接发货。一旦用户支付成功但同步返回丢失,系统会漏单;若同步返回被伪造,系统会错发。正确做法是同步返回只用于页面跳转,入账一律以异步通知或主动查询为准。
支付通信路径:同步返回 vs 异步通知 用户客户端 浏览器 / App 支付网关 微信 / 支付宝 商户系统 订单 / 账务 ① 预下单 ② 同步返回(可丢失) ③ 异步通知(重试) ④ 应答 SUCCESS 同步返回特征 · 面向用户,用于页面跳转 · 可能被关页 / 断网丢失 · 参数可被伪造,需验签 · 不可作为入账依据 异步通知特征 · 面向系统,用于入账 · 网关主动重试,可达多次 · 必须幂等 + 验签 + 快速应答 · 失败时需主动查询兜底

五、场景复盘:一次漏单是怎么发生的

某电商在大促期间出现少量“用户已付款但订单仍显示待支付”。排查发现:支付成功后用户立即关闭了页面,同步返回未到达;同时商户的 notify_url 接口因数据库连接池耗尽,处理超时未返回成功标识,网关按策略重试三次后停止推送。由于系统没有主动查询兜底,这笔订单永久停留在 PAYING。

复盘结论有三点:其一,异步通知处理必须与主业务解耦,先落库再异步处理,保证快速应答;其二,必须实现定时主动查询,对 PAYING 超过阈值(如 5 分钟)的订单调用网关查询接口;其三,状态机要允许“查询到成功”这一事件把订单从 PAYING 推进到 SUCCESS,且该事件与异步通知事件共享同一套幂等逻辑。