PAYATHON 2026

一年前的支付宝订单为何近期再次触发异步通知

支付小周

结论

一年前已经完成的支付宝订单,最近又命中了异步通知接口,并不代表支付宝刚刚主动发送了一条新通知。

可能的原因有:

  • 历史通知被网关、消息队列、补偿任务或人工操作重新投递。
  • 有人或某个程序重放了之前保存的通知请求。
  • 订单最近发生退款、关闭、争议处理等状态变化,相关产品再次发送了业务通知。
  • 支付宝当时没有收到正确的 success 响应,因此触发重试。不过,普通支付通知时隔一年仍在重试,通常不符合常见的重试行为,具体要以所用产品的官方通知规则为准。
  • 通知接口遭到扫描或伪造调用。如果验签失败,就不能认定请求来自支付宝。

即使验签成功,也只能说明参数与签名能够匹配支付宝公钥,无法单凭这一点证明请求是在本次访问时由支付宝新生成的。如果有人完整重放历史上的合法通知,原有签名仍可能通过验证。

如何判断通知来源

先在服务器原始访问日志中找到这次请求,保留完整的 POST 参数、接收时间、请求头和链路日志,再重点检查这些字段:

  • out_trade_no:商户订单号。
  • trade_no:支付宝交易号。
  • trade_status:通知携带的交易状态。
  • notify_time:通知声明的发送时间。
  • gmt_payment:支付完成时间。
  • app_id:是否属于当前应用。
  • seller_id:是否属于当前收款账号。
  • total_amount:是否与本地订单金额一致。
  • sign、sign_type:用于验证通知签名。

可以按照下面的顺序判断:

  1. 使用支付宝公钥完成 RSA2 验签。
  2. 核对 app_id、seller_id、订单号和金额。
  3. 查询本地订单,确认相同状态是否已经处理。
  4. 必要时调用支付宝交易查询接口,核实当前交易状态,不要只凭通知修改订单。
  5. 检查反向代理、API 网关、消息队列和定时补偿任务,确认是否存在请求重放。
  6. 与一年前的通知日志进行对比。如果参数和签名完全相同,很可能是历史请求被重放。

来源 IP 只能作为排查线索,不能作为判断请求是否可信的依据。代理转发、网络架构变化和 IP 伪造风险都会影响判断,最终还是要看验签结果和订单数据是否一致。

正确处理方式

异步通知接口必须具备幂等性。同一订单、同一交易状态无论收到多少次通知,都只能产生一次业务结果。

处理流程可以是:

接收通知
  -> 验签
  -> 核对 app_id、seller_id、订单号和金额
  -> 查询本地订单
  -> 已处理则直接返回 success
  -> 未处理则在事务中更新订单并记录通知
  -> 提交成功后返回 success

如果验签失败、金额不一致或商户身份不匹配,应停止业务处理并记录安全日志。不能因为接口收到请求,就直接把订单改为已支付。

Java 验签示例

下面的示例使用支付宝 Java SDK 中常见的 AlipaySignature.rsaCheckV1 方法。实际包名、SDK 版本和配置方式要以项目当前使用的依赖为准。

import com.alipay.api.internal.util.AlipaySignature;

import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

public boolean verifyAlipayNotify(
        HttpServletRequest request,
        String alipayPublicKey,
        String charset) throws Exception {

    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)
        );
    }

    String signType = params.getOrDefault("sign_type", "RSA2");

    return AlipaySignature.rsaCheckV1(
            params,
            alipayPublicKey,
            charset,
            signType
    );
}

通过验签后,还要继续核对业务参数:

public void validateBusinessParams(
        Map<String, String> params,
        Order order,
        String expectedAppId,
        String expectedSellerId) {

    if (!expectedAppId.equals(params.get("app_id"))) {
        throw new IllegalArgumentException("app_id mismatch");
    }

    if (!expectedSellerId.equals(params.get("seller_id"))) {
        throw new IllegalArgumentException("seller_id mismatch");
    }

    if (!order.getOutTradeNo().equals(params.get("out_trade_no"))) {
        throw new IllegalArgumentException("out_trade_no mismatch");
    }

    if (order.getTotalAmount().compareTo(
            new java.math.BigDecimal(params.get("total_amount"))) != 0) {
        throw new IllegalArgumentException("total_amount mismatch");
    }
}

金额不要转换成 double 后再比较,应使用 BigDecimal,以免出现浮点误差。

幂等处理示例

可以在支付通知记录表中建立唯一约束,例如将支付宝交易号和交易状态组合为幂等键:

CREATE UNIQUE INDEX uk_alipay_trade_status
ON payment_notify_record (trade_no, trade_status);

业务处理示意:

@Transactional
public void handleNotify(Map<String, String> params) {
    String tradeNo = params.get("trade_no");
    String outTradeNo = params.get("out_trade_no");
    String tradeStatus = params.get("trade_status");

    if (notifyRecordRepository.exists(tradeNo, tradeStatus)) {
        return;
    }

    Order order = orderRepository.lockByOutTradeNo(outTradeNo);
    if (order == null) {
        throw new IllegalStateException("order not found");
    }

    if ("TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus)) {

        if (!order.isPaid()) {
            order.markPaid(tradeNo);
            orderRepository.save(order);
        }
    }

    notifyRecordRepository.insert(tradeNo, tradeStatus, params);
}

只有数据库事务成功提交后,接口才能返回以下纯文本:

success

如果先返回 success,之后业务处理失败,支付宝通常不会针对本地处理失败自动补偿。反过来,如果业务已经处理成功,但接口返回了其他内容,就可能再次收到通知。

排查重点

如果确认收到的是一年前的支付成功通知,可以重点检查:

  • API 网关是否配置了失败重试或流量回放。
  • 消息队列是否存在积压、死信恢复或重新消费。
  • 定时补偿程序是否重新请求过通知地址。
  • 运维平台是否执行过历史请求重放。
  • 是否有人通过抓包数据、日志参数或测试工具重新发送请求。
  • 通知地址是否曾被其他系统共用。
  • 本次请求中的 notify_time 是否也在一年前。
  • 本次参数和签名是否与历史通知完全一致。

如果 notify_time、签名和全部业务参数都与历史记录一致,更可能是旧请求被重放。如果通知时间是近期,而且交易查询结果也表明订单出现了新的状态变化,则需要继续核对对应支付宝产品的通知规则。

注意事项

  • 不要因为 notify_time 不够新就直接拒绝通知,否则可能误伤合法的延迟通知。时间只能用于风险判断。
  • 不要把客户端同步跳转结果作为支付成功的依据。
  • 验签使用支付宝公钥,不是应用私钥,也不是应用公钥。
  • 验签前不要随意修改、解码或重新拼接参数。
  • 日志中的敏感信息应当脱敏,同时保留足够的订单号、交易号、签名摘要和接收时间,以便追踪。
  • 已完成订单再次收到通知时,应记录请求并直接返回 success,不能重复发货、重复记账或重复增加余额。
  • 如需向支付宝技术支持确认,应提供 trade_no、out_trade_no、通知接收时间、notify_time、验签结果和请求链路日志。

备注:内容仅供参考。