APP支付完成后未收到支付宝异步通知
结论
request.setNotifyUrl(...) 的写法本身没有明显问题。如果支付成功后完全收不到异步通知,应优先检查以下三点:
- 支付宝服务器能否通过公网直接访问
http://dl.dongleikeji.com:9998/...; - APP 实际使用的订单字符串是否包含该
notify_url; - 交易是否确实支付成功,生产环境、沙箱环境和应用配置是否一致。
示例中的 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" 或附带其他说明的字符串。如果没有返回支付宝认可的成功响应,支付宝可能继续重试。不过,重试机制不能代替商户侧的主动查询和补偿处理。
如果日志中完全没有请求
可以按网络链路逐层检查:
- 域名 DNS 是否解析到正确的公网 IP;
- 云服务器安全组是否开放了对应端口;
- 操作系统防火墙是否放行;
- Nginx、Ingress 或 API 网关的访问日志中是否有请求;
- WAF、反向代理和限流规则是否拦截请求;
- 请求是否已经到达网关,但没有转发到应用;
- 接口是否因 CSRF、登录鉴权或 IP 白名单被拒绝;
- 服务是否只监听
127.0.0.1; - HTTPS 握手和证书链是否正常;
- 回调路径是否发生重定向。
如果网关日志中有请求,但应用日志中没有,问题一般出在反向代理、路由或鉴权层。如果应用已经收到请求,业务日志却没有记录,则应检查参数解析、验签失败处理和异常日志。
检查订单号字段
商户订单号应在创建支付订单前生成,例如:
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。
异步通知适合用来驱动订单状态更新,但不能作为唯一依据。生产系统还应加入主动查询和定时补偿。当订单超过一定时间仍处于待确认状态时,主动调用查询接口,并根据支付宝侧的最终状态修正本地订单。
备注:内容仅供参考。
