PAYATHON 2026

支付宝订单支付成功后次年仍收到回调,是 bug 吗?

支付老李

结论

支付成功一年后,又收到同一订单的异步通知,这通常不是支付宝的常规重试,但也不能只凭订单号相同就断定是支付宝的 bug。

常见原因有:

  • 商户没有正确返回纯文本 success,支付宝在规定周期内重试。不过,正常重试一般不会持续到次年。
  • 商户接口、网关或消息队列发生积压,导致旧请求延迟处理或被重新投递。
  • 回调请求遭到伪造或重放。
  • 另一笔订单复用了相同的商户订单号。
  • 日志时间、运行环境或应用配置混淆,例如把测试环境日志当成生产环境日志。
  • 商户自己的补偿任务或通知转发服务重新推送了历史数据。

如果已经确认请求验签通过、来自生产环境,而且确实是支付宝在一年后新发出的通知,应携带支付宝交易号、商户订单号和通知时间,联系支付宝技术支持核查。

为什么支付回调可能重复出现

支付宝异步通知采用“至少送达一次”的机制。如果商户通知接口没有按要求返回 success,支付宝可能再次发送通知。因此,回调接口必须能处理同一笔交易的多次调用。

即使第一次通知已经处理成功,也可能出现重复通知,常见情况包括:

  • 数据库已经提交,但接口在返回 success 前超时。
  • 负载均衡器返回 502、504 等错误。
  • 接口返回了 JSON、HTML 或带空格的字符串,而不是支付宝要求的成功响应。
  • 应用处理时间过长,支付宝没有及时收到响应。
  • 商户内部的消息队列触发了重试。

支付宝自动重试通常有明确的时间窗口,不会按年持续重试。具体重试频率和持续时间,应以当前接入产品的官方文档为准。

建议的排查步骤

1. 保留原始回调信息

日志至少应记录以下字段:

  • app_id
  • seller_id
  • out_trade_no
  • trade_no
  • trade_status
  • total_amount
  • notify_id
  • notify_time
  • sign_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。
  • 应保留异常请求的原始参数,同时对敏感信息进行脱敏。
  • 如果确认请求验签成功,且没有经过商户内部重放,应通过支付宝后台或技术支持核对通知记录,不要直接把订单重新置为已支付。

备注:内容仅供参考。