PAYATHON 2026

支付成功后网站未显示回调成功,如何排查原因?

支付小周

结论

支付成功,不代表网站已经处理完异步通知。如果同一服务器上的其他网站都能正常接收回调,基本可以排除服务器整体网络故障。问题通常出在当前网站的回调地址、路由规则、应用配置、签名验证、订单匹配或业务代码中。

只看订单号 2022122322001495491452800829,还无法判断具体在哪一步失败。按格式看,它可能是支付平台交易号 trade_no,但必须结合支付接口类型、商户后台的通知记录和网站日志确认,不能直接把它当作商户订单号 out_trade_no 使用。

先确认是“没有收到回调”还是“收到后处理失败”

可以在回调入口的第一行记录原始请求,不要等签名验证通过后才写日志:

<?php

$rawBody = file_get_contents('php://input');

file_put_contents(
    __DIR__ . '/payment-notify.log',
    sprintf(
        "[%s] method=%s ip=%s content_type=%s post=%s raw=%s\n",
        date('Y-m-d H:i:s'),
        $_SERVER['REQUEST_METHOD'] ?? '',
        $_SERVER['REMOTE_ADDR'] ?? '',
        $_SERVER['CONTENT_TYPE'] ?? '',
        json_encode($_POST, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
        $rawBody
    ),
    FILE_APPEND | LOCK_EX
);

根据日志,可以把问题分为两类:

  • 完全没有请求记录:检查 notify_url、DNS、HTTPS、端口、防火墙、WAF 和路由。
  • 有请求记录,但订单状态没有更新:检查签名验证、参数解析、订单查询、金额校验、数据库事务和响应内容。

在生产环境中记录通知参数时,需要隐藏敏感字段,并限制日志文件的访问权限。

常见原因

1. notify_url 配置错误

异步通知地址必须是支付平台能从公网访问的完整 URL,例如:

https://www.example.com/payment/alipay/notify

常见配置错误有:

  • 使用相对路径或内网地址;
  • 域名仍然指向旧服务器;
  • HTTP 和 HTTPS 协议配置错误;
  • URL 指向必须登录后才能访问的页面;
  • 端口没有对公网开放;
  • 路由不存在,或被重写到其他控制器;
  • 把同步跳转地址 return_url 配成了异步通知地址。

支付完成后,浏览器跳回网站属于同步跳转,不能代替服务器异步通知。即使用户关闭了页面,订单也应该能够正常入账。

2. 回调接口被网站中间件拦截

如果只有一个网站出现问题,应先比较它与正常网站之间的应用层配置差异。例如:

  • CSRF 校验拒绝了支付平台发来的 POST 请求;
  • 登录鉴权要求回调请求携带 Session;
  • WAF、CDN 或防爬规则拦截了请求;
  • Nginx 重写规则导致接口返回 301、302 或 404;
  • PHP、Java、Node.js 路由只接受 JSON,而支付平台发送的是表单;
  • 请求体大小、字符集或参数过滤规则不兼容。

支付回调路由通常需要免除登录和 CSRF 校验,但签名验证不能省略。

可以先从服务器内部检查接口状态:

curl -i -X POST 'https://www.example.com/payment/alipay/notify' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'test=1'

这个测试只能确认地址能否访问,不能证明真实通知能够通过签名验证并完成业务处理。如果接口返回 301、302、403、404、405 或 500,需要先处理对应的路由或应用错误。

3. 签名验证失败

接入支付宝等支付平台时,应使用平台官方 SDK,并按照当前接口文档验证签名。常见问题包括:

  • 使用了商户公钥,而不是支付平台公钥;
  • 应用 app_id 和密钥不是同一套配置;
  • sign_type 配置不一致;
  • 验签前修改、过滤或重新编码了参数;
  • 混用了测试环境和正式环境的密钥;
  • 混淆了证书模式与普通公钥模式;
  • 参数中的 +、空格或中文在解析时发生变化。

不要自己拼接待验签字符串,也不能只根据来源 IP 判断通知是否可信。

4. 混淆 trade_no 和 out_trade_no

支付回调通常会同时提供:

  • trade_no:支付平台生成的交易号;
  • out_trade_no:商户系统创建的订单号。

网站应使用 out_trade_no 查询本地订单,再把 trade_no 保存为第三方交易凭据。如果代码使用 trade_no 查询本地订单表,而表中保存的是 out_trade_no,就会出现支付已经成功、订单状态却没有更新的情况。

对于给出的号码,应先确认它对应哪个字段,不能仅凭“订单编号”这个名称判断。

5. 业务校验没有通过

签名验证通过后,还要核对以下内容:

  • app_id 是否属于当前网站;
  • 收款账号或商户身份是否正确;
  • out_trade_no 是否存在;
  • 支付金额和币种是否与本地订单一致;
  • 交易状态是否表示支付成功;
  • 订单是否已经关闭、退款或被其他流程修改;
  • 当前通知是否为重复通知。

不要直接使用浮点数比较金额。可以先统一换算成最小货币单位:

<?php

$notifyAmountFen = (int) round(((float) $notifyAmount) * 100);
$orderAmountFen  = (int) $order['amount_fen'];

if ($notifyAmountFen !== $orderAmountFen) {
    throw new RuntimeException('支付金额不一致');
}

更稳妥的方式是使用十进制定点运算库,或者始终以“分”为单位存储和传递金额。

6. 回调响应不符合平台要求

业务处理成功后,接口必须按照支付平台的规定返回指定内容。以支付宝常见的异步通知协议为例,成功时通常要返回纯文本:

success

不要返回 HTML 页面、JSON、调试信息、BOM、额外空格或框架错误页。处理失败时也不能提前输出 success,否则支付平台可能停止重试。

<?php

header('Content-Type: text/plain; charset=utf-8');

try {
    // 1. 获取通知参数
    // 2. 使用官方 SDK 验签
    // 3. 核对 app_id、商户身份、订单号、金额和交易状态
    // 4. 在数据库事务中幂等更新订单
    echo 'success';
} catch (Throwable $e) {
    error_log('payment notify failed: ' . $e->getMessage());
    http_response_code(500);
    echo 'failure';
}

不同支付产品对成功响应的要求可能不同,应以实际接入产品的官方文档为准。

推荐排查顺序

  1. 在支付平台商户后台查询这笔交易,确认支付状态和异步通知发送记录。
  2. 确认号码 2022122322001495491452800829 是 trade_no 还是 out_trade_no。
  3. 核对创建支付请求时实际提交的 notify_url,不要只检查后台或代码里的默认配置。
  4. 查看域名解析、HTTPS 证书、Nginx 访问日志、CDN/WAF 日志和应用日志。
  5. 在回调入口的最前面记录请求,确认支付平台的请求是否到达。
  6. 如果请求已经到达,依次记录验签、订单查询、金额核对、状态判断和数据库更新的结果。
  7. 确认业务处理成功后,返回支付平台要求的精确响应。
  8. 修复后使用平台的通知重发功能验证。如果平台不支持重发,可以主动调用交易查询接口补单。

如果接入的是支付宝,可以使用对应支付产品支持的 alipay.trade.query 查询交易状态。查询时必须区分 trade_no 和 out_trade_no,并使用当前应用对应的正确凭据。

回调处理的正确结构

下面是一个不绑定具体 SDK 的示例。verifySignature() 必须由所接入支付平台的官方 SDK 实现,不能直接使用这个占位函数上线:

<?php

header('Content-Type: text/plain; charset=utf-8');

$params = $_POST;

try {
    if (!$officialSdk->verifySignature($params)) {
        throw new RuntimeException('签名验证失败');
    }

    $merchantOrderNo = $params['out_trade_no'] ?? '';
    $platformTradeNo = $params['trade_no'] ?? '';
    $tradeStatus     = $params['trade_status'] ?? '';
    $notifyAmountFen = amountToFen($params['total_amount'] ?? '');

    $db->beginTransaction();

    $order = findOrderForUpdate($merchantOrderNo);

    if (!$order) {
        throw new RuntimeException('本地订单不存在');
    }

    if ($order['amount_fen'] !== $notifyAmountFen) {
        throw new RuntimeException('支付金额不一致');
    }

    if ($order['status'] === 'paid') {
        $db->commit();
        echo 'success';
        exit;
    }

    if (!in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
        throw new RuntimeException('交易状态尚未成功');
    }

    markOrderAsPaid($order['id'], $platformTradeNo);
    $db->commit();

    echo 'success';
} catch (Throwable $e) {
    if ($db->inTransaction()) {
        $db->rollBack();
    }

    error_log(sprintf(
        'payment callback failed: order=%s trade=%s reason=%s',
        $params['out_trade_no'] ?? '',
        $params['trade_no'] ?? '',
        $e->getMessage()
    ));

    http_response_code(500);
    echo 'failure';
}

TRADE_SUCCESS、TRADE_FINISHED 等状态值只适用于相应的支付宝接口。接入其他支付平台时,需要替换为该平台定义的状态。

注意事项

回调处理必须具备幂等性。支付平台可能因为网络超时或没有收到正确响应,多次发送同一条通知。重复回调不能导致重复发货、重复充值或重复记账。

支付系统也不应只依赖异步通知,还需要提供主动查询和补单机制。对于长时间停留在“待支付”状态的订单,可以定时调用支付平台的查询接口,并在验签及核对订单、金额、商户身份后修正状态。这样即使某次通知因网络或程序异常丢失,已经支付的订单也不会长期停留在未支付状态。

备注:内容仅供参考。