PC端扫码支付成功但未收到异步通知
结论
扫码支付成功,只能说明交易已经完成,不能说明商户服务器一定收到了异步通知,更不能说明通知已处理成功。网站配置了证书,也不代表支付平台能通过公网访问 notify_url。
订单号 2023050922001417181438452261 从格式看更像支付宝交易号 trade_no。仅凭这个号码无法判断支付宝是否发送过通知,还要查看支付宝侧的通知记录、网关访问日志和应用日志。常见原因包括:
- 最终支付请求中没有
notify_url; - 通知地址无法从公网访问,或者被跳转、鉴权、防火墙拦截;
- 网关收到了通知,但应用没有记录,或处理过程中失败;
- 验签参数、支付宝公钥或字符集配置有误;
- 接口没有返回纯文本
success,支付宝因此判定通知失败; - 业务处理已经完成,但本地订单更新事务回滚,或状态判断有误。
问题解决前,应主动调用交易查询接口确认支付状态并补单,不能只依赖异步通知。
第一步:确认订单号类型
支付宝支付通常会涉及两个订单号:
out_trade_no:商户生成的订单号;trade_no:支付宝生成的交易号。
2023050922001417181438452261 更可能是 trade_no。排查时还要找到对应的 out_trade_no,因为本地订单通常通过 out_trade_no 关联。
可以使用 alipay.trade.query 查询:
{
"trade_no": "2023050922001417181438452261"
}
也可以使用商户订单号:
{
"out_trade_no": "商户订单号"
}
主要查看返回的 trade_status:
TRADE_SUCCESS:支付成功;TRADE_FINISHED:交易完成;WAIT_BUYER_PAY:等待付款;TRADE_CLOSED:交易关闭。
如果查询结果是 TRADE_SUCCESS 或 TRADE_FINISHED,但本地订单仍显示未支付,就应执行幂等补单。
第二步:确认最终请求中确实包含 notify_url
不能只看代码中的配置项。应记录实际提交给支付宝网关的请求参数,确认其中包含正确的 notify_url。
例如:
notify_url=https://pay.example.com/alipay/notify
通知地址应满足以下条件:
- 可以从公网访问;
- 不使用
localhost、127.0.0.1或内网地址; - 不依赖 Cookie、登录状态或人工认证;
- 不包含 URL Fragment,例如
#notify; - 尽量不使用 HTTP 301、302 或 307 跳转;
- HTTPS 证书完整有效,域名与证书匹配;
- 网关、WAF、防火墙和反向代理允许支付宝发送 POST 请求;
- 接口可以接收
application/x-www-form-urlencoded表单参数。
如果使用 SDK,notify_url 通常属于公共请求参数,不应误放到 biz_content 中。
第三步:检查 Web 服务器访问日志
先确认通知有没有到达服务器,这一步可以把问题分成两类。
以 Nginx 为例:
grep "/alipay/notify" /var/log/nginx/access.log
需要查看:
- 是否出现 POST 请求;
- HTTP 状态码是否为
200; - 请求是否被重定向到登录页或 HTTPS 地址;
- 是否返回
403、404、405、499、500、502或504; - 请求是否被限流、WAF 或 CSRF 校验拦截。
如果访问日志里完全没有相关请求,应检查:
- 实际支付请求是否包含
notify_url; - 通知地址能否从公网访问;
- 支付宝侧是否有通知发送记录或失败记录;
- 域名解析、证书链、防火墙和反向代理配置是否正确。
如果访问日志中有请求,但业务日志没有记录,问题通常出在网关转发、路由、请求方法限制或应用启动阶段。
第四步:检查接口是否错误拦截通知
异步通知是服务器之间发送的 POST 请求,不会携带用户浏览器中的登录状态。以下中间件可能会误拦截通知:
- 登录认证;
- CSRF 校验;
- Referer 校验;
- User-Agent 白名单;
- IP 白名单;
- JSON 请求体强制校验;
- 全局参数过滤;
- 防重放策略。
例如,在 Spring Security 中,应结合项目的实际配置,对通知路径放行认证和 CSRF:
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/alipay/notify").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/alipay/notify")
);
通知接口仍然要验证支付宝签名。放行登录认证和 CSRF,并不等于跳过安全校验。
第五步:按原始表单参数验签
通知参数通常来自 POST 表单。不要先把参数转换成业务对象,再重新拼接用于验签,否则可能造成字段遗漏、编码变化或空值处理不一致。
Java 示例:
@PostMapping(
value = "/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));
});
try {
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
String appId = params.get("app_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、商户订单号、金额和收款方信息。
// 确认无误后,以 out_trade_no 为幂等键更新订单。
processPayment(outTradeNo, tradeNo, tradeStatus, totalAmount, appId);
return "success";
} catch (Exception e) {
// 记录异常及必要的排查信息,避免记录敏感数据。
return "failure";
}
}
示例中的 alipayPublicKey 指支付宝公钥,不是以下任何一种密钥:
- 应用公钥;
- 应用私钥;
- 网站 HTTPS 证书公钥。
如果使用支付宝公钥证书模式,应按照相应 SDK 的证书模式完成初始化和验签,不能与普通公钥模式混用。
第六步:完整校验业务参数
验签通过后,不能马上把订单改为支付成功。至少还要校验:
app_id是否属于当前应用;out_trade_no是否存在;total_amount是否与本地订单金额一致;- 收款方标识是否符合当前商户配置;
trade_status是否属于业务允许的成功状态;trade_no是否已经绑定到其他订单。
金额应使用十进制金额类型或最小货币单位进行比较,不要直接用浮点数判断:
BigDecimal notifiedAmount = new BigDecimal(totalAmount);
BigDecimal orderAmount = order.getPayAmount();
if (notifiedAmount.compareTo(orderAmount) != 0) {
throw new IllegalStateException("支付金额不一致");
}
第七步:正确返回 success
只有验签、订单校验和本地状态更新全部成功后,接口才能返回:
success
响应体最好只有小写纯文本 success。不要返回:
{"code":200,"message":"success"}
也不要返回 HTML 页面、调试信息或包含其他内容的字符串。
Spring 示例:
@PostMapping("/alipay/notify")
@ResponseBody
public String notify(HttpServletRequest request) {
// 验签并处理业务
return "success";
}
如果本地处理失败,应返回 failure 或其他非成功结果,让支付平台按照自身机制重新发送通知。具体重试次数和时间应以当前支付宝官方规则及后台记录为准,不要根据固定时间推测。
第八步:保证订单处理幂等
异步通知可能重复到达,主动查询和异步通知也可能同时更新同一笔订单,因此需要以订单号进行幂等控制。
UPDATE payment_order
SET
pay_status = 'PAID',
trade_no = :tradeNo,
paid_at = CURRENT_TIMESTAMP
WHERE
out_trade_no = :outTradeNo
AND pay_status = 'UNPAID';
如果受影响行数为 0,应重新查询订单:
- 订单已经是
PAID:按成功处理,不要重复发货; - 订单不存在:记录异常并返回失败;
- 金额或交易号存在冲突:停止处理并告警。
临时补救方案
对于当前订单,应立即通过 alipay.trade.query 主动查询。查询确认支付成功后:
- 校验商户订单号、金额、应用和收款方;
- 通过幂等事务把本地订单补记为已支付;
- 触发尚未执行的后续业务;
- 保存补单来源、支付宝交易号和查询结果摘要;
- 继续排查异步通知链路,避免后续订单再次漏单。
长期可增加定时对账或补偿任务,扫描用户已经付款但本地仍为待支付状态的订单,再通过交易查询接口确认状态。异步通知仍是主要处理入口,主动查询和账单对账用于容错。
备注:内容仅供参考。