APP支付成功后未收到异步回调
结论
外部浏览器能访问回调地址,不代表支付宝异步通知一定能正常送达。首先确认创建 APP 支付订单时,是否通过 notify_url 传入了这个完整地址:
https://test.hybtco.com/zsnc/s20UserClient/mall/aliPayNotify.do
“授权回调地址”和“支付异步通知地址”是两回事。开放平台后台配置的授权回调地址,通常不会自动用于 APP 支付的异步通知。服务端调用支付接口时,需要明确设置 notify_url。
如果支付请求已经正确设置了该参数,再依次检查网关访问日志、HTTP 状态码、重定向、TLS/WAF、参数解析、验签和响应内容。
先检查支付请求
APP 支付订单应由服务端创建,并在 alipay.trade.app.pay 请求中设置通知地址。Java SDK 示例:
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo("1678950734-Test0011472");
model.setTotalAmount("0.01");
model.setSubject("订单支付");
model.setProductCode("QUICK_MSECURITY_PAY");
request.setBizModel(model);
request.setNotifyUrl(
"https://test.hybtco.com/zsnc/s20UserClient/mall/aliPayNotify.do"
);
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
String orderString = response.getBody();
检查服务端最终提交给支付宝的请求,不要只看客户端保存的配置:
- 请求中是否有
notify_url。 - 地址是否与当前测试环境一致。
- 是否误填了 OAuth 授权回调地址、同步跳转地址或旧域名。
- 配置是否被覆盖,例如测试配置被生产配置替换。
- URL 是否被错误地二次编码。
- 当前支付订单是否确实由这段服务端代码创建。
修改 notify_url 只会影响此后创建的新订单。已经创建并支付的订单,不会因为后来修改了配置而自动更换通知地址。
根据访问日志定位问题层级
同时查看 CDN、负载均衡、Nginx 和应用日志,并用订单号搜索:
1678950734-Test0011472
根据日志结果,可以按下面几种情况排查。
完全没有请求记录
这通常说明请求没有到达入口层。需要检查:
- 创建支付订单时没有传递
notify_url,或者传入了其他地址。 - DNS 解析异常,尤其是 IPv6 解析错误。
- HTTPS 证书过期、证书链不完整或域名不匹配。
- 服务器只允许特定来源 IP。
- CDN、防火墙或 WAF 拦截了支付宝的
POST请求。 - 回调地址只能通过公司内网、VPN 或特定 Host 访问。
- 网络入口不支持外部 IPv4/IPv6 访问。
不要只用浏览器发起 GET 请求来验证。异步通知通常使用表单格式的 POST 请求,两种请求经过的安全规则可能不同。
可以从外部环境测试该路由是否接受 POST:
curl -i -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'out_trade_no=1678950734-Test0011472&trade_status=TRADE_SUCCESS' \
'https://test.hybtco.com/zsnc/s20UserClient/mall/aliPayNotify.do'
这个请求没有支付宝签名,只能用来检查网络、路由和请求解析,不能验证完整的支付逻辑。
有请求,但返回 301、302、307 或 308
这说明回调地址发生了重定向,例如:
HTTP → HTTPS
旧域名 → 新域名
未带斜杠 → 带斜杠
登录页或统一认证页
应将 notify_url 直接设置为最终处理地址,并确保接口不依赖登录、Cookie、验证码或其他浏览器交互。
有请求,但返回 403 或 404
需要检查:
- WAF、CSRF、防爬虫或来源校验是否拦截了请求。
- Spring Security 等安全组件是否要求 CSRF Token。
- 反向代理配置的路径是否与应用路径一致。
- 接口是否只允许
GET,没有接收POST。 - 域名对应的虚拟主机是否正确。
- 发布后的实际 Context Path 是否仍是
/zsnc。
异步通知接口通常需要免登录,并对该路径合理处理 CSRF。免登录或豁免 CSRF 不等于跳过支付宝签名验证。
有请求,但返回 500 或超时
查看应用异常日志,常见原因有:
- 把表单参数当成 JSON 解析。
- 字符集处理不一致。
- 验签时使用了错误的支付宝公钥。
- 数据库事务异常或连接池耗尽。
- 业务逻辑执行时间过长。
- 重复通知引发唯一键冲突。
- 接口先调用耗时的第三方服务,导致响应超时。
回调入口应尽快完成验签、订单校验和本地状态更新。耗时操作可以在事务提交后交给消息队列或异步任务。
正确接收并验签
支付宝异步通知通常以 application/x-www-form-urlencoded 格式提交,不要直接按 JSON 请求体读取。Spring MVC 示例:
@PostMapping(
value = "/zsnc/s20UserClient/mall/aliPayNotify.do",
produces = "text/plain;charset=UTF-8"
)
public String alipayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((name, values) -> {
params.put(name, String.join(",", values));
});
try {
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String appId = params.get("app_id");
String totalAmount = params.get("total_amount");
String sellerId = params.get("seller_id");
// 1. 根据 outTradeNo 查询本地订单
// 2. 校验 appId、sellerId、订单金额和订单归属
// 3. 仅处理 TRADE_SUCCESS 或 TRADE_FINISHED
// 4. 使用事务和唯一约束保证幂等
// 5. 保存 tradeNo 及支付状态
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
paymentService.confirmPayment(
outTradeNo,
tradeNo,
totalAmount,
appId,
sellerId
);
}
return "success";
} catch (Exception e) {
// 记录异常及可定位信息,但不要记录私钥等敏感数据
return "failure";
}
}
验签时需要确认:
- 使用的是支付宝公钥,不是应用公钥或应用私钥。
sign_type、字符集和密钥模式与实际配置一致。- 验签参数来自支付宝提交的原始表单参数。
- 验签前没有随意修改、重新编码或格式化参数值。
- 证书模式与普通公钥模式的配置方式不同,需要与项目当前的接入方式一致。
- 验签成功后,仍需校验
app_id、seller_id、out_trade_no、total_amount等业务字段。
响应内容必须准确
处理成功后,响应体应直接返回:
success
不要返回 JSON、HTML 页面或统一响应对象。下面这些内容通常不符合要求:
{"code":0,"data":"success"}
<html><body>success</body></html>
还要防止框架把返回值当成视图名称。可以使用 @RestController、@ResponseBody,或者直接写入响应体,并确认最终 HTTP 状态码是 200。
业务处理失败时,应返回 failure 或非成功响应,让支付平台按其机制继续通知。重试次数和时间间隔可能随平台规则调整,应以当前官方文档和实际通知记录为准。
保证重复通知安全
支付宝可能重复发送异步通知,所以第二次收到通知不能直接当作异常。订单更新必须具备幂等性,例如:
UPDATE payment_order
SET
pay_status = 'PAID',
alipay_trade_no = ?
WHERE
out_trade_no = ?
AND pay_status = 'UNPAID';
也可以为支付宝交易号建立唯一约束:
CREATE UNIQUE INDEX uk_payment_alipay_trade_no
ON payment_order (alipay_trade_no);
如果订单已经处于已支付状态,应直接按处理成功对待,不能重复发货、增加余额或生成权益。
对当前订单进行补偿查询
对于这个订单:
out_trade_no = 1678950734-Test0011472
如果找不到任何异步通知记录,应由服务端调用 alipay.trade.query 主动查询交易状态。确认支付成功后,再通过幂等的补偿流程更新本地订单。
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo("1678950734-Test0011472");
request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()
&& ("TRADE_SUCCESS".equals(response.getTradeStatus())
|| "TRADE_FINISHED".equals(response.getTradeStatus()))) {
paymentService.confirmPaymentByQuery(
response.getOutTradeNo(),
response.getTradeNo(),
response.getTotalAmount()
);
}
客户端显示“支付成功”只能用于界面提示,不能作为服务端发货或入账的依据。可靠的支付结果必须来自验签通过的异步通知,或者服务端主动查询。
推荐排查顺序
- 打印创建 APP 支付订单时的实际请求参数,确认其中包含正确的
notify_url。 - 创建新订单并重新支付,不要用修改配置前创建的旧订单验证。
- 在网关和应用访问日志中搜索
out_trade_no。 - 查看回调请求的 HTTP 状态码、耗时和响应体。
- 排除重定向、登录认证、CSRF、WAF 和 IP 白名单限制。
- 确认接口按表单参数接收通知,并使用正确的支付宝公钥验签。
- 确认处理成功时只返回纯文本
success。 - 使用
alipay.trade.query核对漏通知订单,并建立定时补偿机制。
现有信息还不足以确定问题出在哪一层。应最先确认该地址是否真的通过 notify_url 写入了 alipay.trade.app.pay 请求,而不是只配置成了“授权回调地址”。
备注:内容仅供参考。
