支付宝 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 请求能否到达应用。这两项是客户端显示支付成功,但服务端完全收不到异步通知时最常见的原因。
备注:内容仅供参考。
