PAYATHON 2026

APP支付完成后未收到支付宝异步通知

支付阿杰

结论

request.setNotifyUrl(...) 的写法本身没有明显问题。如果支付成功后完全收不到异步通知,应优先检查以下三点:

  1. 支付宝服务器能否通过公网直接访问 http://dl.dongleikeji.com:9998/...;
  2. APP 实际使用的订单字符串是否包含该 notify_url;
  3. 交易是否确实支付成功,生产环境、沙箱环境和应用配置是否一致。

示例中的 out_trade_no 和 trade_no 完全相同,这通常不正常:

out_trade_no = 2024011522001493321420813051
trade_no     = 2024011522001493321420813051

out_trade_no 由商户系统生成,trade_no 由支付宝生成。两者相同未必会直接导致通知丢失,但很可能意味着订单号取值、字段映射或日志记录有误,需要重点检查。

首先检查通知地址

当前地址使用了非标准端口 9998,末尾还有一个多余的 ?:

http://dl.dongleikeji.com:9998/factory/iser/orderRecord/pay/notify?

建议删除尾部问号,并改用完整的 HTTPS 地址:

request.setNotifyUrl(
    "https://dl.dongleikeji.com/factory/iser/orderRecord/pay/notify"
);

通知地址需要满足这些条件:

  • 支付宝服务器可以通过公网访问;
  • 不能使用内网 IP、localhost 或必须连接 VPN 才能访问的地址;
  • 域名解析正确;
  • 服务器端口已向公网开放;
  • 防火墙、安全组、Nginx、网关和 WAF 没有拦截支付宝的请求;
  • 接口不要求登录,也不依赖 Cookie、Token 或自定义请求头;
  • HTTPS 证书有效,证书链完整;
  • 接口可以接收 POST 表单请求。

支付宝是否限制当前产品和环境使用非标准端口,应以实际使用的开放平台文档和网关要求为准。即便没有明确限制,生产环境也建议通过标准的 HTTPS 443 端口提供回调接口。

可以先用一台不在服务器内网中的设备测试地址是否可达:

curl -i -X POST \
  "https://dl.dongleikeji.com/factory/iser/orderRecord/pay/notify" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "out_trade_no=test_order&trade_status=TRADE_SUCCESS"

这个测试只能检查公网路由和接口的处理能力,无法模拟支付宝签名。

确认订单字符串中包含通知地址

APP 支付一般由服务端调用 sdkExecute 生成订单字符串,再把订单字符串交给 APP 调起支付宝。需要确认 APP 使用的是本次服务端生成的完整结果,而不是缓存的旧订单字符串,也不是另一套接口返回的内容。

AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setNotifyUrl(
    "https://dl.dongleikeji.com/factory/iser/orderRecord/pay/notify"
);
request.setBizContent(bizContent.toString());

AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);

if (!response.isSuccess()) {
    throw new IllegalStateException(
        "创建 APP 支付订单失败: " + response.getSubMsg()
    );
}

String orderString = response.getBody();

排查期间可以临时记录以下信息,但不要输出应用私钥:

  • 服务端生成订单的时间;
  • app_id;
  • out_trade_no;
  • notify_url;
  • sdkExecute 返回的订单字符串;
  • APP 实际提交给支付宝 SDK 的订单字符串;
  • 当前使用的是生产网关还是沙箱网关。

如果服务端已经生成了新的订单字符串,客户端却仍在使用旧字符串,那么新的通知地址不会生效。

查询交易是否真正成功

客户端显示“支付完成”不能代替服务端确认。应调用 alipay.trade.query 查询交易状态:

AlipayTradeQueryRequest queryRequest = new AlipayTradeQueryRequest();

JSONObject queryContent = new JSONObject();
queryContent.put("out_trade_no", merchantOrderNo);
queryRequest.setBizContent(queryContent.toString());

AlipayTradeQueryResponse queryResponse = alipayClient.execute(queryRequest);

if (queryResponse.isSuccess()) {
    System.out.println("trade_no=" + queryResponse.getTradeNo());
    System.out.println("out_trade_no=" + queryResponse.getOutTradeNo());
    System.out.println("trade_status=" + queryResponse.getTradeStatus());
} else {
    System.out.println("code=" + queryResponse.getCode());
    System.out.println("sub_code=" + queryResponse.getSubCode());
    System.out.println("sub_msg=" + queryResponse.getSubMsg());
}

重点关注以下结果:

  • TRADE_SUCCESS:交易支付成功;
  • TRADE_FINISHED:交易已经结束;
  • WAIT_BUYER_PAY:仍在等待付款;
  • 查询失败:检查订单号、应用、环境和签约账号是否一致。

如果查询不到交易,或者查询结果还是 WAIT_BUYER_PAY,就不能只从异步通知环节排查问题。

正确接收并响应通知

支付宝异步通知通常使用 application/x-www-form-urlencoded 格式,并非 JSON 请求体。Spring 接口可以按表单参数接收:

@PostMapping("/factory/iser/orderRecord/pay/notify")
public String receiveNotify(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 sellerId = params.get("seller_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");

        // 校验 app_id、seller_id、订单金额和商户订单状态。
        // 业务处理必须保证幂等,避免重复通知造成重复入账。
        if ("TRADE_SUCCESS".equals(tradeStatus)
                || "TRADE_FINISHED".equals(tradeStatus)) {
            // 更新订单状态
        }

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

处理成功后,接口必须返回以下纯文本:

success

不要返回 JSON、HTML、"SUCCESS" 或附带其他说明的字符串。如果没有返回支付宝认可的成功响应,支付宝可能继续重试。不过,重试机制不能代替商户侧的主动查询和补偿处理。

如果日志中完全没有请求

可以按网络链路逐层检查:

  1. 域名 DNS 是否解析到正确的公网 IP;
  2. 云服务器安全组是否开放了对应端口;
  3. 操作系统防火墙是否放行;
  4. Nginx、Ingress 或 API 网关的访问日志中是否有请求;
  5. WAF、反向代理和限流规则是否拦截请求;
  6. 请求是否已经到达网关,但没有转发到应用;
  7. 接口是否因 CSRF、登录鉴权或 IP 白名单被拒绝;
  8. 服务是否只监听 127.0.0.1;
  9. HTTPS 握手和证书链是否正常;
  10. 回调路径是否发生重定向。

如果网关日志中有请求,但应用日志中没有,问题一般出在反向代理、路由或鉴权层。如果应用已经收到请求,业务日志却没有记录,则应检查参数解析、验签失败处理和异常日志。

检查订单号字段

商户订单号应在创建支付订单前生成,例如:

String outTradeNo = "ORDER_" + System.currentTimeMillis();
bizContent.put("out_trade_no", outTradeNo);

支付宝返回的 trade_no 应从查询结果或异步通知中取得:

String tradeNo = params.get("trade_no");

不要用 trade_no 填充 out_trade_no,也不要把客户端返回的订单号直接当作支付宝交易号。服务端应分别保存:

merchant_order_no -> out_trade_no
alipay_trade_no   -> trade_no

如果日志中的两个字段始终完全相同,应检查:

  • 创建订单时生成 out_trade_no 的代码;
  • 支付结果解析代码;
  • 数据库字段映射;
  • 日志输出时是否写错变量;
  • 是否使用了模拟数据;
  • 是否把商户订单号同时赋给了两个字段。

建议的最终处理方式

把通知地址改为公网可访问、使用 HTTPS 标准端口的地址,并删除末尾的 ?。同时确认 APP 使用的订单字符串确实由设置了该地址的请求生成,再通过 alipay.trade.query 核实交易状态,分别保存 out_trade_no 和 trade_no。

异步通知适合用来驱动订单状态更新,但不能作为唯一依据。生产系统还应加入主动查询和定时补偿。当订单超过一定时间仍处于待确认状态时,主动调用查询接口,并根据支付宝侧的最终状态修正本地订单。

备注:内容仅供参考。