付款成功后收不到支付结果且无 IPN 日志
结论
当前参数不是 IPN(异步通知),而是支付宝支付完成后的同步跳转结果。判断依据是:
method=alipay.trade.page.pay.return
这类请求是在用户浏览器访问 return_url 时产生的,不能代替服务端异步通知,也无法通过这些参数判断支付宝异步请求的来源 IP。
如果服务器完全没有 IPN 日志,应先确认下单时是否正确传入了 notify_url,再检查该地址能否通过公网访问,以及网关、防火墙或反向代理是否在请求到达应用前将其拦截。请求来源 IP 只能通过接入层日志确认,现有参数不包含这项信息。
同步返回与异步通知的区别
支付宝网页支付通常有两个回调地址:
return_url:支付完成后跳转到商户页面,请求由用户浏览器发起。notify_url:支付宝服务器主动向商户服务端发送支付结果,也就是通常所说的 IPN。
当前数据中包含 alipay.trade.page.pay.return,表明它来自同步跳转流程。用户关闭页面、网络中断或浏览器拦截都可能导致同步跳转无法到达,因此不能将它作为订单支付成功的唯一依据。
订单状态应以以下结果为准:
notify_url收到异步通知,并且验签通过;- 商户服务端调用
alipay.trade.query主动查询订单状态。
排查步骤
1. 检查创建支付请求时的 notify_url
确认创建支付请求时已经正确设置 notify_url,而不是只配置了 return_url。
示例:
$request->setNotifyUrl('https://pay.example.com/alipay/notify');
$request->setReturnUrl('https://www.example.com/payment/return');
具体设置方式取决于项目使用的支付宝 SDK。还需要检查最终提交给支付宝的请求是否确实包含 notify_url,以及这项配置是否被其他环境的配置覆盖。
2. 验证回调地址能否从公网访问
notify_url 应满足以下条件:
- 使用可通过公网访问的域名;
- DNS 解析正确;
- HTTPS 证书有效,证书链完整;
- 不要求用户登录;
- 不跳转到其他地址;
- 不经过验证码或人机验证;
- 能接收支付宝实际使用的请求方法和表单参数;
- 能在较短时间内处理完请求,并返回纯文本
success。
可以通过外部网络测试该地址能否访问。普通测试请求只能检查网络连通性,无法模拟合法的支付宝签名。
3. 从接入层逐级检查日志
如果应用中没有相关日志,可以按照请求经过的顺序逐级检查:
- CDN 或云防护日志;
- WAF、安全组和防火墙日志;
- 负载均衡访问日志;
- Nginx、Apache 或网关日志;
- 应用访问日志和异常日志。
例如,可以在 Nginx 日志中记录来源地址、代理链和请求状态:
log_format payment_callback
'$time_iso8601 remote_addr=$remote_addr '
'xff="$http_x_forwarded_for" '
'request="$request" status=$status '
'request_time=$request_time';
location = /alipay/notify {
access_log /var/log/nginx/alipay_notify.log payment_callback;
proxy_pass http://payment_backend;
}
如果 Nginx 日志也没有记录,通常表示请求没有到达这台服务器。此时应继续检查域名解析、CDN、WAF、防火墙和 notify_url 配置。
4. 检查路由和请求处理方式
常见问题包括:
- 回调路由只允许
GET,无法接收异步通知使用的请求; - Web 框架的 CSRF 校验拒绝了回调;
- HTTP 被强制跳转到 HTTPS;
- URL 末尾的斜杠触发了
301或302跳转; - WAF 将签名中的
/、+等字符判定为异常; - 应用按 JSON 请求体读取数据,但实际收到的是表单参数;
- 回调处理发生异常,却没有单独记录异常日志。
支付回调接口通常需要绕过交互式登录和 CSRF 校验,但支付宝签名验证仍然必不可少。
5. 主动查询交易状态
对于已经付款但未收到异步通知的订单,可以使用:
out_trade_no=790781425401073664
或:
trade_no=2021122222001416101432415596
调用 alipay.trade.query 查询交易状态。只有查询结果验签成功,且应用编号、商户身份、订单号、金额等信息均与本地订单一致时,才能更新订单状态。
回调处理示例
以下示例展示了基本的处理结构。SDK 类名和初始化参数需要根据项目实际使用的支付宝 SDK 调整:
<?php
// 异步通知通常以表单参数发送
$params = $_POST;
error_log(json_encode([
'event' => 'alipay_notify_received',
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
'params' => $params,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
// 使用支付宝 SDK 验证签名
$verified = $aopClient->rsaCheckV1(
$params,
$alipayPublicKey,
$params['sign_type'] ?? 'RSA2'
);
if (!$verified) {
http_response_code(400);
echo 'failure';
exit;
}
// 还应核对 app_id、seller_id、订单号、金额和本地订单信息
$outTradeNo = $params['out_trade_no'] ?? '';
$tradeNo = $params['trade_no'] ?? '';
$tradeStatus = $params['trade_status'] ?? '';
$totalAmount = $params['total_amount'] ?? '';
// 伪代码:必须使用事务或唯一约束保证幂等
$order = findOrderByOutTradeNo($outTradeNo);
if (
!$order ||
$params['app_id'] !== EXPECTED_APP_ID ||
$params['seller_id'] !== EXPECTED_SELLER_ID ||
!amountEquals($order->amount, $totalAmount)
) {
http_response_code(400);
echo 'failure';
exit;
}
if ($tradeStatus === 'TRADE_SUCCESS' || $tradeStatus === 'TRADE_FINISHED') {
markOrderPaidIdempotently($order, $tradeNo);
}
echo 'success';
比较金额时应使用十进制定点数或最小货币单位,不要直接使用浮点数:
function amountEquals(string $expected, string $actual): bool
{
return bccomp($expected, $actual, 2) === 0;
}
如何确认请求来源 IP
来源 IP 应从 CDN、负载均衡、Web 服务器或应用日志中获取,例如:
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';
如果服务部署在可信的反向代理之后,真实地址可能记录在 X-Forwarded-For 等请求头中。这些请求头可以被客户端伪造,因此只有在确认请求经过受信任代理,且代理会覆盖相应请求头时,才能采用其中的地址。
当前提供的回调参数不包含网络层来源地址,无法据此判断请求由哪个 IP 发出。另外:
- 同步
return_url请求通常由用户浏览器发起,来源 IP 可能是用户出口、代理或 CDN 地址; - 异步
notify_url请求由支付平台服务端发起,但日志中最先出现的也可能是 CDN、WAF 或负载均衡地址; - 支付结果是否可信,应通过 RSA2 验签和业务字段校验来判断,不能只依赖 IP 白名单。
注意事项
不要根据同步跳转页面直接将订单标记为已支付。同步返回页可以显示”正在确认支付结果”,再由服务端查询订单状态。
支付宝可能重复发送异步通知,因此回调处理必须具备幂等性。同一个 out_trade_no 或 trade_no 被多次通知时,不能重复发货、充值或记账。
日志可以记录订单号、交易号、请求时间、来源地址、HTTP 状态和验签结果,但不应长期以明文保存完整签名、密钥或其他敏感信息。当前已经公开展示的完整 sign,建议在后续排查材料中进行脱敏。
备注:内容仅供参考。
