支付成功后网站未显示回调成功,如何排查原因?
结论
支付成功,不代表网站已经处理完异步通知。如果同一服务器上的其他网站都能正常接收回调,基本可以排除服务器整体网络故障。问题通常出在当前网站的回调地址、路由规则、应用配置、签名验证、订单匹配或业务代码中。
只看订单号 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';
}
不同支付产品对成功响应的要求可能不同,应以实际接入产品的官方文档为准。
推荐排查顺序
- 在支付平台商户后台查询这笔交易,确认支付状态和异步通知发送记录。
- 确认号码
2022122322001495491452800829是trade_no还是out_trade_no。 - 核对创建支付请求时实际提交的
notify_url,不要只检查后台或代码里的默认配置。 - 查看域名解析、HTTPS 证书、Nginx 访问日志、CDN/WAF 日志和应用日志。
- 在回调入口的最前面记录请求,确认支付平台的请求是否到达。
- 如果请求已经到达,依次记录验签、订单查询、金额核对、状态判断和数据库更新的结果。
- 确认业务处理成功后,返回支付平台要求的精确响应。
- 修复后使用平台的通知重发功能验证。如果平台不支持重发,可以主动调用交易查询接口补单。
如果接入的是支付宝,可以使用对应支付产品支持的 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 等状态值只适用于相应的支付宝接口。接入其他支付平台时,需要替换为该平台定义的状态。
注意事项
回调处理必须具备幂等性。支付平台可能因为网络超时或没有收到正确响应,多次发送同一条通知。重复回调不能导致重复发货、重复充值或重复记账。
支付系统也不应只依赖异步通知,还需要提供主动查询和补单机制。对于长时间停留在“待支付”状态的订单,可以定时调用支付平台的查询接口,并在验签及核对订单、金额、商户身份后修正状态。这样即使某次通知因网络或程序异常丢失,已经支付的订单也不会长期停留在未支付状态。
备注:内容仅供参考。