PAYATHON 2026

沙箱环境下 APP 支付成功后未收到 notify_url 回调

支付老李

结论

APP 支付成功,但服务端没有收到 notify_url 通知,通常与以下环节有关:

  1. notify_url 没有进入最终签名的支付请求;
  2. 沙箱页面中配置的地址并不是 APP 支付订单的异步通知地址;
  3. 支付平台已经发出通知,但请求被网关、WAF、HTTPS 配置或应用路由拦截;
  4. 服务端收到了通知,但验签或业务处理失败,也没有记录原始请求日志;
  5. 接口没有返回纯文本 success,平台因此判定通知失败并重试;
  6. 沙箱通知链路出现延迟或临时异常。

排查时,先通过订单查询接口确认交易状态,再核对最终请求参数和入口访问日志。客户端显示”支付成功”,并不代表异步通知一定已经发出。

先确认 notify_url 是否进入最终支付请求

APP 支付的异步通知地址应随支付请求一起提交,并参与签名。只在沙箱信息页面配置地址,通常不能代替支付请求中的 notify_url。

以支付宝 Java SDK 为例:

AlipayClient alipayClient = new DefaultAlipayClient(
        gatewayUrl,
        appId,
        privateKey,
        "json",
        "UTF-8",
        alipayPublicKey,
        "RSA2"
);

AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setNotifyUrl("https://pay.example.com/alipay/notify");

AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo("ORDER_202609130001");
model.setTotalAmount("0.01");
model.setSubject("测试订单");
model.setProductCode("QUICK_MSECURITY_PAY");

request.setBizModel(model);

AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
String orderString = response.getBody();

需要重点检查以下几点:

  • setNotifyUrl() 必须在 sdkExecute() 之前调用;
  • APP 应使用服务端生成的完整 orderString;
  • 签名完成后,不要手工追加或修改 notify_url;
  • 不要把同步跳转地址、应用网关或授权回调地址当成支付异步通知地址;
  • 如果支付参数由多个服务共同拼装,应记录最终参与签名的参数,不能只记录最初的业务对象。

可以对最终订单字符串进行脱敏记录,确认其中确实包含 notify_url,同时检查地址是否存在编码、拼接或环境替换错误。

确认交易是否真的成功

客户端 SDK 返回成功,只表示客户端支付流程得到了成功结果,不能代替服务端查单。

应调用支付平台提供的订单查询接口,根据 out_trade_no 或 trade_no 查询最终状态。支付宝场景通常使用 alipay.trade.query,重点检查:

  • trade_status 是否为 TRADE_SUCCESS 或业务接受的其他最终状态;
  • out_trade_no 是否与本地订单一致;
  • total_amount、seller_id 和 app_id 是否正确;
  • 交易是否属于当前沙箱应用及对应的沙箱账号。

如果查单结果还不是最终成功状态,暂时没有异步通知可能属于正常情况。如果查单确认交易成功,而入口层完全没有请求记录,再继续检查通知地址和沙箱链路。

从入口层判断通知是”没有发送”还是”没有处理”

不能只查看控制器中的业务日志,因为通知请求可能还没到达应用就被拒绝了。

建议按以下顺序检查:

  1. CDN、负载均衡或反向代理访问日志;
  2. WAF、防火墙及安全组拦截日志;
  3. Nginx、Apache 等 Web 服务器访问日志;
  4. 应用路由和异常日志;
  5. 验签失败及参数解析失败日志。

如果入口日志中能看到平台发出的 POST 请求,说明通知已经发送,问题出在服务端处理链路。只有当所有入口层都没有记录时,才更可能是支付请求中的 notify_url 未生效、DNS 或网络不可达,或者沙箱通知链路出现异常。

回调地址至少要满足以下条件:

  • 使用公网可访问的完整 http:// 或 https:// 地址;
  • 不能使用 localhost、127.0.0.1 或只能在内网解析的域名;
  • DNS 能从公网正常解析;
  • HTTPS 证书有效、证书链完整,且域名与证书匹配;
  • 接口允许 POST 请求;
  • 接口不依赖登录状态、Cookie、验证码或 CSRF Token;
  • 不发生 HTTP 跳转到 HTTPS、切换域名等多次重定向;
  • 网关不会根据 User-Agent、IP 地区或请求体类型误拦截;
  • 接口能处理 application/x-www-form-urlencoded 表单,而不是只能接收 JSON。

浏览器可以打开回调地址,只能说明 GET 请求大致可达,无法证明平台发送的 POST 表单也能被正确处理。

正确接收通知并验签

支付宝异步通知通常通过表单参数提交。下面是一个简化的 Spring Boot 示例:

@PostMapping(
        value = "/alipay/notify",
        consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public String handleNotify(HttpServletRequest request) {
    Map<String, String> params = new HashMap<>();

    request.getParameterMap().forEach((key, values) -> {
        if (values != null && values.length > 0) {
            params.put(key, values[0]);
        }
    });

    try {
        boolean verified = AlipaySignature.rsaCheckV1(
                params,
                alipayPublicKey,
                "UTF-8",
                "RSA2"
        );

        if (!verified) {
            log.warn("Alipay notify signature verification failed, outTradeNo={}",
                    params.get("out_trade_no"));
            return "failure";
        }

        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 appId = params.get("app_id");
        String sellerId = params.get("seller_id");

        // 必须校验订单号、金额、app_id、seller_id 等业务字段。
        // 更新订单时使用 out_trade_no 或 trade_no 做幂等控制。
        if ("TRADE_SUCCESS".equals(tradeStatus)
                || "TRADE_FINISHED".equals(tradeStatus)) {
            markOrderPaidIdempotently(
                    outTradeNo,
                    tradeNo,
                    totalAmount,
                    appId,
                    sellerId
            );
        }

        return "success";
    } catch (Exception e) {
        log.error("Failed to process Alipay notify", e);
        return "failure";
    }
}

实际项目还要注意:

  • 使用当前应用对应的支付宝公钥验签,不要误用应用公钥;
  • charset 和 sign_type 应与请求配置一致;
  • 验签前不要修改参与验签的参数值;
  • 比较金额时应使用 BigDecimal,不能使用 double;
  • 必须校验通知中的订单号、金额、收款方和应用身份;
  • 数据库更新必须保证幂等,因为异步通知可能重复发送;
  • 业务处理成功后应返回纯文本 success,不能返回 JSON、HTML 或带引号的 "success";
  • 如果处理失败,应返回 success 以外的内容,让平台按照通知机制重试。

排查框架和网关配置

常见的本地代码问题包括:

@PostMapping("/alipay/notify")
public String notify(@RequestBody NotifyRequest body) {
    // 错误风险:平台发送的可能是表单,而不是 JSON。
    return "success";
}

如果接口只接受 JSON,表单通知可能直接收到 400 或 415。还需要检查:

  • Spring Security 是否要求认证;
  • CSRF 是否拦截了该路径;
  • API 网关是否限制请求来源;
  • Controller 的返回值是否被统一包装成 JSON;
  • 全局异常处理器是否把异常转换成 200 OK,但响应体并不是 success;
  • 日志脱敏或采样规则是否让人误以为请求没有到达;
  • 回调路径是否存在大小写或尾部斜杠差异。

可以从外部网络模拟表单请求,测试完整的入口链路:

curl -i -X POST \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'out_trade_no=TEST_ORDER&trade_status=TRADE_SUCCESS' \
  'https://pay.example.com/alipay/notify'

这个请求无法通过真实签名校验是正常的。测试的重点是确认 DNS、TLS、网关、路由和表单解析是否正常,并观察服务端能否留下访问日志和验签失败日志。

建议的排查顺序

  1. 调用 alipay.trade.query,确认订单的最终状态。
  2. 检查服务端生成的最终支付请求,确认 notify_url 已在签名前设置。
  3. 确认客户端使用的是这份完整订单字符串,没有缓存旧订单,也没有切换到其他环境。
  4. 检查公网 DNS、HTTPS 证书、POST 路由和表单解析。
  5. 查看 CDN、网关、Nginx 和应用入口日志。
  6. 检查验签公钥、字符集、签名类型及业务字段校验。
  7. 确认业务处理成功后的响应体严格为 success。
  8. 如果查单已经成功、请求参数无误,而且入口层长时间没有任何通知记录,再考虑沙箱链路延迟或异常。保留 app_id、out_trade_no、trade_no、支付时间和查单结果,以便进一步核查。

生产系统不能只依赖异步通知。还应建立主动查单补偿机制:订单支付后,如果在一定时间内没有收到通知,由定时任务调用查询接口核实状态,再以幂等方式补记支付结果。即使沙箱或正式环境中的通知出现延迟,订单状态也能最终保持一致。

备注:内容仅供参考。