支付宝v3支付成功后未收到回调且未跳转
结论
支付成功且查询接口能查到订单,说明交易已经创建并完成。notify_url 没有收到通知与 return_url 没有跳转属于两条不同的链路,需要分开排查:
notify_url是支付宝服务器向商户服务器发送异步通知。需要检查参数是否实际提交、地址能否从公网访问、网关是否拦截请求,以及验签和响应内容是否正确。return_url是用户完成支付后,由浏览器发起的同步跳转。这个跳转不一定会发生。用户关闭页面、支付环境限制跳转,或商户端没有正确输出支付页面,都可能导致跳转失败。
业务状态应以异步通知和主动查询结果为准,不能通过 return_url 判断支付是否成功。
先确认请求中的地址是否真正生效
代码中配置了地址,不代表支付宝实际收到了这些参数。调用 /v3/alipay/trade/page/pay 时,应记录最终发送的请求 URI、查询参数和响应,但不要记录应用私钥、Authorization 完整签名等敏感信息。
主要核对以下内容:
notify_url和return_url是否传在当前接口要求的位置。- 参数经过 URL 编码后是否仍然完整。
- SDK 配置、网关代理或请求封装是否覆盖了参数。
- 当前请求使用的
app_id是否确实为2021004170645344。 - 查询到的订单
out_trade_no是否为240907oybb5du4aoekamjt,同时核对返回的app_id、trade_no、trade_status和金额。
可以参照接口文档核对请求形式,例如:
curl -X POST \
'https://openapi.alipay.com/v3/alipay/trade/page/pay?notify_url=https%3A%2F%2Fpay.example.com%2Falipay%2Fnotify&return_url=https%3A%2F%2Fwww.example.com%2Fpay%2Fresult' \
-H 'Content-Type: application/json' \
-H 'Authorization: ALIPAY-SHA256withRSA ...' \
-d '{
"out_trade_no": "240907oybb5du4aoekamjt",
"total_amount": "实际订单金额",
"subject": "订单标题",
"product_code": "FAST_INSTANT_TRADE_PAY"
}'
这个示例只说明检查方向。具体请求路径、签名头和字段,应以当前使用的支付宝 v3 接口文档及 SDK 实际要求为准。
排查 notify_url 未收到通知
1. 检查公网可达性
通知地址必须能由支付宝服务器从公网直接访问,不能是以下地址:
localhost、127.0.0.1或内网 IP;- 只有连接公司 VPN 后才能访问的地址;
- 依赖浏览器登录状态、Cookie 或验证码的地址;
- 会被 IP 白名单、WAF、CDN 或网关鉴权拦截的地址;
- 经过多次跳转或最终跳转到登录页的地址。
可以从外部网络测试:
curl -i -X POST 'https://pay.example.com/alipay/notify' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'out_trade_no=connectivity-test'
这个测试只能检查基本连通性,无法模拟带有支付宝有效签名的通知。
还需要查看以下日志:
- CDN、WAF 和负载均衡访问日志;
- Nginx、Apache 或网关访问日志;
- 应用入口日志和异常日志;
- 防火墙、安全组和限流记录。
如果最外层访问日志中完全没有请求,先检查实际提交的 notify_url、DNS、证书、网络策略和支付宝侧通知记录。如果网关收到了请求,但应用没有收到,问题通常在路由、鉴权、中间件或转发规则。
2. 通知接口不要使用普通页面鉴权
回调接口通常需要单独放行,以免被以下机制拦截:
- 登录认证;
- CSRF 校验;
- 前端路由;
- 强制添加尾部斜杠的重定向;
- 只接受 JSON 的全局请求过滤器;
- 针对未知 User-Agent 的拦截规则。
例如,Nginx 路由可以直接转发:
location = /alipay/notify {
proxy_pass http://payment_service;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
3. 按通知的实际格式读取参数
不要默认通知一定是 JSON。应查看当前 v3 接口对应的异步通知文档,根据实际 Content-Type 读取参数,并保留原始参数用于验签。
Spring Boot 示例:
@PostMapping(
value = "/alipay/notify",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public String alipayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((name, values) -> {
params.put(name, values == null ? "" : String.join(",", values));
});
boolean valid = verifyWithAlipayOfficialSdk(params);
if (!valid) {
return "failure";
}
String appId = params.get("app_id");
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
if (!"2021004170645344".equals(appId)) {
return "failure";
}
// 从数据库读取订单,并核对订单号、金额、收款方等关键字段。
// 更新操作必须具有幂等性,避免重复通知导致重复入账。
processPaymentIdempotently(
outTradeNo,
tradeNo,
tradeStatus,
totalAmount
);
return "success";
}
verifyWithAlipayOfficialSdk 和 processPaymentIdempotently 是业务占位方法。验签必须使用与当前接入方式匹配的支付宝官方 SDK、公钥或证书配置,不能只检查订单号和交易状态。
4. 正确响应通知
验签通过并完成业务处理后,应返回接口要求的成功响应,通常是以下纯文本:
success
不要返回:
{"status":"success"}
也不要返回完整 HTML 页面、额外空格、调试信息或跳转响应。处理失败时不能提前返回 success,否则可能无法获得后续重试通知。
支付宝可能重复发送通知,因此订单更新必须支持幂等处理。例如,只在本地订单尚未支付时入账:
UPDATE payment_order
SET status = 'PAID',
alipay_trade_no = :trade_no,
paid_at = CURRENT_TIMESTAMP
WHERE out_trade_no = :out_trade_no
AND status = 'PENDING';
排查 return_url 未跳转
1. 确认浏览器真正进入了页面支付流程
调用页面支付接口后,服务端通常会收到用于拉起支付页面的响应。商户端需要把这个响应正确交给浏览器,比如输出表单页面,或让浏览器跳转到返回的支付地址。
常见错误包括:
- 后端调用接口后,只把响应封装成 JSON;
- 前端通过 Ajax 请求支付接口,却没有提交返回的表单;
- 支付页面被放进受限制的 iframe;
- 前端拦截了表单提交或重定向;
- 后端提前关闭响应,或转义了支付宝返回的 HTML。
如果接口返回的是支付表单,浏览器需要实际渲染并提交,不能只把表单当作普通字符串保存:
<div id="alipay-form"></div>
<script>
// payFormHtml 应来自商户后端。
// 插入未经信任的 HTML 会有安全风险,只能处理可信的支付接口响应。
document.getElementById('alipay-form').innerHTML = payFormHtml;
document.querySelector('#alipay-form form')?.submit();
</script>
具体处理方式取决于当前 SDK 返回的是 HTML 表单、URL 还是其他响应类型。
2. 检查 return_url 本身
return_url 应满足以下条件:
- 是浏览器可以访问的完整 URL;
- 协议、域名、端口和路径正确;
- 不会被登录状态、跨域策略或路由配置错误拦截;
- 查询参数经过正确编码;
- 页面能够处理支付宝附带的参数;
- 页面发生异常时,不会再次跳回支付页并形成循环。
可以直接在浏览器中打开该地址,检查页面是否出现 404、500、证书错误或重定向循环。
3. 不要把同步跳转当作支付凭证
即使 return_url 能正常跳转,也不能直接根据 URL 参数把订单更新为已支付。同步页面应把商户订单号提交给后端,再由后端查询支付宝交易状态:
async function loadPaymentResult(outTradeNo) {
const response = await fetch(
`/api/payment/status?out_trade_no=${encodeURIComponent(outTradeNo)}`
);
const result = await response.json();
if (result.status === 'PAID') {
location.replace('/pay/success');
} else {
location.replace('/pay/pending');
}
}
后端查询时仍需核对 app_id、订单号、金额和收款方信息。
建议的排查顺序
- 查询订单
240907oybb5du4aoekamjt,确认交易状态、支付宝交易号、金额和app_id。 - 打印脱敏后的最终支付请求,确认
notify_url、return_url已实际发送。 - 在支付宝相关控制台或开发工具中查看该笔交易的异步通知记录和失败原因。具体入口名称可能随平台版本变化。
- 按照 CDN、WAF、Nginx、应用的顺序逐层检查访问日志。
- 确认通知接口允许匿名 POST,并根据实际
Content-Type读取参数。 - 使用当前接入模式对应的官方 SDK 验签,处理成功后返回纯文本
success。 - 检查页面支付响应是否真正由浏览器打开或提交。
- 直接测试
return_url,排除页面自身的路由、证书和登录问题。
如果主动查询已经确认订单支付成功,可以在严格核对订单号、金额、app_id、收款方和交易状态后进行补单。同时仍需修复通知链路,避免后续订单只能通过人工查询处理。
备注:内容仅供参考。
