支付宝订单支付成功后次年仍收到回调,是 bug 吗?
结论
支付成功一年后,又收到同一订单的异步通知,这通常不是支付宝的常规重试,但也不能只凭订单号相同就断定是支付宝的 bug。
常见原因有:
- 商户没有正确返回纯文本
success,支付宝在规定周期内重试。不过,正常重试一般不会持续到次年。 - 商户接口、网关或消息队列发生积压,导致旧请求延迟处理或被重新投递。
- 回调请求遭到伪造或重放。
- 另一笔订单复用了相同的商户订单号。
- 日志时间、运行环境或应用配置混淆,例如把测试环境日志当成生产环境日志。
- 商户自己的补偿任务或通知转发服务重新推送了历史数据。
如果已经确认请求验签通过、来自生产环境,而且确实是支付宝在一年后新发出的通知,应携带支付宝交易号、商户订单号和通知时间,联系支付宝技术支持核查。
为什么支付回调可能重复出现
支付宝异步通知采用“至少送达一次”的机制。如果商户通知接口没有按要求返回 success,支付宝可能再次发送通知。因此,回调接口必须能处理同一笔交易的多次调用。
即使第一次通知已经处理成功,也可能出现重复通知,常见情况包括:
- 数据库已经提交,但接口在返回
success前超时。 - 负载均衡器返回
502、504等错误。 - 接口返回了 JSON、HTML 或带空格的字符串,而不是支付宝要求的成功响应。
- 应用处理时间过长,支付宝没有及时收到响应。
- 商户内部的消息队列触发了重试。
支付宝自动重试通常有明确的时间窗口,不会按年持续重试。具体重试频率和持续时间,应以当前接入产品的官方文档为准。
建议的排查步骤
1. 保留原始回调信息
日志至少应记录以下字段:
app_idseller_idout_trade_notrade_notrade_statustotal_amountnotify_idnotify_timesign_type- 请求到达服务器的时间
- 网关生成的请求 ID
- 验签结果
不要在日志中记录私钥、访问令牌等敏感信息。
2. 检查请求是否验签通过
来源 IP、请求头和订单号都不能用于确认请求是否来自支付宝。必须使用支付宝公钥验证 sign。
验签失败时,应直接拒绝处理,不得更新订单状态。
3. 核对交易归属
即使验签成功,也要继续核对以下信息:
app_id是否属于当前应用。seller_id是否属于当前收款账号。out_trade_no是否存在。trade_no是否与数据库记录一致。total_amount是否与订单金额一致。trade_status是否属于业务允许处理的状态。
如果只验签,不检查这些字段,仍有可能误处理来自其他应用或其他商户账号的有效通知。
4. 检查商户侧链路
从入口网关开始追踪请求,确认它是否真的经过支付宝通知地址对应的公网入口。重点检查以下环节:
- API 网关是否重放过历史请求。
- 消息队列是否发生死信恢复或延迟重投。
- 定时补偿任务是否重新调用了回调处理接口。
- 日志平台是否使用了错误的时间字段。
- 测试环境和生产环境是否写入了同一个日志索引。
5. 主动查询支付宝交易状态
遇到时间跨度很长的异常通知时,不要只依据回调内容判断。可以调用 alipay.trade.query,根据 out_trade_no 或 trade_no 查询当前交易状态,再与本地记录比对。
查询接口只能用于辅助校验,不能代替回调验签。
Java 处理示例
下面的示例包含回调处理的核心逻辑。实际使用时,类名和配置方式需要根据当前使用的支付宝 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) -> {
if (values != null && values.length > 0) {
params.put(key, String.join(",", values));
}
});
boolean signVerified;
try {
signVerified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
} catch (AlipayApiException e) {
log.error("Alipay notification signature verification failed", e);
return "failure";
}
if (!signVerified) {
log.warn("Rejected Alipay notification: invalid signature");
return "failure";
}
String appId = params.get("app_id");
String sellerId = params.get("seller_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");
if (!expectedAppId.equals(appId) || !expectedSellerId.equals(sellerId)) {
log.warn("Rejected Alipay notification: merchant identity mismatch");
return "failure";
}
Order order = orderService.findByOutTradeNo(outTradeNo);
if (order == null) {
log.warn("Rejected Alipay notification: order not found, outTradeNo={}",
outTradeNo);
return "failure";
}
if (new BigDecimal(totalAmount).compareTo(order.getPayAmount()) != 0) {
log.warn("Rejected Alipay notification: amount mismatch, outTradeNo={}",
outTradeNo);
return "failure";
}
if (!"TRADE_SUCCESS".equals(tradeStatus)
&& !"TRADE_FINISHED".equals(tradeStatus)) {
// 对不需要处理的合法状态,也应根据业务规则及时确认接收。
return "success";
}
try {
orderService.confirmPaidIdempotently(
outTradeNo,
tradeNo,
new BigDecimal(totalAmount)
);
// 已经处理过的重复通知也应返回 success。
return "success";
} catch (Exception e) {
log.error("Failed to process Alipay notification, outTradeNo={}",
outTradeNo, e);
// 处理未完成时不要返回 success,否则支付宝会认为通知已成功接收。
return "failure";
}
}
订单更新必须具备幂等性。例如,可以为支付宝交易号设置唯一约束,并通过条件更新避免重复执行业务:
UPDATE payment_order
SET pay_status = 'PAID',
alipay_trade_no = :tradeNo,
paid_at = CURRENT_TIMESTAMP
WHERE out_trade_no = :outTradeNo
AND pay_status = 'UNPAID';
如果更新行数为 0,需要继续确认订单是否已经支付。订单若已使用相同的 trade_no 完成处理,应将其视为正常的重复通知并返回 success。如果交易号、金额或商户信息不一致,则应记录异常并停止处理。
注意事项
- 回调接口必须具备幂等性,不能以“只会通知一次”为设计前提。
- 收到回调后,不要重复发货、充值、增加积分或发送优惠券。
- 只有验签、订单核对和本地事务都成功后,才能返回纯文本
success。 - 对已经处理过的合法重复通知,也要返回
success,否则可能继续触发重试。 - 不要只根据
notify_time判断请求真伪,验签和业务字段校验才是主要依据。 - 比较金额时应使用
BigDecimal,不要使用double。 - 应保留异常请求的原始参数,同时对敏感信息进行脱敏。
- 如果确认请求验签成功,且没有经过商户内部重放,应通过支付宝后台或技术支持核对通知记录,不要直接把订单重新置为已支付。
备注:内容仅供参考。
