手机网站支付支付宝服务器异步通知延迟的原因是什么?
结论
支付宝异步通知并非实时回调,可能延迟,也可能重复发送。示例订单从付款到记录通知触发相隔约 86 分钟。仅根据交易号和时间还不能确定原因,常见情况有:
- 商户通知地址暂时无法访问、响应超时,或者没有返回纯文本
success,支付宝随后发起重试。 - 商户网关、负载均衡、WAF、防火墙或应用线程池短时异常。
- 支付宝侧的通知任务排队或出现短时波动。
- 商户记录的是业务处理完成时间,并非 HTTP 请求实际到达时间。
业务系统不应把异步通知作为支付结果的唯一来源。如果订单长时间停留在“待支付”状态,应主动调用 alipay.trade.query 查询交易状态。无论处理异步通知还是主动查询,都要保证幂等性。
如何判断延迟发生在哪一侧
先核对三个时间:
- 支付宝交易的实际付款时间。
- 商户服务器首次收到通知请求的时间。
- 商户订单更新为已支付的时间。
该订单的时间如下:
- 付款时间:
2020-09-03 21:54 - 记录的触发通知时间:
2020-09-03 23:20 - 时间差:约 86 分钟
如果商户 Web 服务器的访问日志显示,通知请求直到 23:20 才第一次到达,那么延迟发生在通知发出前或传输途中,还需结合支付宝侧的通知记录继续排查。
如果日志显示通知早已到达,但订单直到 23:20 才更新,问题通常出在商户应用内部,例如验签失败、数据库锁等待、消息积压、事务回滚或异常重试。
同时要确认支付宝是否发送过多次通知。如果早期请求已经到达,但商户返回的不是 success、HTTP 状态异常或响应超时,那么 23:20 收到的可能只是重试,不一定是首次通知。
建议的排查步骤
1. 检查入口访问日志
使用商户订单号和支付宝交易号搜索 Nginx、网关及应用日志:
rg 'UZFB20200903215430707613676|2020090322001438751425798479' access.log application.log
重点检查:
- 第一次请求到达的时间。
- HTTP 状态码及请求耗时。
- 是否出现
499、502、503、504等异常状态码。 - 相同
trade_no是否收到过重复通知。 - 验签、金额校验和数据库事务是否成功。
- 接口实际返回的内容是否为纯文本
success。
2. 检查通知接口的响应
通知处理成功后,接口应直接返回:
success
不要返回 JSON、HTML、额外空格或页面模板。下面这种响应不符合要求:
{"code":200,"message":"success"}
处理失败时,可以返回 failure 或其他非成功响应,让支付宝按照通知机制重试。接口也不能长时间阻塞,否则即使本地业务最终处理成功,支付宝仍可能把本次通知判定为失败。
3. 主动查询交易状态
订单超过预期时间仍处于“待支付”状态时,可以调用 alipay.trade.query 查询。请求通常传入 out_trade_no 或 trade_no,具体字段以项目当前使用的支付宝 SDK 和接口文档为准。
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent(
"{\"out_trade_no\":\"UZFB20200903215430707613676\"}"
);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
String tradeStatus = response.getTradeStatus();
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
// 校验订单金额、商户身份等信息后,幂等地更新订单状态
markOrderPaidIdempotently(
response.getOutTradeNo(),
response.getTradeNo(),
response.getTotalAmount()
);
}
}
查询接口调用成功不等于订单已经支付。系统必须继续检查 trade_status,并核对订单号、金额和收款方身份,不能直接把订单改为已支付。
4. 正确处理异步通知
下面是简化后的 Java 处理结构。SDK 类名和支付宝公钥的获取方式需要按照项目的实际配置调整:
@PostMapping(value = "/alipay/notify", produces = "text/plain;charset=UTF-8")
public String alipayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> {
params.put(key, String.join(",", values));
});
try {
boolean valid = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!valid) {
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. total_amount 是否与订单金额一致
// 3. app_id、seller_id 是否属于当前商户
// 4. trade_no 是否存在冲突
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
updateOrderIdempotently(
outTradeNo,
tradeNo,
totalAmount,
appId,
sellerId
);
}
return "success";
} catch (Exception exception) {
// 记录异常,但不要记录私钥等敏感信息
return "failure";
}
}
updateOrderIdempotently 应使用唯一约束、条件更新或状态机来防止重复处理。例如:
UPDATE payment_order
SET status = 'PAID',
alipay_trade_no = :trade_no,
paid_at = :paid_at
WHERE out_trade_no = :out_trade_no
AND status = 'WAIT_PAY';
如果更新行数为 0,需要判断订单是已经处理过,还是根本不存在或数据不一致,不能一律按成功处理。
接口设计注意事项
- 不要依赖通知的到达顺序,系统应能安全处理重复通知和延迟通知。
- 使用异步通知中的金额前,必须与本地订单金额进行核对。
- 验签应使用支付宝公钥,不能使用应用私钥或应用公钥。
- 通知参数应按照支付宝 SDK 的要求原样参与验签,不要自行拼接,以免验签失败。
- 日志中应记录通知原始参数、首次接收时间、验签结果、处理结果和响应耗时,同时过滤敏感数据。
- 通知接口只处理必要的校验和可靠落库。发货、积分、短信等耗时操作应交给内部消息队列。
- 前端同步跳转只能改善用户体验,不能作为可信的支付成功依据。
- 主动查询需要限制频率,并设置明确的订单补偿周期,避免无限轮询。
- 不能根据用户截图或客户端回传结果直接确认支付成功。
如果确认商户侧在 23:20 之前没有收到任何请求,而且通知地址当时始终可以访问,应向支付宝技术支持提供 trade_no、out_trade_no、付款时间、首次收到通知的时间及商户网关日志,请平台查询该笔交易的通知投递记录。在缺少投递记录的情况下,不能仅凭 86 分钟的时间差认定支付宝系统发生异常。
备注:内容仅供参考。
