小程序支付成功但后端未收到异步通知
支付老李
结论
支付成功不等于后端一定能收到异步通知。支付宝是否发送通知,主要取决于创建交易时有没有正确传入 notify_url,以及支付宝服务器能否访问该地址。
仅凭现有信息,无法确认交易号 2020110722001422215734505242 在平台侧的通知记录。可以先检查以下三项:
- 是否通过
request.setNotifyUrl(...)设置了异步通知地址。 notify_url是否能从公网访问,是否被防火墙、登录鉴权、反向代理或其他安全策略拦截。- 通知接口处理完请求后,是否以纯文本返回
success。
如果创建交易时没有携带有效的 notify_url,即使支付成功,支付宝也不会向业务服务器发送这笔交易的异步通知。
正确设置异步通知地址
使用 alipay-sdk-java 创建小程序支付交易时,需要在请求对象上设置 notify_url。不要把它放进业务参数,也不能用 return_url 代替。
AlipayTradeCreateRequest request = new AlipayTradeCreateRequest();
AlipayTradeCreateModel model = new AlipayTradeCreateModel();
model.setOutTradeNo(outTradeNo);
model.setTotalAmount("0.01");
model.setSubject("测试商品");
model.setBuyerId(buyerId);
request.setBizModel(model);
// 必须设置在 request 上
request.setNotifyUrl("https://example.com/alipay/notify");
AlipayTradeCreateResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
String tradeNo = response.getTradeNo();
// 将 tradeNo 返回给小程序,由前端调用支付能力
} else {
// 记录 code、subCode、msg、subMsg,便于定位问题
}
还要检查最终发送到支付宝网关的请求,确认其中确实包含 notify_url。只在应用后台配置网关地址,通常不能代替单笔支付请求中的 notify_url。
检查通知地址
通知地址应满足这些条件:
- 使用完整的公网 URL,例如
https://example.com/alipay/notify。 - 不能使用
localhost、127.0.0.1、局域网 IP,也不能是只能通过 VPN 访问的地址。 - 不应跳转到登录页,也不要返回
301或302。 - 接口应允许支付宝直接发送
POST请求。 - 不要要求 Cookie、Token、自定义请求头或登录状态。
- 检查 Nginx、WAF、CDN、防火墙和安全组有没有拦截请求。
- 确认域名解析、TLS 证书和服务端口均正常。
- 排查应用日志时,也要查看网关访问日志,以免请求已经到达 Nginx,却没有进入 Java 应用。
可以从公网环境模拟访问通知地址。不过,模拟请求只能验证网络是否连通,不能证明支付宝发送过通知。
验签并返回 success
支付宝会以表单参数的形式提交异步通知。后端需要先取得全部参数,再使用支付宝公钥验签。验签通过后,还要核对订单信息。
@PostMapping("/alipay/notify")
public void alipayNotify(
HttpServletRequest request,
HttpServletResponse response) throws IOException {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry
: request.getParameterMap().entrySet()) {
String[] values = entry.getValue();
params.put(
entry.getKey(),
values == null ? "" : String.join(",", values)
);
}
boolean signVerified;
try {
signVerified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
} catch (AlipayApiException e) {
signVerified = false;
}
if (!signVerified) {
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().print("failure");
return;
}
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");
// 必须核对 app_id、out_trade_no、total_amount、seller_id 等关键字段
// 订单更新必须支持幂等,避免重复通知导致重复记账
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
// 根据 outTradeNo 查询本地订单
// 核对金额和收款方
// 在事务中更新订单状态
}
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().print("success");
}
接口必须返回纯文本 success。如果返回的是 JSON、HTML、空字符串、异常页面或其他内容,平台可能会把本次处理视为失败,并按平台策略重试。
通过主动查询补偿
不能把异步通知作为确认支付结果的唯一依据。前端显示支付成功后,后端可以调用 alipay.trade.query 主动查询交易状态。定时任务也应扫描长时间停留在待支付状态的订单。
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(outTradeNo);
// 也可以根据实际情况使用 model.setTradeNo(tradeNo);
request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
String tradeStatus = response.getTradeStatus();
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
// 核对金额、商户订单号、应用和收款方后,幂等更新订单
}
} else {
// 记录 code、subCode、msg、subMsg
}
主动查询只是补偿机制,仍然需要校验金额、商户订单号、应用身份和收款方。
建议的排查顺序
- 查看创建交易时的完整请求日志,确认其中包含正确的
notify_url。 - 查询这笔交易的实际状态,确认状态是
TRADE_SUCCESS或TRADE_FINISHED,不要只根据前端提示判断。 - 查看通知域名对应的 Nginx、负载均衡和应用访问日志。
- 检查通知接口是否被鉴权、CSRF、防火墙或 WAF 拦截。
- 确认接口没有发生重定向,并且能够快速返回纯文本
success。 - 核对支付宝公钥、签名类型和字符集是否与实际配置一致。
- 如果公网入口完全没有这笔交易的访问记录,可通过支付宝商家平台或技术支持渠道,结合交易号、应用
app_id、商户订单号和交易创建时间查询平台侧通知记录。
注意事项
notify_url是异步通知地址,return_url是页面跳转地址,不能混用。- 不要只根据小程序前端回调把订单标记为已支付。
- 通知可能重复到达,订单处理必须支持幂等。
- 验签前不要自行拼接参数,也不要把通知转换成 JSON。
- 如果业务处理耗时较长,应先可靠保存通知,再异步处理业务逻辑。
- SDK 版本通常不会造成某一笔交易完全不发送通知。应先检查请求参数、网络入口和接口响应。如果准备升级 SDK,还需要结合当前 Java 版本和支付宝开放平台的兼容要求单独验证。
备注:内容仅供参考。
