PAYATHON 2026

支付成功后为什么没有收到回调?

支付老李

结论

支付成功不代表回调已经送达。只看接口地址无法确定具体故障点。常见原因包括:支付请求没有正确传入异步通知地址、服务器无法从公网访问、接口收到请求后执行报错,或者接口没有按支付平台的要求返回成功响应。

如果使用支付宝,先检查支付请求中的 notify_url。文件名是 zfb_notify.php,不代表支付订单实际配置了这个地址。应以创建订单时提交的参数和支付宝后台的通知记录为准。

先确认回调地址是否真正生效

创建支付订单时,需要明确传入完整的 HTTPS 地址:

$bizContent = [
    'out_trade_no' => $orderNo,
    'total_amount' => $amount,
    'subject'      => $subject,
    'product_code' => 'FAST_INSTANT_TRADE_PAY',
];

$request->setBizContent(json_encode($bizContent, JSON_UNESCAPED_UNICODE));
$request->setNotifyUrl('https://chengduzhiyou.cn/zfb_notify.php');

这里需要区分两个地址:

  • notify_url 是服务器异步通知地址。
  • return_url 是用户支付后的页面跳转地址。
  • 用户成功跳转到支付完成页,只能说明同步跳转正常,不能证明异步通知已经处理成功。
  • 如果订单由第三方支付 SDK、聚合支付平台或中间服务创建,还要确认该服务有没有覆盖 notify_url。

可以在支付宝商家平台或开放平台中查询对应订单的异步通知记录。如果平台记录的通知地址不是上述地址,应先修正下单参数。

检查接口能否从公网访问

支付平台需要通过公网访问以下地址:

https://chengduzhiyou.cn/zfb_notify.php

可以从服务器外部测试接口:

curl -i -X POST 'https://chengduzhiyou.cn/zfb_notify.php' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'test=1'

检查以下项目:

  • 域名是否正确解析到当前服务器。
  • HTTPS 证书是否有效,证书链是否完整。
  • 请求是否发生 301、302 或循环跳转。
  • 接口是否返回 403、404、500、502、503 等状态码。
  • 防火墙、WAF、CDN 或安全插件是否拦截了支付平台的请求。
  • 接口是否要求登录、Cookie、验证码或 CSRF Token。
  • Web 服务器是否允许访问 PHP 文件。
  • PHP-FPM 或应用进程是否正常运行。

回调接口不能只允许浏览器访问,也不能依赖用户会话。

在业务处理之前记录原始请求

排查问题时,可以在 zfb_notify.php 的最前面记录请求时间、请求参数和原始请求体,以便判断请求是没有到达服务器,还是到达后处理失败。

<?php

$logFile = __DIR__ . '/payment_notify.log';

$logData = [
    'time'         => date('Y-m-d H:i:s'),
    'method'       => $_SERVER['REQUEST_METHOD'] ?? '',
    'content_type' => $_SERVER['CONTENT_TYPE'] ?? '',
    'post'         => $_POST,
    'raw_body'     => file_get_contents('php://input'),
];

file_put_contents(
    $logFile,
    json_encode($logData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL,
    FILE_APPEND | LOCK_EX
);

日志目录需要有写入权限,但日志文件不能放在可从公网直接下载的位置。生产环境中也不应长期记录身份证号、手机号等敏感数据。

如果日志里完全没有请求,继续检查下单参数、平台通知记录、DNS、HTTPS、CDN 和防火墙。如果日志已经记录了请求,问题通常出在验签或业务处理环节。

正确处理支付宝异步通知

下面的示例展示了基本处理顺序。SDK 类名和初始化方式取决于项目实际使用的支付宝 SDK 版本,不能假定所有项目都一样。

<?php

// 先加载项目实际使用的支付宝 SDK。
// require __DIR__ . '/vendor/autoload.php';

$params = $_POST;

if (empty($params)) {
    http_response_code(400);
    echo 'failure';
    exit;
}

try {
    // 使用项目对应 SDK 提供的验签方法。
    // 下面的 $verified 仅表示验签结果,不应固定写成 true。
    $verified = verifyAlipayNotify($params);

    if (!$verified) {
        http_response_code(400);
        echo 'failure';
        exit;
    }

    $outTradeNo = $params['out_trade_no'] ?? '';
    $tradeNo    = $params['trade_no'] ?? '';
    $tradeStatus = $params['trade_status'] ?? '';
    $totalAmount = $params['total_amount'] ?? '';
    $appId       = $params['app_id'] ?? '';

    if ($outTradeNo === '') {
        throw new RuntimeException('缺少 out_trade_no');
    }

    // 从数据库查询本地订单。
    $order = findOrderByOrderNo($outTradeNo);

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

    // 必须核对订单金额、应用 ID、商户身份等关键数据。
    if ((string) $totalAmount !== (string) $order['amount']) {
        throw new RuntimeException('订单金额不一致');
    }

    if ($appId !== EXPECTED_ALIPAY_APP_ID) {
        throw new RuntimeException('app_id 不一致');
    }

    if (in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
        // 此操作必须支持重复执行,避免重复发货或重复入账。
        markOrderPaidIdempotently($outTradeNo, $tradeNo);
    }

    // 只有验签、数据核对和必要业务处理全部成功后才能返回 success。
    echo 'success';
} catch (Throwable $e) {
    error_log('Alipay notify error: ' . $e->getMessage());

    http_response_code(500);
    echo 'failure';
}

verifyAlipayNotify()、findOrderByOrderNo() 和 markOrderPaidIdempotently() 都是示意函数,需要接入项目现有的 SDK 和数据库代码。

不要自行拼接参数实现验签。优先使用当前支付宝 SDK 提供的验签方法,并确认验签使用的是支付宝公钥,不是应用公钥或应用私钥。

检查是否正确返回 success

支付宝异步通知处理成功后,接口通常需要直接输出:

success

响应中不要附加 HTML、JSON、调试信息、空格或 PHP 警告。例如,下面的响应通常不符合要求:

{"code":200,"message":"success"}

成功响应之前也不能输出调试内容:

var_dump($_POST);
echo 'success';

可以使用输出缓冲,减少意外输出造成的影响:

<?php

ob_start();

try {
    // 验签、订单校验和业务处理。
    processPaymentNotify($_POST);

    ob_clean();
    echo 'success';
} catch (Throwable $e) {
    ob_clean();
    http_response_code(500);
    echo 'failure';
}

如果接口已经收到并处理了通知,却没有返回平台认可的成功内容,支付平台可能继续重试。遇到这种情况,很容易误以为平台没有回调。

查看各层日志

可以按这个顺序排查:

  1. 支付平台的订单记录和异步通知记录。
  2. CDN 或 WAF 访问日志。
  3. Nginx 或 Apache 访问日志。
  4. Nginx、Apache 和 PHP 错误日志。
  5. zfb_notify.php 的入口日志。
  6. 应用业务日志和数据库订单状态。

可以按回调路径筛选 Nginx 日志:

rg 'zfb_notify\.php' /var/log/nginx/access.log /var/log/nginx/error.log

根据日志判断问题所在:

  • Web 服务器日志中没有记录,说明请求没有到达源站,需要检查通知地址、DNS、CDN 和防火墙。
  • 有访问记录,但状态码是 4xx,需要检查路由、权限、安全策略和请求限制。
  • 状态码是 5xx,需要检查 PHP 错误、依赖加载、数据库连接和业务异常。
  • 状态码是 200,但平台仍在重试,需要确认响应正文是否严格为 success。
  • 接口日志存在,但订单没有更新,需要检查验签、金额校验、事务和订单状态判断。

常见代码问题

回调处理中常见的问题有:

  • 只读取 JSON,没有读取 application/x-www-form-urlencoded 格式的 $_POST。
  • 把 notify_url 错写成 return_url。
  • 验签时使用了错误的公钥。
  • 直接用浮点数比较订单金额,造成精度问题。
  • 捕获数据库异常后仍然返回 success。
  • 更新订单前没有检查原订单状态,造成重复入账或重复发货。
  • 回调依赖登录状态、Session 或浏览器 Cookie。
  • PHP 警告或调试输出污染了响应正文。
  • 业务处理时间过长,导致支付平台请求超时。
  • 回调脚本内部再次请求当前域名,造成阻塞或循环调用。

金额应使用字符串或最小货币单位比较,不建议使用浮点数:

$notifyFen = (int) bcmul((string) $totalAmount, '100', 0);
$orderFen  = (int) $order['amount_fen'];

if ($notifyFen !== $orderFen) {
    throw new RuntimeException('订单金额不一致');
}

如果环境没有安装 BCMath,可以在创建订单时统一按“分”保存和比较,避免临时进行浮点换算。

注意事项

回调接口必须具备幂等性。同一笔订单可能收到多次通知,重复通知不能造成重复发货、重复增加余额或重复生成账单。

收到 trade_status=TRADE_SUCCESS 后,不能直接更新订单。至少要完成以下校验:

  • 通知签名有效。
  • app_id 与当前应用一致。
  • out_trade_no 对应本地真实订单。
  • 通知金额与本地订单金额一致。
  • 商户身份与当前收款方一致。
  • 当前订单状态允许变更。
  • 支付交易号尚未绑定到其他订单。

如果暂时无法确认异步通知的情况,可以通过支付平台的订单查询接口主动核对交易状态。不过,主动查询不能代替回调验签、金额校验和幂等处理。

备注:内容仅供参考。