支付成功后为什么没有收到回调?
结论
支付成功不代表回调已经送达。只看接口地址无法确定具体故障点。常见原因包括:支付请求没有正确传入异步通知地址、服务器无法从公网访问、接口收到请求后执行报错,或者接口没有按支付平台的要求返回成功响应。
如果使用支付宝,先检查支付请求中的 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';
}
如果接口已经收到并处理了通知,却没有返回平台认可的成功内容,支付平台可能继续重试。遇到这种情况,很容易误以为平台没有回调。
查看各层日志
可以按这个顺序排查:
- 支付平台的订单记录和异步通知记录。
- CDN 或 WAF 访问日志。
- Nginx 或 Apache 访问日志。
- Nginx、Apache 和 PHP 错误日志。
zfb_notify.php的入口日志。- 应用业务日志和数据库订单状态。
可以按回调路径筛选 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对应本地真实订单。- 通知金额与本地订单金额一致。
- 商户身份与当前收款方一致。
- 当前订单状态允许变更。
- 支付交易号尚未绑定到其他订单。
如果暂时无法确认异步通知的情况,可以通过支付平台的订单查询接口主动核对交易状态。不过,主动查询不能代替回调验签、金额校验和幂等处理。
备注:内容仅供参考。