PAYATHON 2026

支付成功回调地址收到一年前订单的回调事件

支付小周

结论

订单已经支付成功,即使接口返回过 success,之后仍可能再次收到支付成功回调。支付回调通常采用“至少投递一次”机制,因此,重复通知不代表订单被重复支付。

业务系统应对支付回调做幂等处理。收到回调后,先验签并核对订单信息,再检查订单状态。对于已经处理成功的订单,只需返回支付平台规定的成功响应,不能再次发货、充值、记账或发放权益。

不过,时隔一年再次收到通知并不常见。除了平台重试,还需要排查人工补发、对账补偿、订单号复用、日志时间错误和伪造请求等情况。

为什么会再次收到回调

平台没有确认收到成功响应

业务接口执行了 return success,不代表支付平台一定成功收到了响应。常见情况包括:

  • 返回响应时连接已经中断;
  • 网关、负载均衡或 CDN 返回超时、502、504;
  • 响应正文、状态码或 Content-Type 不符合平台要求;
  • 应用日志记录了 success,但响应被代理层修改或拦截;
  • 接口处理时间过长,平台在收到响应前已经判定超时。

支付平台通常会按照既定策略重试。至于通知是否可能持续或延迟一年,需要查看具体平台的通知规则,不能只根据重试机制判断。

平台执行了补偿或重新通知

部分支付平台可能因为对账修复、历史数据补偿、人工操作或系统迁移而重新发送通知。是否存在这种机制,需要向对应平台核实,并提供商户号、订单号、平台交易号和本次通知时间。

商户订单号被重复使用

如果业务系统复用了旧订单号,新交易或测试请求可能被错误关联到一年前的订单。排查时应同时比较:

  • 商户订单号;
  • 支付平台交易号;
  • 支付金额和币种;
  • 商户号、应用 ID;
  • 支付完成时间;
  • 回调中的通知 ID 或事件 ID。

仅凭商户订单号,无法确认是不是同一笔支付。

请求并非来自支付平台

攻击者可能重放历史通知,也可能自行构造回调请求。如果系统没有严格验签,只根据请求参数更新订单,就可能造成重复发货或订单数据被篡改。

来源 IP 只能用于辅助判断,不能替代签名验证。如果对请求有疑问,应通过支付平台的订单查询 API 主动核实交易状态。

时间字段或日志存在误判

还需要确认“一年前”具体指订单创建时间、支付时间,还是回调参数中的时间。服务器时区错误、日志采集延迟、消息队列积压或历史日志重新导入,都可能导致时间判断出现偏差。

建议的排查顺序

1. 保存本次回调原始信息

至少保留以下内容:

  • 原始请求体,不要重新序列化;
  • 请求头和请求时间;
  • 商户订单号、平台交易号;
  • 通知 ID、事件 ID;
  • 金额、币种和支付状态;
  • 签名、时间戳、随机串;
  • 接口最终返回的 HTTP 状态码和响应正文;
  • 网关及应用服务器的访问日志。

日志中的敏感字段应脱敏。密钥、证书私钥和完整身份信息不得写入日志。

2. 验证签名和商户身份

按照对应支付平台的官方规则,使用原始请求体或平台规定的字段验签,同时核对商户号、应用 ID 和回调类型等信息。

验签失败时不能更新订单。此时应返回的状态码和响应正文,以支付平台协议为准。

3. 查询平台订单状态

调用平台提供的订单查询 API,根据平台交易号或商户订单号查询,并核对:

  • 交易是否真实存在;
  • 当前状态是否为支付成功;
  • 支付金额和币种是否一致;
  • 支付时间是否确实为一年前;
  • 平台交易号是否与历史记录一致。

交易结果不能只依赖回调参数判断。

4. 检查历史响应链路

查看应用、反向代理、负载均衡和 API 网关日志,确认历史回调实际返回了什么。重点检查:

  • 是否确实返回 HTTP 200;
  • 响应正文是否完全符合平台要求;
  • 是否发生超时或连接重置;
  • 是否先返回成功,随后又抛出异常;
  • 是否存在多个回调地址或旧服务实例。

应用日志中的“已返回 success”只能证明代码执行到了相应位置,不能证明支付平台收到了有效确认。

5. 检查重复业务结果

查询订单关联的发货记录、余额流水、积分、优惠券和财务凭证,确认本次通知是否引起了重复处理。如果已经发生重复记账,应通过冲正或补偿流程修复,不能直接删除流水。

回调接口的幂等处理示例

以下是通用伪代码。验签方法、成功响应格式和字段名称都需要替换为支付平台的实际规定。

@PostMapping("/payment/callback")
public ResponseEntity<String> callback(
        @RequestBody String rawBody,
        @RequestHeader Map<String, String> headers) {

    // 必须使用平台规定的验签算法和原始请求数据
    PaymentEvent event = paymentVerifier.verifyAndParse(rawBody, headers);
    if (event == null) {
        return ResponseEntity.status(401).body("invalid signature");
    }

    paymentService.handleCallback(event);

    // 必须替换成平台要求的状态码和响应正文
    return ResponseEntity.ok("success");
}

业务处理应放在数据库事务中,并通过唯一约束或行锁防止并发重复执行:

@Transactional
public void handleCallback(PaymentEvent event) {
    Order order = orderRepository.findByOrderNoForUpdate(event.getOrderNo())
            .orElseThrow(() -> new IllegalStateException("order not found"));

    if (!order.getMerchantId().equals(event.getMerchantId())) {
        throw new IllegalStateException("merchant mismatch");
    }

    if (order.getAmount().compareTo(event.getAmount()) != 0) {
        throw new IllegalStateException("amount mismatch");
    }

    if (!"SUCCESS".equals(event.getPaymentStatus())) {
        return;
    }

    // 已支付订单直接结束,不能重复执行发货、充值等操作
    if ("PAID".equals(order.getStatus())) {
        return;
    }

    int updated = orderRepository.markPaidIfUnpaid(
            order.getOrderNo(),
            event.getTransactionId(),
            event.getPaidAt()
    );

    if (updated == 1) {
        fulfillmentService.createOnce(order.getOrderNo());
    }
}

数据库还应设置唯一约束,作为最后一道防线:

ALTER TABLE payment_notification
ADD CONSTRAINT uk_payment_notification_event
UNIQUE (provider, merchant_id, event_id);

ALTER TABLE payment_transaction
ADD CONSTRAINT uk_payment_transaction_id
UNIQUE (provider, merchant_id, transaction_id);

ALTER TABLE fulfillment
ADD CONSTRAINT uk_fulfillment_order
UNIQUE (order_no, fulfillment_type);

如果平台没有稳定的 event_id,可以用平台交易号和事件类型组成幂等键。但在这样处理前,必须确认同一笔交易是否允许产生多个合法事件,不能直接去重。

注意事项

  • 即使订单已经支付,也不能跳过验签。重复通知同样可能来自伪造请求。
  • 不要先返回 success,再交给不可靠的异步任务处理业务。如果使用异步模式,应先将事件可靠地写入本地表或消息系统。
  • 收到支付成功通知,不代表只能执行一次发货逻辑。发货、充值和积分等下游操作都需要各自实现幂等。
  • 回调接口应尽快完成验签、落库和状态更新,耗时操作应交给可靠的异步流程。
  • 对于时隔一年的通知,完成本地核查后,建议携带脱敏后的通知 ID、平台交易号、商户订单号和接收时间,向支付平台确认是否属于补偿或人工重发。

备注:内容仅供参考。