PAYATHON 2026

支付宝交易成功后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

还需要逐项检查:

  1. 域名解析是否正确。
  2. HTTPS 证书是否有效,证书链是否完整。
  3. 服务器是否允许公网访问对应端口。
  4. 防火墙、WAF、CDN、安全组或反向代理是否拦截请求。
  5. 路由是否允许 POST 请求。
  6. 接口是否要求登录、Cookie、验证码或其他鉴权。
  7. 是否存在 HTTP 跳转 HTTPS、切换域名或补充尾斜杠等重定向。
  8. 接口能否快速响应,避免在通知线程中执行耗时任务。

可以从外部网络模拟表单请求,检查网络、路由和应用入口是否可达:

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 被访问就直接把订单标记为已支付,因为同步跳转参数可能被重复提交,浏览器链路本身也不可靠。

推荐排查顺序

按以下顺序检查,通常能较快找到问题:

  1. 打印并确认本次支付请求确实携带了正确的 notify_url。
  2. 使用全新订单重新支付,排除旧交易的干扰。
  3. 从公网测试通知 URL 的 DNS、HTTPS、端口和 POST 路由。
  4. 查看 CDN、WAF、Nginx 和应用访问日志,不要只查业务日志。
  5. 确认登录校验、CSRF 和访问控制没有拦截该路由。
  6. 检查通知参数解析、支付宝公钥、字符集和签名类型是否匹配。
  7. 确保处理成功后返回纯文本 success。
  8. 通过支付宝侧的交易记录、通知记录或开发者诊断能力核对发送情况,具体入口以当前开放平台界面为准。
  9. 调用交易查询接口确认订单状态,并建立定时补偿机制。

多数问题最终可归为两类:支付请求没有真正携带 notify_url,或者通知已经发出,却被公网链路、Web 框架安全机制或应用异常拦截。即使网关地址和授权回调地址配置正确,也不能替代这些检查。

备注:内容仅供参考。