沙箱环境下 APP 支付成功后未收到 notify_url 回调
结论
APP 支付成功,但服务端没有收到 notify_url 通知,通常与以下环节有关:
notify_url没有进入最终签名的支付请求;- 沙箱页面中配置的地址并不是 APP 支付订单的异步通知地址;
- 支付平台已经发出通知,但请求被网关、WAF、HTTPS 配置或应用路由拦截;
- 服务端收到了通知,但验签或业务处理失败,也没有记录原始请求日志;
- 接口没有返回纯文本
success,平台因此判定通知失败并重试; - 沙箱通知链路出现延迟或临时异常。
排查时,先通过订单查询接口确认交易状态,再核对最终请求参数和入口访问日志。客户端显示”支付成功”,并不代表异步通知一定已经发出。
先确认 notify_url 是否进入最终支付请求
APP 支付的异步通知地址应随支付请求一起提交,并参与签名。只在沙箱信息页面配置地址,通常不能代替支付请求中的 notify_url。
以支付宝 Java SDK 为例:
AlipayClient alipayClient = new DefaultAlipayClient(
gatewayUrl,
appId,
privateKey,
"json",
"UTF-8",
alipayPublicKey,
"RSA2"
);
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setNotifyUrl("https://pay.example.com/alipay/notify");
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo("ORDER_202609130001");
model.setTotalAmount("0.01");
model.setSubject("测试订单");
model.setProductCode("QUICK_MSECURITY_PAY");
request.setBizModel(model);
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
String orderString = response.getBody();
需要重点检查以下几点:
setNotifyUrl()必须在sdkExecute()之前调用;- APP 应使用服务端生成的完整
orderString; - 签名完成后,不要手工追加或修改
notify_url; - 不要把同步跳转地址、应用网关或授权回调地址当成支付异步通知地址;
- 如果支付参数由多个服务共同拼装,应记录最终参与签名的参数,不能只记录最初的业务对象。
可以对最终订单字符串进行脱敏记录,确认其中确实包含 notify_url,同时检查地址是否存在编码、拼接或环境替换错误。
确认交易是否真的成功
客户端 SDK 返回成功,只表示客户端支付流程得到了成功结果,不能代替服务端查单。
应调用支付平台提供的订单查询接口,根据 out_trade_no 或 trade_no 查询最终状态。支付宝场景通常使用 alipay.trade.query,重点检查:
trade_status是否为TRADE_SUCCESS或业务接受的其他最终状态;out_trade_no是否与本地订单一致;total_amount、seller_id和app_id是否正确;- 交易是否属于当前沙箱应用及对应的沙箱账号。
如果查单结果还不是最终成功状态,暂时没有异步通知可能属于正常情况。如果查单确认交易成功,而入口层完全没有请求记录,再继续检查通知地址和沙箱链路。
从入口层判断通知是”没有发送”还是”没有处理”
不能只查看控制器中的业务日志,因为通知请求可能还没到达应用就被拒绝了。
建议按以下顺序检查:
- CDN、负载均衡或反向代理访问日志;
- WAF、防火墙及安全组拦截日志;
- Nginx、Apache 等 Web 服务器访问日志;
- 应用路由和异常日志;
- 验签失败及参数解析失败日志。
如果入口日志中能看到平台发出的 POST 请求,说明通知已经发送,问题出在服务端处理链路。只有当所有入口层都没有记录时,才更可能是支付请求中的 notify_url 未生效、DNS 或网络不可达,或者沙箱通知链路出现异常。
回调地址至少要满足以下条件:
- 使用公网可访问的完整
http://或https://地址; - 不能使用
localhost、127.0.0.1或只能在内网解析的域名; - DNS 能从公网正常解析;
- HTTPS 证书有效、证书链完整,且域名与证书匹配;
- 接口允许
POST请求; - 接口不依赖登录状态、Cookie、验证码或 CSRF Token;
- 不发生 HTTP 跳转到 HTTPS、切换域名等多次重定向;
- 网关不会根据 User-Agent、IP 地区或请求体类型误拦截;
- 接口能处理
application/x-www-form-urlencoded表单,而不是只能接收 JSON。
浏览器可以打开回调地址,只能说明 GET 请求大致可达,无法证明平台发送的 POST 表单也能被正确处理。
正确接收通知并验签
支付宝异步通知通常通过表单参数提交。下面是一个简化的 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) -> {
if (values != null && values.length > 0) {
params.put(key, values[0]);
}
});
try {
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
log.warn("Alipay notify signature verification failed, outTradeNo={}",
params.get("out_trade_no"));
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");
String sellerId = params.get("seller_id");
// 必须校验订单号、金额、app_id、seller_id 等业务字段。
// 更新订单时使用 out_trade_no 或 trade_no 做幂等控制。
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
markOrderPaidIdempotently(
outTradeNo,
tradeNo,
totalAmount,
appId,
sellerId
);
}
return "success";
} catch (Exception e) {
log.error("Failed to process Alipay notify", e);
return "failure";
}
}
实际项目还要注意:
- 使用当前应用对应的支付宝公钥验签,不要误用应用公钥;
charset和sign_type应与请求配置一致;- 验签前不要修改参与验签的参数值;
- 比较金额时应使用
BigDecimal,不能使用double; - 必须校验通知中的订单号、金额、收款方和应用身份;
- 数据库更新必须保证幂等,因为异步通知可能重复发送;
- 业务处理成功后应返回纯文本
success,不能返回 JSON、HTML 或带引号的"success"; - 如果处理失败,应返回
success以外的内容,让平台按照通知机制重试。
排查框架和网关配置
常见的本地代码问题包括:
@PostMapping("/alipay/notify")
public String notify(@RequestBody NotifyRequest body) {
// 错误风险:平台发送的可能是表单,而不是 JSON。
return "success";
}
如果接口只接受 JSON,表单通知可能直接收到 400 或 415。还需要检查:
- Spring Security 是否要求认证;
- CSRF 是否拦截了该路径;
- API 网关是否限制请求来源;
- Controller 的返回值是否被统一包装成 JSON;
- 全局异常处理器是否把异常转换成
200 OK,但响应体并不是success; - 日志脱敏或采样规则是否让人误以为请求没有到达;
- 回调路径是否存在大小写或尾部斜杠差异。
可以从外部网络模拟表单请求,测试完整的入口链路:
curl -i -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'out_trade_no=TEST_ORDER&trade_status=TRADE_SUCCESS' \
'https://pay.example.com/alipay/notify'
这个请求无法通过真实签名校验是正常的。测试的重点是确认 DNS、TLS、网关、路由和表单解析是否正常,并观察服务端能否留下访问日志和验签失败日志。
建议的排查顺序
- 调用
alipay.trade.query,确认订单的最终状态。 - 检查服务端生成的最终支付请求,确认
notify_url已在签名前设置。 - 确认客户端使用的是这份完整订单字符串,没有缓存旧订单,也没有切换到其他环境。
- 检查公网 DNS、HTTPS 证书、
POST路由和表单解析。 - 查看 CDN、网关、Nginx 和应用入口日志。
- 检查验签公钥、字符集、签名类型及业务字段校验。
- 确认业务处理成功后的响应体严格为
success。 - 如果查单已经成功、请求参数无误,而且入口层长时间没有任何通知记录,再考虑沙箱链路延迟或异常。保留
app_id、out_trade_no、trade_no、支付时间和查单结果,以便进一步核查。
生产系统不能只依赖异步通知。还应建立主动查单补偿机制:订单支付后,如果在一定时间内没有收到通知,由定时任务调用查询接口核实状态,再以幂等方式补记支付结果。即使沙箱或正式环境中的通知出现延迟,订单状态也能最终保持一致。
备注:内容仅供参考。