支付成功后未收到 notify_url 回调,如何排查?
明确结论
Postman 能访问 notify_url,只说明这个接口可以手动请求,不能证明支付平台能够正常回调。
先确认以下事项:
- 下单时是否提交了
notify_url,支付平台是否接受了这个参数。 - 回调地址是否为公网可访问的完整 URL,而不是内网地址、临时域名或受限地址。
- 服务器、防火墙或反向代理是否拦截了支付平台的请求。
- 回调接口是否支持平台使用的请求方法和数据格式。
- 接口收到通知后,是否按平台协议返回了正确的成功响应。
- 该订单在支付平台是否确实处于”支付成功”状态。
使用 GET 方法获取支付链接,通常不影响异步通知。支付跳转与 notify_url 回调是两个独立过程。需要重点检查下单时传递的回调参数和服务端的接收逻辑。
第一步:确认下单请求中实际提交了 notify_url
不要只看代码中是否定义了变量,还要记录最终发送给支付接口的完整参数。记录时应隐藏密钥、签名和用户隐私数据。
重点核对:
out_trade_no=363563A20241007220649919797
notify_url=https://pay.example.com/payment/notify
常见问题有:
- 只定义了参数变量,却没有将其加入最终请求。
- 参数名拼写错误,例如写成
notifyUrl。 - 生成签名后又修改了
notify_url。 notify_url参与签名时,使用的编码方式不符合平台要求。- 使用 SDK 时,没有将该字段设置到实际发送的请求对象中。
- 获取支付链接的接口不支持异步通知参数。
- 平台后台配置覆盖了接口传入的地址。
参数的具体名称、是否参与签名以及配置优先级,都要以所用支付平台的接口文档为准。
第二步:检查回调地址的公网可达性
支付平台的服务器需要主动访问 notify_url。以下地址通常不能用作正式的回调地址:
http://localhost/payment/notify
http://127.0.0.1/payment/notify
http://192.168.1.10/payment/notify
应使用完整的公网 HTTPS 地址:
https://pay.example.com/payment/notify
同时检查:
- 域名能否在公网正常解析。
- HTTPS 证书是否有效,证书链是否完整。
- TLS 配置是否受支付平台支持。
- 端口是否开放。
- URL 是否要求登录,或依赖 Cookie、Token、验证码。
- 是否配置了 IP 白名单、WAF、CDN 或限流规则。
- 回调地址是否出现
301、302、307等重定向。 - 路由是否区分末尾斜杠,例如
/notify和/notify/。 - 服务器是否拒绝未知
User-Agent或非浏览器请求。
使用 Postman 测试时,请求可能带有登录状态或自定义请求头,也可能来自白名单 IP。因此,Postman 请求成功不代表支付平台也能访问。
可以在一台外部服务器上发起不带认证信息的测试:
curl -i -X POST 'https://pay.example.com/payment/notify' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'out_trade_no=363563A20241007220649919797&trade_status=SUCCESS'
这些测试数据只能用于检查网络和接口行为,不能作为真实支付通知处理,因为它没有通过平台的签名验证。
第三步:在接口入口记录原始请求
许多看似”没有收到回调”的问题,其实是请求已经到达,但业务代码在验签、解析参数或更新数据库之前报错了。
应尽早在回调入口记录:
- 到达时间
- 请求方法
Content-Type- 请求头
- 原始请求体
- 查询参数
- HTTP 响应状态
- 处理结果和异常信息
不要在日志中记录商户私钥、API 密钥或完整的敏感信息。
下面是一个通用的 PHP 排查示例:
<?php
$rawBody = file_get_contents('php://input');
$log = [
'time' => date('c'),
'method' => $_SERVER['REQUEST_METHOD'] ?? '',
'content_type' => $_SERVER['CONTENT_TYPE'] ?? '',
'query' => $_GET,
'form' => $_POST,
'raw_body' => $rawBody,
];
error_log(
json_encode($log, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
// 后续必须按照支付平台文档解析参数并验证签名。
// 验签成功且业务处理完成后,再返回平台要求的成功响应。
如果平台发送的是 JSON,不能只读取 $_POST:
<?php
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);
if (!is_array($data)) {
http_response_code(400);
exit('invalid request');
}
// 按平台文档验证签名。
// 验签通过后再处理订单。
如果入口日志中完全没有记录,问题通常出在支付平台发送请求之前,或者 DNS、网络、防火墙、网关、路由等环节。如果入口已有日志,但订单状态没有更新,则要继续排查参数解析、验签和业务处理。
第四步:确认请求方法和数据格式
异步通知通常由支付平台服务器发起,常见形式如下:
POST /payment/notify
Content-Type: application/x-www-form-urlencoded
也可能是:
POST /payment/notify
Content-Type: application/json
不要根据浏览器的跳转方式判断回调方式。即使支付链接通过 GET 打开,异步通知仍可能使用 POST。
回调接口需要按照实际协议读取数据:
// application/x-www-form-urlencoded
$data = $_POST;
// application/json
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);
还要确认框架是否因 CSRF 校验而拒绝外部 POST 请求。支付通知接口通常需要排除 CSRF 校验,但支付签名验证仍然不能省略。
第五步:检查网关和应用日志
沿着请求链路逐层检查:
支付平台
→ DNS/CDN/WAF
→ Nginx 或 Apache
→ 应用框架
→ 验签逻辑
→ 数据库更新
→ 成功响应
重点查看回调时间附近的以下记录:
- CDN 或 WAF 拦截日志
- Nginx、Apache 访问日志
- Web 服务器错误日志
- 应用异常日志
- PHP-FPM、Java、Node.js 等运行时日志
- 数据库错误或事务回滚记录
常见 HTTP 状态码及其原因:
| 状态码 | 常见原因 |
|---|---|
301、302 | 回调地址发生跳转 |
400 | 参数解析失败或请求格式不匹配 |
401、403 | 登录验证、IP 白名单、WAF 或 CSRF 拦截 |
404 | 路由配置错误 |
405 | 接口不允许平台使用的请求方法 |
413 | 请求体大小受到限制 |
500 | 验签、数据库或业务代码异常 |
502、504 | 上游服务异常或处理超时 |
例如,可以在 Nginx 访问日志中按订单号检索,前提是日志确实记录了请求参数或请求体:
grep '363563A20241007220649919797' /var/log/nginx/access.log
如果访问日志不记录 POST 请求体,可根据回调时间、请求路径、来源 IP 和状态码排查。
第六步:按平台要求返回成功响应
很多支付平台会根据响应内容判断通知是否处理成功。只返回 HTTP 200 不一定够,有些平台还要求返回指定文本或 JSON。
示意代码如下:
<?php
try {
// 1. 读取原始通知
// 2. 验证签名
// 3. 核对订单信息
// 4. 幂等更新订单状态
http_response_code(200);
// 此处必须替换成支付平台文档规定的内容。
echo 'success';
} catch (Throwable $e) {
error_log($e->getMessage());
http_response_code(500);
echo 'fail';
}
success 和 fail 只是常见示例,并非所有平台都采用这一标准。响应内容、大小写和格式必须严格遵循对应平台的规定。
如果平台没有收到正确响应,通常会按照一定策略重试。重试次数和时间间隔也要以平台文档为准。
第七步:核对订单状态和通知记录
使用 out_trade_no 查询订单:
363563A20241007220649919797
需要核对:
- 商户订单号是否与支付时使用的订单号完全一致。
- 平台订单是否已经支付成功,而不是仍在处理中。
- 是否存在重复订单号。
- 下单请求是否成功创建了平台订单。
- 支付平台后台是否有异步通知记录。
- 通知记录中的 URL、发送时间、响应状态和响应内容分别是什么。
- 平台是否支持手动重发通知。
如果支付平台提供订单查询接口,应主动查询订单状态,不能只依赖异步通知来确认支付结果。
回调处理的安全示例
处理真实回调时,不能只根据 trade_status 更新订单。至少要验证:
- 通知签名是否有效。
out_trade_no是否存在并属于当前商户。- 平台商户号是否与本地配置一致。
- 支付金额和币种是否与本地订单一致。
- 当前订单状态是否允许变更。
- 重复通知是否会造成重复发货或重复记账。
通用伪代码如下:
<?php
$rawBody = file_get_contents('php://input');
$data = parseNotifyData($rawBody, $_POST);
// verifySignature 的实现必须遵循对应支付平台的签名规则。
if (!verifySignature($data)) {
http_response_code(400);
exit('invalid signature');
}
$order = findOrderByOutTradeNo($data['out_trade_no'] ?? '');
if ($order === null) {
http_response_code(404);
exit('order not found');
}
if (!amountMatches($order, $data) || !merchantMatches($data)) {
http_response_code(400);
exit('order mismatch');
}
// 使用事务或唯一约束实现幂等处理。
if (!$order->isPaid()) {
markOrderAsPaid($order, $data);
}
http_response_code(200);
// 替换为平台文档要求的成功响应。
echo 'success';
推荐的排查顺序
按以下顺序检查,通常可以较快找到问题所在:
- 在支付平台后台确认订单是否支付成功。
- 查看该订单的异步通知记录。
- 核对下单请求实际发送的
notify_url。 - 检查 Nginx、WAF 和应用日志,确认请求是否到达。
- 检查请求方法、
Content-Type、路由和 CSRF 设置。 - 排查验签、金额核对、数据库事务及异常日志。
- 确认接口返回了平台规定的成功响应。
- 修复问题后,通过平台重发通知,或主动查询订单状态并进行安全补偿。
现有信息不足以判断故障具体发生在哪一层。补充支付平台名称、下单接口、实际提交的 notify_url、平台通知记录和服务器响应状态后,可以进一步缩小排查范围。
备注:内容仅供参考。