沙箱APP支付成功后未收到回调接口请求
结论
沙箱 APP 支付成功却没有收到回调,问题通常不在客户端的支付结果,而在以下几个环节:异步通知地址 notify_url 没有正确传入;支付宝服务器无法直接访问回调地址;通知已经到达,但被网关、HTTPS 配置或业务代码拦截。
APP 支付状态应以服务端收到的异步通知为准。客户端返回 9000,只能说明支付流程已经成功结束,不能代替服务端回调。
先确认是否传入了 notify_url
服务端调用 APP 支付接口时,需要在请求参数中传入完整的异步通知地址,例如:
https://pay.example.com/api/alipay/notify
注意以下几点:
notify_url是异步通知地址,支付宝服务器会向该地址发送 POST 请求。- APP 支付一般不依赖
return_url。return_url主要用于网页同步跳转,不能代替异步通知。 notify_url应在服务端生成支付订单时设置,不能只写在 APP 客户端代码中。- 地址必须包含协议和完整路径,不能使用相对地址。
以 Java SDK 的常见写法为例:
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setBizContent(objectMapper.writeValueAsString(bizContent));
request.setNotifyUrl("https://pay.example.com/api/alipay/notify");
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
String orderInfo = response.getBody();
不同版本 SDK 的类名和调用方式可能略有差异,请以项目实际使用的支付宝开放平台 SDK 为准。
检查回调地址能否从公网访问
浏览器能够打开回调地址,不代表支付宝服务器可以正常调用它。需要从公网环境检查以下项目:
- 回调域名能否正常解析。
- 服务器的 80 或 443 端口是否开放。
- URL 是否存在跳转,例如从 HTTP 自动跳转到 HTTPS。
- 接口是否要求登录、Cookie、Token 或其他鉴权信息。
- 防火墙、WAF、CDN 或反向代理是否拦截了支付宝的 POST 请求。
- HTTPS 证书是否有效,证书链是否完整,证书域名是否匹配。
- 回调接口是否支持
application/x-www-form-urlencoded格式。
回调地址不能使用以下类型的地址:
http://localhost:8080/notify
http://127.0.0.1/notify
http://192.168.1.10/notify
本地调试时,需要使用公网可访问的测试服务器,或者通过带有公网 HTTPS 地址的内网穿透服务转发请求。是否允许 HTTP、非标准端口或特定 TLS 配置,应以当前支付宝开放平台的接入要求为准。
可以先用 curl 模拟表单 POST,确认请求能够通过网关并进入应用:
curl -i -X POST 'https://pay.example.com/api/alipay/notify' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'out_trade_no=test_order&trade_status=TRADE_SUCCESS'
这个命令只能检查接口连通性,无法模拟带有合法支付宝签名的通知。
在接口入口记录原始请求
排查时不能只看业务处理日志。请求刚进入回调方法时,就应记录请求时间、请求参数和请求来源,然后再进行验签和业务处理。
Spring Boot 示例:
@PostMapping(
value = "/api/alipay/notify",
consumes = "application/x-www-form-urlencoded"
)
public String alipayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> {
params.put(key, String.join(",", values));
});
log.info("Received Alipay notify: {}", params);
// 后续执行验签和业务处理
return "success";
}
如果 Nginx 开启了访问日志,也要检查对应路径是否出现 POST 请求:
location /api/alipay/notify {
proxy_pass http://127.0.0.1:8080;
}
可以根据日志结果缩小排查范围:
- Nginx 没有记录:检查
notify_url、DNS、防火墙、CDN 和 HTTPS。 - Nginx 有记录,但应用没有记录:检查路由、反向代理和请求体限制。
- 应用有记录,但订单状态没有变化:检查验签、参数解析和业务异常。
- 应用处理成功,但平台仍在重复通知:检查响应内容是否为纯文本
success。
正确验签并核对订单信息
收到通知后,不能直接更新订单。必须使用支付宝公钥验签,并核对应用、商户订单、金额和卖家信息。
Java 示例:
@PostMapping("/api/alipay/notify")
public String alipayNotify(HttpServletRequest request) {
try {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> {
params.put(key, String.join(",", values));
});
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
log.warn("Alipay notify signature verification failed");
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");
// 必须查询本地订单并核对:
// 1. out_trade_no 是否存在
// 2. app_id 是否为当前应用
// 3. seller_id 是否为预期收款账号
// 4. total_amount 是否与本地订单金额一致
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
paymentService.markPaid(
outTradeNo,
tradeNo,
new BigDecimal(totalAmount)
);
}
return "success";
} catch (Exception e) {
log.error("Failed to process Alipay notify", e);
return "failure";
}
}
这里的 alipayPublicKey 是支付宝公钥,不是应用私钥,也不是应用公钥。沙箱和生产环境的应用配置、网关、密钥及账号不能混用。
如果使用证书模式,应按照当前 SDK 的证书验签方式处理,不能直接使用普通公钥模式的初始化代码。
检查是否混用了沙箱和生产配置
线上回调正常,只能证明线上域名和线上应用配置可用,无法证明沙箱订单参数正确。沙箱环境需要逐项核对:
- 创建订单时使用的是否为沙箱网关。
app_id是否属于沙箱应用。- 签名私钥是否与沙箱应用配置匹配。
- 验签使用的支付宝公钥是否属于同一套沙箱配置。
- 付款账号是否为沙箱买家账号。
- 收款账号是否为对应的沙箱卖家账号。
- 实际提交的订单参数中是否包含预期的
notify_url。
除了检查配置文件,还要记录最终发送给支付网关的请求参数。多环境项目中,环境变量覆盖、配置中心未刷新或实例未重启,都可能导致实际运行配置与预期不一致。
回调接口必须返回纯文本 success
通知处理完成后,应直接返回:
success
不要返回 JSON:
{"code":200,"message":"success"}
也不要返回 HTML、重定向页面,或由统一响应对象包装后的内容。Spring Boot 可以明确返回纯文本:
@PostMapping(
value = "/api/alipay/notify",
produces = "text/plain;charset=UTF-8"
)
@ResponseBody
public String alipayNotify(HttpServletRequest request) {
// 验签并处理订单
return "success";
}
只有验签通过、订单信息核对无误,而且业务处理成功后,才能返回 success。处理失败时不要返回成功,否则可能失去通过通知重试修正订单状态的机会。
排查框架和安全组件的拦截
回调请求由支付宝服务器发起,不会携带当前网站用户的登录状态。需要确认回调路径没有被以下机制拦截:
- 登录认证拦截器;
- Spring Security 鉴权;
- CSRF 校验;
- API 网关签名校验;
- Referer 或 User-Agent 白名单;
- 请求频率限制;
- IP 白名单;
- WAF 的表单或敏感参数规则。
Spring Security 项目可以根据实际版本放行回调路径。下面的代码仅用于说明配置意图:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/alipay/notify").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/alipay/notify")
);
允许回调请求通过网络和登录鉴权,并不等于降低支付安全性。接口仍需通过支付宝签名验证和订单信息核对,判断请求是否合法。
用主动查询补偿异步通知
异步通知不应是确认支付状态的唯一方式。客户端返回支付成功后,服务端可以使用商户订单号调用交易查询接口,再根据查询结果更新订单。
常用的查询接口是:
alipay.trade.query
示例:
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent(
"{\"out_trade_no\":\"" + outTradeNo + "\"}"
);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
String tradeStatus = response.getTradeStatus();
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
paymentService.markPaid(
response.getOutTradeNo(),
response.getTradeNo(),
response.getTotalAmount()
);
}
}
查询结果也需要核对订单号、金额和收款方等信息。具体字段和 SDK 方法以项目使用的 SDK 版本为准。
可以采用下面的处理流程:
客户端完成支付
↓
服务端等待异步通知
↓
通知到达:验签、核对订单、幂等更新
↓
通知未及时到达:主动查询交易状态
↓
定时任务继续扫描未确认订单
建议的排查顺序
按照以下顺序检查,通常可以较快找到问题:
- 打印创建支付订单时实际提交的
notify_url。 - 确认沙箱网关、沙箱
app_id和沙箱密钥属于同一套配置。 - 从公网环境向回调地址发送 POST 请求。
- 检查 DNS、HTTPS 证书、防火墙、CDN、WAF 和 Nginx 日志。
- 检查应用入口日志,确认请求是否被鉴权或 CSRF 拦截。
- 确认接口能够解析表单参数,而不是只接收 JSON。
- 检查验签公钥、字符集和签名类型是否正确。
- 确认处理成功后返回的是纯文本
success。 - 使用
alipay.trade.query查询该沙箱订单的实际交易状态。 - 为未确认订单增加定时查询和人工补单能力。
支付通知可能重复到达,也可能因网络问题出现延迟,因此订单更新必须具备幂等性。可以将商户订单号作为唯一条件,只允许订单从“待支付”变更为“已支付”,防止重复通知引起重复发货、重复记账或重复增加余额。
备注:内容仅供参考。
