PAYATHON 2026

支付宝 App 支付沙箱无法收到异步回调

支付小周

结论

alipay.trade.app.pay 的异步通知地址必须通过请求对象的 notify_url 参数传给支付宝。只在代码里定义 ALI_CALL_BACK_URL,或者把地址返回给 App,都不会触发异步回调。

如果已经调用了 request.setNotifyUrl(...),应优先检查回调地址能否从公网访问。当前地址使用 natappfree.cc 的免费内网穿透域名,可能遇到隧道离线、域名临时变化、连接不稳定或支付宝服务器无法访问等问题。

App 端收到的支付结果只是客户端同步结果,不代表服务端已经收到并处理了异步通知。订单的最终状态应以验签后的异步通知或主动查询结果为准。

正确设置异步通知地址

Java 服务端生成 App 支付订单时,需要在 AlipayTradeAppPayRequest 上设置通知地址:

AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setNotifyUrl(ALI_CALL_BACK_URL);

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

request.setBizModel(model);

AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);

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

// 将 response.getBody() 返回给 App,由支付宝 SDK 发起支付
String orderString = response.getBody();

其中这行代码需要特别注意:

request.setNotifyUrl(ALI_CALL_BACK_URL);

它必须在调用 sdkExecute(request) 之前执行。不能只声明常量,也不能把 notify_url 放进业务模型的自定义字段里。

建议记录 response.getBody(),确认最终生成的支付参数中包含经过编码的 notify_url。如果日志涉及签名或敏感配置,需要做好脱敏处理。

回调接口应满足的条件

支付宝服务器会直接发起异步通知,因此回调接口需要满足以下条件:

  • 可以从公网直接访问,不能使用 localhost、局域网 IP 或仅本机可访问的地址。
  • 支持 HTTP POST。
  • 不依赖登录状态、Cookie、Session 或前端页面跳转。
  • 不会被 Spring Security、Shiro、网关鉴权或 CSRF 校验拦截。
  • 不会发生 HTTP 到 HTTPS、域名或路径重定向。
  • 处理成功后返回纯文本 success。
  • 不能返回 JSON、HTML 页面或带引号的 "success"。
  • 尽快响应。耗时业务可以在验签后异步执行。
  • 同一条通知可能重复送达,业务处理必须保证幂等。

当前地址是:

http://4ngxtw.natappfree.cc/zhiFuBaoCallback/getAliPayResult

至少要在一台未连接本地网络的公网设备上测试这个地址。浏览器直接打开地址时通常发送 GET 请求,而支付宝发送的是 POST,所以浏览器能够打开并不能完全证明回调接口可用。

可以发送 POST 请求检查基本连通性:

curl -i -X POST \
  "http://4ngxtw.natappfree.cc/zhiFuBaoCallback/getAliPayResult" \
  -d "test=1"

测试时主要检查:

  • 是否连接超时;
  • 是否返回 301、302、403、404、405 或 500;
  • NATAPP 客户端是否在线;
  • 请求是否到达本地应用;
  • 域名和转发端口是否仍然有效;
  • 反向代理是否保留了 POST 表单参数。

如果需要长期或稳定联调,建议使用固定公网域名和 HTTPS 地址,不要依赖可能变化的免费临时隧道。

正确接收并验证异步通知

支付宝异步通知通常以 application/x-www-form-urlencoded 表单提交。Spring MVC 可以这样接收:

import com.alipay.api.internal.util.AlipaySignature;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class AlipayCallbackController {

    @Value("${alipay.alipay-public-key}")
    private String alipayPublicKey;

    @PostMapping(
        value = "/zhiFuBaoCallback/getAliPayResult",
        produces = "text/plain;charset=UTF-8"
    )
    public String getAliPayResult(HttpServletRequest request) {
        try {
            Map<String, String> params = new HashMap<>();

            request.getParameterMap().forEach((name, values) -> {
                if (values != null && values.length > 0) {
                    params.put(name, String.join(",", values));
                }
            });

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

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

            // 还应校验:
            // 1. appId 是否与当前沙箱应用一致
            // 2. outTradeNo 是否存在且属于当前业务
            // 3. totalAmount 是否与数据库订单金额一致
            // 4. seller_id(如果使用)是否与预期收款账号一致

            if ("TRADE_SUCCESS".equals(tradeStatus)
                    || "TRADE_FINISHED".equals(tradeStatus)) {
                // 使用 outTradeNo 或 tradeNo 做唯一约束,保证重复通知不会重复入账。
                // 在数据库事务中更新订单状态。
            }

            return "success";
        } catch (Exception e) {
            // 记录异常,但避免把密钥或完整敏感报文写入公开日志。
            return "failure";
        }
    }
}

沙箱环境必须使用对应沙箱应用的支付宝公钥和应用配置,不能混用正式环境的网关、App ID、应用私钥或支付宝公钥。

验签通过只是第一步。更新订单前,还要核对 app_id、out_trade_no、total_amount、收款方等字段,避免把通知关联到错误的订单。

建议按以下顺序排查

1. 确认支付请求中设置了 notify_url

检查生成支付参数的代码是否调用了:

request.setNotifyUrl(ALI_CALL_BACK_URL);

还要确认设置通知地址和执行 sdkExecute() 时使用的是同一个 request 对象。

2. 检查实际生成的订单字符串

确认 response.getBody() 中包含 notify_url。如果没有,说明通知地址没有进入这次支付请求,支付宝也就不知道该通知哪个接口。

如果订单已经创建并完成支付,之后再修改代码中的通知地址,不会改变这笔历史交易携带的参数。修改后需要使用新的商户订单号,重新发起一笔沙箱支付。

3. 检查请求是否到达 NATAPP

在回调接口最前面增加经过安全处理的访问日志,同时检查:

  • NATAPP 客户端日志;
  • Web 服务器访问日志;
  • Spring Boot 应用日志;
  • Nginx、网关或 WAF 日志。

如果所有入口日志都没有收到请求,通常要从公网域名、隧道、DNS、端口、防火墙或 notify_url 设置中排查原因。

如果入口日志记录了请求,但控制器没有收到,则应检查路由、请求方法、上下文路径、反向代理配置和安全过滤器。

4. 检查接口返回值

处理成功后,响应体必须是:

success

以下响应通常不符合要求:

{"code":200,"message":"success"}
"success"
<html>success</html>

如果支付宝已经请求了接口,却没有收到符合要求的响应,后续可能继续重试通知。具体重试节奏应以当前支付宝开放平台文档和沙箱行为为准。

5. 检查沙箱配置是否混用

确认下面这些配置全部属于同一个沙箱应用:

沙箱网关
沙箱 App ID
应用私钥
沙箱支付宝公钥
沙箱买家账号

网关通常应使用沙箱网关,而不是正式环境网关。具体地址以当前支付宝开放平台沙箱控制台显示的内容为准。

6. 主动查询订单状态

对于订单号:

2023041617244916744864471

可以调用 alipay.trade.query,主动确认支付宝侧的交易状态:

AlipayTradeQueryRequest queryRequest = new AlipayTradeQueryRequest();

AlipayTradeQueryModel queryModel = new AlipayTradeQueryModel();
queryModel.setOutTradeNo("2023041617244916744864471");

queryRequest.setBizModel(queryModel);

AlipayTradeQueryResponse queryResponse =
    alipayClient.execute(queryRequest);

if (queryResponse.isSuccess()) {
    System.out.println("tradeStatus=" + queryResponse.getTradeStatus());
    System.out.println("tradeNo=" + queryResponse.getTradeNo());
} else {
    System.out.println("subCode=" + queryResponse.getSubCode());
    System.out.println("subMsg=" + queryResponse.getSubMsg());
}

主动查询可以确认支付是否成功,但不能代替异步通知接口。在实际业务中,可以把主动查询作为补偿机制,例如订单长时间没有收到回调时,用它核对交易状态。

容易忽略的问题

App 端返回的 resultStatus=9000 等客户端结果可以用来改善用户体验,但不建议据此直接把服务端订单标记为已支付。客户端结果可能受到网络、进程退出或数据篡改的影响,并不可靠。

回调接口收到请求后,也不能立即更新订单。正确的处理顺序是:

接收参数
→ 验证支付宝签名
→ 核对应用、订单、金额和收款方
→ 幂等更新订单
→ 返回纯文本 success

针对当前情况,应先确认 request.setNotifyUrl(ALI_CALL_BACK_URL) 是否实际执行,再检查付款完成时 NATAPP 隧道是否在线,以及 POST 请求能否到达应用。这两项是客户端显示支付成功,但服务端完全收不到异步通知时最常见的原因。

备注:内容仅供参考。