支付宝交易成功后notify_url未调用但return_url已调用
明确结论
return_url 能正常访问,只能说明支付宝完成了浏览器同步跳转,不能证明服务器异步通知 notify_url 配置正确。
排查时通常不必先盯着网关地址或授权回调地址,而应检查:
- 创建支付请求时是否真正传入了
notify_url notify_url能否从公网直接访问- 接口是否被登录校验、CSRF、防火墙或网关规则拦截
- 请求是否遇到重定向、TLS 证书异常或响应超时
- 应用是否收到了请求,却在验签、解析或业务处理阶段报错
- 当前支付方式或接口产品是否支持异步通知配置
授权回调地址主要用于 OAuth 授权后的回跳,通常不是支付异步通知地址。
return_url 与 notify_url 的区别
return_url 是同步跳转地址。用户付款后,支付宝通过浏览器将用户带回商户页面。这个过程依赖用户浏览器,页面可能被关闭、刷新或重复访问,因此不能作为最终入账依据。
notify_url 是支付宝服务器主动请求商户服务器的异步通知地址。订单状态更新、发货、开通服务等核心业务,应以异步通知验签成功后的交易状态为准,同时用主动查询接口兜底。
两个地址相互独立,因此可能出现以下情况:
return_url已调用,但notify_url未调用notify_url已调用,但用户关闭了页面,导致return_url未调用
首先检查支付请求参数
确认创建支付请求时传入了 notify_url。仅在本地配置文件、开放平台授权回调页面或网关设置中配置是不够的。
以支付宝开放平台常见的 Java SDK 调用方式为例:
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl("https://www.example.com/alipay/return");
request.setNotifyUrl("https://www.example.com/alipay/notify");
request.setBizContent("""
{
"out_trade_no": "ORDER_202609090001",
"total_amount": "99.00",
"subject": "测试订单",
"product_code": "FAST_INSTANT_TRADE_PAY"
}
""");
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
检查时要看最终发往支付宝的请求,不能只看代码里的配置值。常见问题有:
- 配置读取失败,最终得到空字符串
- 测试环境和生产环境使用了不同配置
- SDK 请求对象没有调用
setNotifyUrl - 封装层覆盖或丢弃了
notify_url - 把
return_url误当成notify_url - 修改配置后仍使用旧订单测试,没有重新创建交易
可以记录支付请求中的 out_trade_no、接口名称和最终使用的通知域名,但不要把应用私钥、用户信息等敏感数据写入日志。
检查通知地址是否真正可访问
notify_url 必须是支付宝服务器可以直接访问的公网地址,例如:
https://pay.example.com/alipay/notify
以下地址通常不能作为正式通知地址:
http://localhost:8080/alipay/notify
http://127.0.0.1/alipay/notify
http://192.168.1.10/alipay/notify
还需要逐项检查:
- 域名解析是否正确。
- HTTPS 证书是否有效,证书链是否完整。
- 服务器是否允许公网访问对应端口。
- 防火墙、WAF、CDN、安全组或反向代理是否拦截请求。
- 路由是否允许
POST请求。 - 接口是否要求登录、Cookie、验证码或其他鉴权。
- 是否存在 HTTP 跳转 HTTPS、切换域名或补充尾斜杠等重定向。
- 接口能否快速响应,避免在通知线程中执行耗时任务。
可以从外部网络模拟表单请求,检查网络、路由和应用入口是否可达:
curl -i -X POST 'https://pay.example.com/alipay/notify' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'out_trade_no=diagnostic_test&trade_status=TRADE_SUCCESS'
这条命令只能测试访问链路,不能模拟合法的支付宝通知,因为请求中没有有效签名,业务代码应当拒绝它。
检查服务端是否收到请求但处理失败
业务状态没有更新,并不一定表示支付宝没有调用通知地址。请求可能已经到达 Nginx、网关或应用,只是在进入业务逻辑前失败了。
建议按以下链路查看日志:
CDN/WAF → 负载均衡 → Nginx/Apache → 应用框架 → 验签逻辑 → 订单更新
重点检查:
- HTTP 状态码是否为
404、403、405、500或502 - 请求是否被 CSRF 中间件拒绝
- 是否因请求体大小、编码或参数解析失败而报错
- 是否误把通知当作 JSON 解析
- 是否在验签前修改、过滤或重新编码了参数
- 数据库事务是否发生回滚
- 接口是否已经处理成功,却没有按要求返回成功标识
支付宝异步通知通常使用 application/x-www-form-urlencoded 提交,不应默认按 JSON 请求体处理。
正确验签并返回 success
下面用 Java Servlet 风格代码说明基本流程。SDK 版本不同,具体类名和方法可能略有差异,应以项目实际使用的支付宝 SDK 为准。
@PostMapping(
value = "/alipay/notify",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
@ResponseBody
public String notify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> {
params.put(key, String.join(",", values));
});
try {
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
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");
String sellerId = params.get("seller_id");
// 除验签外,还应校验 app_id、seller_id、订单号和金额等业务字段。
// 订单更新必须具备幂等性,避免重复通知造成重复入账。
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
orderService.markPaidIdempotently(
outTradeNo,
tradeNo,
totalAmount,
appId,
sellerId
);
}
return "success";
} catch (Exception e) {
logger.error("Failed to process Alipay notification", e);
return "failure";
}
}
处理成功后,应返回以下纯文本:
success
不要返回 HTML、JSON、页面模板,也不要附加其他字符,例如:
{"status":"success"}
处理失败或验签失败时,不应返回成功。支付宝是否重试以及重试次数和间隔,可能因产品和接口规则而异,应以当前接入产品的官方文档为准。
验签成功后仍要校验业务字段
签名有效只说明通知内容通过了签名验证,并不能证明这笔交易一定属于当前订单。至少还要核对:
app_id是否属于当前应用seller_id是否属于预期收款账号out_trade_no是否存在total_amount是否与订单应付金额一致- 订单当前状态是否允许变更
trade_status是否为业务认可的成功状态
比较金额时应使用精确十进制类型,例如 Java 的 BigDecimal,不要使用 double:
BigDecimal notifiedAmount = new BigDecimal(params.get("total_amount"));
BigDecimal expectedAmount = order.getPayableAmount();
if (notifiedAmount.compareTo(expectedAmount) != 0) {
throw new IllegalStateException("Payment amount mismatch");
}
保证通知处理幂等
异步通知可能重复到达,因此接口不能假设通知只会调用一次。可以通过订单状态判断、数据库唯一约束或支付流水唯一索引实现幂等。
示例逻辑:
@Transactional
public void markPaidIdempotently(
String outTradeNo,
String tradeNo,
String totalAmount,
String appId,
String sellerId) {
Order order = orderRepository.lockByOrderNo(outTradeNo);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
if (order.isPaid()) {
return;
}
validatePayment(order, totalAmount, appId, sellerId);
order.markPaid(tradeNo);
orderRepository.save(order);
}
通知入口应尽快完成验签和必要的状态落库。发送邮件、生成报表、调用其他慢服务等操作,更适合在事务提交后通过消息队列或后台任务执行。
使用主动查询作为兜底
如果日志确认完全没有收到通知,可以调用对应产品的交易查询接口,根据 out_trade_no 或 trade_no 查询真实交易状态。例如常见交易接口中的:
alipay.trade.query
查询结果确认支付成功后,应按同一套订单校验和幂等规则补偿订单状态。不能因为 return_url 被访问就直接把订单标记为已支付,因为同步跳转参数可能被重复提交,浏览器链路本身也不可靠。
推荐排查顺序
按以下顺序检查,通常能较快找到问题:
- 打印并确认本次支付请求确实携带了正确的
notify_url。 - 使用全新订单重新支付,排除旧交易的干扰。
- 从公网测试通知 URL 的 DNS、HTTPS、端口和
POST路由。 - 查看 CDN、WAF、Nginx 和应用访问日志,不要只查业务日志。
- 确认登录校验、CSRF 和访问控制没有拦截该路由。
- 检查通知参数解析、支付宝公钥、字符集和签名类型是否匹配。
- 确保处理成功后返回纯文本
success。 - 通过支付宝侧的交易记录、通知记录或开发者诊断能力核对发送情况,具体入口以当前开放平台界面为准。
- 调用交易查询接口确认订单状态,并建立定时补偿机制。
多数问题最终可归为两类:支付请求没有真正携带 notify_url,或者通知已经发出,却被公网链路、Web 框架安全机制或应用异常拦截。即使网关地址和授权回调地址配置正确,也不能替代这些检查。
备注:内容仅供参考。
