PAYATHON 2026

沙箱环境 APP 支付使用 https + 域名后收不到异步通知

支付老李

结论

https + 域名通常不会阻止 APP 支付异步通知。Apifox 可以访问、nginx 有日志,只能说明回调接口在当前网络环境下可用,不能证明支付平台拿到了正确的 notify_url,也不能证明支付平台的服务器可以正常完成 DNS 解析、TLS 握手和 HTTP 请求。

建议先确认以下事项:

  1. 创建 APP 支付订单时,是否实际传入了 notify_url。
  2. APP 是否直接使用服务端返回的完整支付参数,而不是自行拼装。
  3. 回调域名是否能从公网访问,DNS、证书链和 443 端口是否正常。
  4. nginx 日志中是否只有 Apifox 请求,没有支付平台发起的请求。
  5. 回调接口是否接受 POST 表单请求,并在验签成功后返回纯文本 success。
  6. 支付交易是否确实成功。异步通知不是判断支付结果的唯一方式,必要时还要主动查询交易状态。

第一步:确认下单请求携带了 notify_url

异步通知地址通常要在服务端创建支付请求时设置。它不是业务参数 JSON 中的普通字段,也不能只配置在 APP 客户端。

以支付宝 Java SDK 的常见写法为例:

AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();

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

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

request.setBizModel(model);

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

重点检查以下内容:

  • request.setNotifyUrl(...) 是否确实执行。
  • notify_url 是否是包含协议、域名和路径的完整 URL。
  • 是否误把通知地址写进了 biz_content。
  • APP 发起支付时,使用的是否就是 sdkExecute 返回的完整 orderString。
  • 当前下单请求是否发往沙箱网关,并使用沙箱应用对应的 app_id 和密钥。
  • 是否存在多套配置,导致测试环境下单时读到了空地址或其他环境的地址。

建议在服务端记录脱敏后的下单参数,确认最终待签名参数中确实包含:

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

不要只看配置文件。配置存在,不代表最终请求真的使用了它。

第二步:区分“没有通知”和“收到后处理失败”

查看 nginx 日志时,要结合支付完成时间、请求路径和请求来源进行检查。

如果日志中只有 Apifox 的访问记录,没有支付平台的请求,问题通常发生在请求到达应用之前。此时应检查:

  • 下单时没有传入 notify_url。
  • 沙箱订单并未真正支付成功。
  • 域名无法从公网解析。
  • HTTPS 证书链不完整或证书已经过期。
  • 服务只对内网、VPN、白名单或 IPv6 环境开放。
  • CDN、WAF、安全组或防火墙拦截了请求。
  • 回调地址发生跳转,例如 HTTP 跳转到 HTTPS、域名跳转或路径自动补 /。
  • 使用了非标准端口,外部网络无法访问。

如果 nginx 已经记录到支付平台发出的 POST 请求,但业务系统仍显示“没有回调”,则要继续检查:

  • nginx 是否正确转发了请求体。
  • 应用路由是否匹配。
  • 请求是否被登录鉴权、CSRF、验证码或网关权限拦截。
  • 程序是否错误地按 JSON 解析请求。
  • 验签是否失败。
  • 程序是否发生异常,或者没有返回 success。

可以为回调地址单独配置访问日志:

location = /alipay/notify {
    access_log /var/log/nginx/alipay_notify_access.log combined;

    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

排查期间应记录请求是否到达,但不要长期将完整通知参数、签名、用户信息或敏感订单数据写入公开日志。

第三步:检查公网和 HTTPS 配置

本机或 Apifox 请求成功,并不能说明支付平台服务器也具备相同的访问条件。应从另一台公网机器测试:

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

检查 DNS:

dig pay.example.com A
dig pay.example.com AAAA

检查 TLS 证书链:

openssl s_client \
  -connect pay.example.com:443 \
  -servername pay.example.com \
  -showcerts

需要确认:

  • 域名解析到了正确的公网地址。
  • 证书的 SAN 包含当前回调域名。
  • 服务器配置了完整证书链,而不只是站点证书。
  • 证书没有过期,也不是自签名证书。
  • TLS 协议和加密套件没有采用过于特殊的配置。
  • 如果配置了 AAAA 记录,IPv6 服务也必须可达。否则应修复 IPv6 服务,或移除错误的 AAAA 记录。
  • 443 端口已对公网开放,并且没有来源 IP 白名单限制。

回调地址最好直接返回业务处理结果,不要依赖 301、302 或 307 跳转。

第四步:正确接收表单并验签

支付异步通知通常使用 POST 请求,内容类型一般是:

application/x-www-form-urlencoded

不能直接按 JSON 请求体解析。

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) -> {
        params.put(key, String.join(",", values));
    });

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

        if (!verified) {
            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");

        // 1. 校验 app_id 是否属于当前应用
        // 2. 根据 out_trade_no 查询本地订单
        // 3. 校验订单金额、商户身份及其他关键字段
        // 4. 仅在允许的状态流转条件下更新订单
        // 5. 使用 out_trade_no 或 trade_no 保证幂等

        if ("TRADE_SUCCESS".equals(tradeStatus)
                || "TRADE_FINISHED".equals(tradeStatus)) {
            return "success";
        }

        return "success";
    } catch (Exception e) {
        return "failure";
    }
}

实际使用时,要根据所采用的 SDK、密钥模式或证书模式调用相应的验签方法。alipayPublicKey 应填写支付平台公钥,不能使用应用公钥或应用私钥。

验签前不要修改通知参数,也不要再次对参数进行 URL 编码。字符集和签名类型必须与通知参数一致。

第五步:按正确格式返回响应

处理成功后,响应体应为纯文本:

success

建议同时满足:

HTTP/1.1 200 OK
Content-Type: text/plain

不要返回这类内容:

{"code":200,"message":"success"}

也不要返回:

SUCCESS

如果支付平台没有识别到约定的成功响应,可能会继续重试。因此,回调逻辑必须支持重复通知,不能因为重复收到通知而重复发货、重复加款或重复更新库存。

第六步:主动查询支付结果

异步通知可能延迟,也可能因网络或配置问题暂时无法送达。订单系统不能一直等待通知,应设置主动查询或补偿任务。

以支付宝交易查询为例:

AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();

AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(outTradeNo);

request.setBizModel(model);

AlipayTradeQueryResponse response = alipayClient.execute(request);

查询结果同样需要经过业务校验。只有确认商户订单号、金额、应用身份和交易状态都符合预期,才能将本地订单更新为已支付。

推荐排查顺序

按以下顺序排查通常更快:

  1. 查询交易,确认沙箱订单确实支付成功。
  2. 检查服务端最终下单参数中是否包含正确的 notify_url。
  3. 确认 APP 使用的是服务端生成的完整支付参数。
  4. 查看支付完成时间附近的 nginx 独立访问日志。
  5. 如果没有 nginx 记录,检查 DNS、TLS、端口、WAF 和网络策略。
  6. 如果有 nginx 记录,检查反向代理、表单解析、验签和应用异常。
  7. 确认处理成功后返回纯文本 success。
  8. 增加主动查询、幂等处理和失败补偿机制。

如果这些配置都正确,但沙箱仍未发出通知,应保留订单号、下单时间、网关响应和服务端日志,再通过对应支付平台的沙箱支持渠道核查。沙箱行为可能与正式环境不同,但在没有具体平台说明时,不应直接将问题归因于 HTTPS,或认定沙箱“不支持回调”。

备注:内容仅供参考。