PAYATHON 2026

支付宝沙箱未向notify_url发送异步回调请求

支付老李

结论

支付宝沙箱交易完成后没有请求 notify_url,一般与交易号本身无关。常见原因有:

  • 支付请求没有正确提交 notify_url;
  • 支付宝服务器无法从公网访问该地址;
  • 回调接口出现跳转或超时,或者被网关、防火墙拦截;
  • 应用、网关、密钥或支付宝公钥误用了正式环境的配置;
  • 接口收到了通知,但验签失败,或者日志记录位置不对;
  • 业务处理完成后没有返回纯文本 success,支付宝因此继续重试。

交易号 2023013022001496810501939748 只能用于查询交易状态,无法单独证明支付宝是否发起过异步通知。建议先确认交易状态,再依次检查支付请求参数、公网链路、应用日志和验签响应。

1. 确认 notify_url 是否真正传给支付宝

notify_url 是支付接口的公共请求参数。即使项目配置文件中已经填写该地址,也不代表 SDK 一定会把它发送给支付宝。

以 Java SDK 的电脑网站支付接口为例:

AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();

request.setNotifyUrl("https://pay.example.com/alipay/notify");
request.setReturnUrl("https://pay.example.com/alipay/return");

request.setBizContent("""
{
  "out_trade_no": "ORDER_20230130_001",
  "total_amount": "0.01",
  "subject": "沙箱测试订单",
  "product_code": "FAST_INSTANT_TRADE_PAY"
}
""");

String form = alipayClient.pageExecute(request).getBody();

检查最终请求,确认其中确实包含:

notify_url=https://pay.example.com/alipay/notify

return_url 和 notify_url 的用途不同:

  • return_url:支付完成后,用户浏览器跳转到该地址;
  • notify_url:支付宝服务器主动向该地址发起 HTTP 请求。

因此,浏览器能够正常跳回商户页面,并不代表异步通知已经成功发送或接收。

2. 确认地址可以从公网访问

支付宝服务器无法访问以下地址:

http://localhost:8080/alipay/notify
http://127.0.0.1/alipay/notify
http://192.168.1.10/alipay/notify

notify_url 应是公网可访问的完整地址,例如:

https://pay.example.com/alipay/notify

可以先通过非本机网络测试:

curl -i -X POST "https://pay.example.com/alipay/notify" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "test=1"

测试时需要确认:

  • 域名能否正确解析;
  • HTTPS 证书是否有效,证书链是否完整;
  • 端口是否已向公网开放;
  • 请求是否被防火墙、WAF、CDN 或安全组拦截;
  • 接口是否返回 301、302 等跳转;
  • 接口是否要求登录、Cookie、Token 或 CSRF 校验;
  • Web 服务器是否允许 POST 请求;
  • 路由是否区分末尾的 /;
  • 接口响应是否过慢。

本地开发时可以使用带有公网入口的反向代理或内网穿透服务,但生成支付请求时提交的必须是该公网地址。

3. 查询交易是否真的成功

异步通知与交易状态变化有关。用户在支付页面上完成操作,不一定表示服务端已经取得最终的成功状态。

可以通过 alipay.trade.query 查询交易号:

AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent("""
{
  "trade_no": "2023013022001496810501939748"
}
""");

AlipayTradeQueryResponse response = alipayClient.execute(request);

System.out.println(response.getCode());
System.out.println(response.getMsg());
System.out.println(response.getTradeStatus());

常见状态包括:

  • TRADE_SUCCESS:交易支付成功;
  • TRADE_FINISHED:交易结束;
  • WAIT_BUYER_PAY:等待买家付款;
  • TRADE_CLOSED:交易关闭。

查询时必须使用创建这笔沙箱交易的应用配置。如果应用 ID、沙箱账号或密钥不匹配,即使交易号正确,也可能查不到预期结果。

4. 检查沙箱与正式环境是否混用

调用沙箱时,整套配置都应来自沙箱环境,包括:

  • 沙箱应用的 app_id;
  • 沙箱网关;
  • 沙箱应用私钥;
  • 与该沙箱应用对应的支付宝公钥或支付宝证书;
  • 沙箱买家账号。

还要区分支付宝公钥和应用公钥:

  • 应用私钥用于给商户请求签名;
  • 支付宝公钥用于验证支付宝通知的签名;
  • 应用公钥需要上传给支付宝,不能用来验证支付宝回调。

使用公钥模式时,Java 客户端可以类似这样初始化:

AlipayClient alipayClient = new DefaultAlipayClient(
    "https://openapi-sandbox.dl.alipaydev.com/gateway.do",
    appId,
    merchantPrivateKey,
    "json",
    "UTF-8",
    alipayPublicKey,
    "RSA2"
);

具体的网关和密钥配置应以当前支付宝开放平台沙箱页面显示的信息为准,不要直接照搬旧项目中的配置。

5. 正确接收并验证通知

支付宝异步通知通常使用 POST 提交,内容类型为 application/x-www-form-urlencoded,不是 JSON 请求体。接收通知时,应保留全部参数,并使用支付宝公钥验签。

Spring MVC 示例:

@PostMapping(
    value = "/alipay/notify",
    produces = "text/plain;charset=UTF-8"
)
@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、订单号和金额。
        // 订单更新必须保证幂等,避免重复通知造成重复入账。
        boolean updated = updateOrderIdempotently(
            outTradeNo,
            tradeNo,
            tradeStatus,
            totalAmount
        );

        return updated ? "success" : "failure";
    } catch (Exception e) {
        // 记录异常,但不要把堆栈或 HTML 错误页返回给支付宝。
        return "failure";
    }
}

不能只验证签名。验签通过后,还需要核对:

app_id
seller_id
out_trade_no
total_amount
trade_status

订单号、金额和收款方信息必须与本地订单一致。只有业务处理成功后,接口才能返回:

success

响应体必须是纯文本,不能返回:

{"status":"success"}

也不要返回 HTML 页面、统一响应对象或带有额外字符的内容。

6. 确认请求是否在应用之外被拦截

如果业务代码中完全找不到回调日志,还需要检查入口层日志:

  • CDN 或 WAF 访问日志;
  • Nginx、Apache、Ingress 日志;
  • API 网关日志;
  • 容器和负载均衡器日志;
  • Spring Security 等认证组件日志。

例如,可以检查 Nginx 访问记录:

rg "/alipay/notify" /var/log/nginx/access.log

如果入口日志中有请求,但应用没有记录,请求可能在反向代理、鉴权中间件或路由层被拒绝。常见状态码包括 403、404、405、413 和 502。

如果入口层同样没有任何请求记录,应优先检查:

  1. 支付请求是否实际提交了 notify_url;
  2. 域名是否解析到了当前服务器;
  3. 公网和防火墙是否已经放行;
  4. 是否误用了另一套沙箱应用配置。

7. 重新创建交易进行验证

这个交易号对应一笔历史沙箱交易。不能假定沙箱数据和通知记录会永久保留,因此不适合只用这笔旧交易反复测试。

修正配置后,应使用新的 out_trade_no 创建一笔沙箱交易,同时记录:

out_trade_no
trade_no
notify_url
请求时间
网关响应
回调入口日志
验签结果
HTTP 响应状态和响应体

不要重复使用已经支付成功的商户订单号进行测试。新交易完成后,先查询交易状态,再检查公网入口日志和应用日志。这样通常可以确定问题出在哪一层。

注意事项

  • 异步通知可能重复发送,因此订单更新必须具备幂等性。
  • 不要依赖异步通知的到达顺序,也不要只处理第一次请求。
  • 回调接口中不要执行耗时任务。核心订单状态写入数据库后,应尽快返回 success。
  • 验签必须使用支付宝公钥,不能使用应用公钥。
  • 验签失败不代表支付宝没有发送请求,应先检查入口访问日志。
  • 如果交易已经成功,但始终找不到回调记录,应整理 app_id、out_trade_no、trade_no、支付时间和脱敏后的请求参数,再通过支付宝开放平台的技术支持渠道核查。

备注:内容仅供参考。