PAYATHON 2026

支付成功但未收到回调,相同程序的另一配置可正常回调

支付老李

明确结论

支付成功只能说明支付渠道已经完成扣款,不能证明商户服务器收到了异步回调,更不能证明回调已经处理成功。

同一套程序换一组配置后可以正常回调,说明业务代码具备可用性。问题通常出在两组配置或运行环境的差异上,例如:

  • 创建订单时没有正确提交回调地址,或后台配置尚未生效
  • 商户号、应用 ID、证书、密钥与支付环境不匹配
  • 支付平台无法从公网访问回调地址
  • 签名验证失败,程序直接拒绝了请求
  • 网关、WAF、反向代理或访问控制拦截了回调
  • 回调已经到达,但业务处理失败,或日志没有完整记录
  • 程序没有按支付平台的要求返回确认响应,平台因此判定通知失败

原问题没有提供支付订单号,暂时无法核对具体交易的通知记录。排查前应先取得商户订单号、支付平台订单号、商户号和支付时间。

先比较两组配置

程序相同,排查重点应放在“能回调”和“不能回调”两组配置的差异上:

检查项重点内容
商户身份商户号、应用 ID、子商户号是否正确
运行环境正式环境与沙箱环境是否混用
回调地址URL、协议、域名、路径、端口是否一致
签名配置API 密钥、公钥、平台证书、证书序列号
接口参数创建订单时是否实际传入回调地址
网络配置DNS、HTTPS 证书、防火墙、IP 白名单
应用路由请求方法、路由前缀、网关转发规则
安全组件WAF、CSRF、鉴权中间件、限流策略

除了比较配置文件中的字段,还要确认程序运行时真正加载了什么值。常见问题包括修改配置后没有重启进程,或生产环境仍在读取环境变量中的旧值。

记录配置时必须对密钥脱敏,不要把完整私钥、API 密钥或证书内容写进日志。

确认创建订单时提交的回调地址

许多支付接口要求在创建订单时传入 notify_url、callback_url 或类似参数。即使管理后台已经配置回调地址,也不代表每笔订单都会自动使用它,仍需以对应支付平台的接口规则为准。

建议记录创建订单请求中的非敏感参数:

{
  "out_trade_no": "ORDER_202609130001",
  "amount": 100,
  "notify_url": "https://pay.example.com/api/payment/notify"
}

需要确认:

  1. 创建失败订单时是否传入了回调地址。
  2. URL 是否有拼写错误、多余空格或错误路径。
  3. 是否把测试域名提交到了正式环境。
  4. 回调地址是否被另一层配置覆盖。
  5. 是否在支付完成后才修改配置。多数情况下,已经创建的订单不会使用后来修改的地址。

如果回调地址通过字符串拼接生成,应记录最终生成的完整 URL,不能只检查基础域名配置。

检查回调地址的公网可达性

支付平台的服务器必须能直接访问回调地址。下列地址通常不能作为正式回调地址:

  • localhost
  • 127.0.0.1
  • 局域网 IP
  • 只能通过公司 VPN 访问的域名
  • 必须先在浏览器中登录才能访问的页面
  • HTTPS 证书无效或证书链不完整的地址

可以从外部网络测试接口是否可达:

curl -i -X POST 'https://pay.example.com/api/payment/notify' \
  -H 'Content-Type: application/json' \
  -d '{"test":true}'

这类测试数据通常无法通过真实签名校验,但足以检查 DNS、TLS、网关和路由是否畅通。预期结果不一定是 200,应重点确认请求有没有到达应用,并检查返回状态:

  • 404:路由或路径可能有误
  • 301、302:请求发生重定向,部分支付平台不会按预期继续请求
  • 401、403:请求被鉴权、WAF 或访问控制拦截
  • 405:请求方法不匹配
  • 413:请求体超过大小限制
  • 500、502、504:应用或上游服务异常
  • TLS 错误:证书、协议或证书链存在问题

不要只在服务器本机测试。本机可以访问,不代表支付平台所在的网络也能访问。

从入口日志判断回调停在哪一层

排查时可以沿请求链路逐层确认:

支付平台
  → DNS/CDN
  → 防火墙或 WAF
  → 负载均衡或 Nginx
  → 应用路由
  → 签名验证
  → 订单查询
  → 业务更新
  → 返回确认响应

先检查最外层的访问日志。

如果 Nginx 日志中完全没有对应请求,应优先检查:

  • 支付平台是否发起了通知
  • 回调 URL 是否正确
  • DNS 是否解析到了正确的服务器
  • 防火墙、CDN 或 WAF 是否拦截了请求
  • HTTPS 证书是否有效

如果 Nginx 有访问记录,但应用没有记录,需要检查反向代理和路由配置。例如:

location /api/payment/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

特别留意 location 和 proxy_pass 末尾斜杠的组合,因为它可能改变转发后的路径。

如果应用入口已经记录请求,但订单状态没有更新,再检查签名验证、订单匹配、金额校验、数据库事务和返回内容。

在签名验证前记录必要信息

回调处理器应在入口处生成追踪 ID,并记录请求时间、路径、请求头摘要、原始请求体摘要和处理结果。所有敏感字段都必须脱敏。

下面是一个通用的 Node.js/Express 示例。代码只用于说明排查方式,签名算法和成功响应必须按照实际支付平台的规定实现:

import crypto from "node:crypto";
import express from "express";

const app = express();

// 某些支付平台要求使用原始请求体参与验签。
app.post(
  "/api/payment/notify",
  express.raw({ type: "*/*" }),
  async (req, res) => {
    const traceId = crypto.randomUUID();
    const rawBody = req.body.toString("utf8");

    console.info("payment callback received", {
      traceId,
      path: req.path,
      contentType: req.get("content-type"),
      bodyLength: req.body.length
    });

    try {
      const signature = req.get("Payment-Signature");
      const timestamp = req.get("Payment-Timestamp");

      // verifySignature 需按实际支付平台文档实现。
      const verified = verifySignature({
        rawBody,
        signature,
        timestamp
      });

      if (!verified) {
        console.warn("payment callback signature invalid", { traceId });
        return res.status(401).send("invalid signature");
      }

      const notification = JSON.parse(rawBody);

      await processPaymentNotification(notification, traceId);

      // 返回内容应严格遵循支付平台要求。
      return res.status(200).send("success");
    } catch (error) {
      console.error("payment callback failed", {
        traceId,
        message: error instanceof Error ? error.message : String(error)
      });

      return res.status(500).send("failed");
    }
  }
);

验签时,一个常见错误是先将 JSON 解析成对象,再重新序列化后用于验签。只要字段顺序、空格或转义方式发生变化,签名就可能不一致。如果平台规定使用原始请求体验签,就必须保留收到的原始字节。

还要核对:

  • 读取的签名头名称是否正确
  • 时间戳和随机串是否参与签名
  • 使用的是平台公钥还是商户私钥
  • 证书序列号是否对应当前证书
  • 密钥是否属于当前商户号和当前环境
  • 服务器时间是否存在明显偏差

避免通用中间件误拦截

支付回调由第三方服务器发起,通常不会带有用户登录态或站点的 CSRF Token。如果回调路由经过通用鉴权或 CSRF 校验,可能会直接返回 401 或 403。

可以只对明确的回调路由豁免不适用的中间件,但仍要执行支付平台要求的签名校验:

app.use("/api", sessionAuth);
app.use("/api", csrfProtection);

// 支付回调应根据框架的路由顺序单独配置。
// 豁免登录或 CSRF 不等于信任请求,仍必须验签。

具体写法取决于所用框架。修改前先确认中间件的执行顺序,避免整个支付模块都变成无需验证的接口。

检查业务处理与幂等性

支付平台可能重复发送同一条通知,因此回调处理必须支持幂等。订单已经处于“已支付”状态时,不应抛出异常,也不能重复发货或重复增加余额。

常见的处理流程如下:

BEGIN;

SELECT status, amount
FROM payment_orders
WHERE out_trade_no = ?
FOR UPDATE;

-- 校验商户订单号、支付金额、币种、商户号和支付状态。

UPDATE payment_orders
SET status = 'PAID',
    paid_at = ?,
    platform_trade_no = ?
WHERE out_trade_no = ?
  AND status = 'PENDING';

COMMIT;

实际实现还应满足以下要求:

  • 找不到订单时记录具体原因,不要静默忽略
  • 校验通知金额和本地订单金额
  • 校验币种、商户号和应用 ID
  • 只在平台状态明确表示支付成功时更新订单
  • 为发货、积分、通知等动作设置唯一约束或幂等键
  • 数据库提交成功后,再返回平台要求的成功响应

如果业务已经处理成功,但程序随后返回 500,平台可能继续重试。相反,如果业务还没完成就提前返回成功,平台可能停止通知,导致订单状态长期不一致。

通过支付平台后台核对通知记录

如果支付平台提供商户后台、日志查询或通知重发功能,可以使用以下信息查询:

  • 商户订单号
  • 支付平台订单号
  • 商户号或应用 ID
  • 支付时间
  • 支付金额

查询时重点检查:

  • 是否生成过异步通知
  • 实际通知 URL 是什么
  • 最近一次通知时间
  • 重试次数
  • 商户服务器返回的 HTTP 状态码
  • 平台记录的响应内容
  • 是否支持手动重发

没有订单号时,很难判断究竟是平台没有发送通知、通知发到了错误地址,还是商户处理失败。因此,当前问题首先要补充订单标识。如果只能取得商户订单号,也可以结合支付时间和金额缩小查询范围。

必要时主动查询订单状态

异步通知不应成为确认支付结果的唯一方式。对于长时间停留在“待支付”状态的订单,可以调用支付平台的订单查询 API 主动核实。

补偿逻辑可以按以下步骤处理:

  1. 订单超过合理等待时间后进入查询队列。
  2. 使用商户订单号或平台订单号查询。
  3. 校验查询结果中的商户号、金额、币种和交易状态。
  4. 按照与回调相同的幂等流程更新订单。
  5. 记录状态来自“异步通知”还是“主动查询”。

不要根据前端跳转页面或客户端返回值直接把订单改为已支付。前端结果可能被伪造,也可能因网络中断而丢失。

推荐排查顺序

  1. 找到问题订单的商户订单号和平台订单号。
  2. 在支付平台后台确认支付状态和通知记录。
  3. 核对创建该订单时实际提交的回调 URL。
  4. 比较正常配置与异常配置中的商户号、环境、证书和密钥。
  5. 检查 DNS、HTTPS、WAF、Nginx 和应用入口日志。
  6. 确认请求是否被鉴权、CSRF 或请求方法限制拦截。
  7. 检查原始请求体验签、证书序列号和服务器时间。
  8. 检查订单匹配、金额校验、数据库事务和幂等处理。
  9. 确认返回状态码和响应内容符合支付平台的要求。
  10. 对未收到通知的订单使用订单查询 API 补偿。

缺少订单号和具体支付平台信息时,无法确定究竟是哪项配置出了问题。排查中最有价值的证据是平台的通知记录、服务器入口访问日志,以及创建订单时最终提交的回调地址。

备注:内容仅供参考。