PAYATHON 2026

支付回调异常:收到大量去年已完成订单的回调

支付阿杰

结论

去年已经完成的订单再次收到支付回调,技术上确实可能发生,但如果短时间内大量出现,就不能简单当作正常现象。原因可能是支付平台补偿重试、历史消息重新投递、内部队列重复消费,也可能是伪造请求或重放攻击。

不论回调来自哪里,只要订单已经完成,系统就不能再次发货、充值或记账。处理时应先验签,再核对订单状态,并通过幂等机制忽略重复通知。

可能的原因

支付平台重新投递

支付平台通常要求商户服务器返回指定的成功响应。如果历史回调没有得到平台认可,比如响应超时或返回内容不符合要求,平台可能继续补偿重试。

平台是否会在一年后重试,以及重试会持续多久,应以相应支付渠道的官方文档和技术支持答复为准。仅凭回调时间无法判断原因。

消息队列或定时任务重复执行

如果回调经过消息队列、任务调度系统或失败补偿表,以下问题都可能让历史消息再次进入处理流程:

  • 消费成功后没有正确提交确认;
  • 死信消息被重新放回主队列;
  • 补偿任务的筛选条件有误;
  • 队列迁移或服务重启后发生重复消费;
  • 数据库中的”待处理”状态没有及时更新。

对方批量补发或数据修复

支付服务商在数据迁移、故障恢复或账务对账期间,可能重新发送历史通知。要确认是否发生过这类操作,需要联系支付渠道,并提供回调时间、商户订单号和支付流水号。

伪造请求或重放攻击

攻击者可能复制以前有效的回调内容,再次请求商户接口。也有可能是扫描器构造了相似参数。订单号真实、金额正确,并不能证明请求一定来自支付平台。

建议的排查步骤

1. 保存原始请求信息

建议记录以下信息,但不要在日志中明文保存密钥、完整银行卡号等敏感内容:

  • 请求时间和来源 IP;
  • HTTP 方法、请求路径和必要的请求头;
  • 原始请求体;
  • 商户订单号;
  • 支付平台交易号;
  • 回调中的支付状态和金额;
  • 签名验证结果;
  • 当前订单状态;
  • 接口最终返回值和响应耗时。

验签通常要使用原始请求体。如果框架提前解析 JSON 后又重新序列化,字段顺序或空白字符可能发生变化,导致验签失败。

2. 检查回调是否通过验签

签名验证必须严格遵循支付平台文档,包括证书、时间戳、随机串、签名算法和报文格式。不要自行认定所有平台都使用 HMAC,也不能只靠来源 IP 判断请求是否可信。

如果大量历史回调无法通过验签,应先按异常请求或重放攻击处理,不得更新订单。

3. 主动查询支付结果

对于通过验签但行为异常的回调,应调用支付平台提供的订单查询 API,核对以下信息:

  • 平台交易号是否真实;
  • 商户订单号是否匹配;
  • 实付金额和币种是否一致;
  • 收款商户是否为当前商户;
  • 支付状态是否确实成功。

回调适合用来通知状态变化。出现冲突或异常时,订单查询结果通常更适合作为核对依据,但最终处理方式仍应遵循相应支付平台的文档。

4. 检查平台认可的响应格式

查看历史日志,确认服务器是否在规定时间内返回了平台要求的成功响应。有的平台要求返回特定文本,有的平台要求 JSON 或 XML。即使业务已经处理成功,只要响应超时或格式不正确,平台仍可能把这次通知判定为失败。

不同支付渠道的响应要求可能不同,不能直接照搬其他渠道的返回值,应以当前渠道的文档为准。

5. 排查内部重试链路

按照消息 ID、商户订单号和平台交易号,检查以下记录:

  • 消息队列消费记录;
  • 死信队列;
  • 补偿任务执行记录;
  • 数据库事务日志;
  • 应用发布和服务重启记录;
  • 最近是否进行过数据迁移、消息回灌或人工重试。

如果外部网关日志中没有对应请求,应用内部却出现了回调处理记录,问题通常出在内部队列或补偿任务。

回调处理应保证幂等

支付回调可能重复到达,业务逻辑不能假设平台只会回调一次。可以在数据库中为支付平台交易号建立唯一约束:

CREATE UNIQUE INDEX uk_payment_channel_transaction
ON payment_record (payment_channel, channel_transaction_id);

更新订单状态时也要设置前置条件,只允许订单从未支付变为已支付:

UPDATE orders
SET payment_status = 'PAID',
    paid_at = :paid_at,
    channel_transaction_id = :channel_transaction_id
WHERE order_no = :order_no
  AND payment_status = 'UNPAID';

应用层可以通过受影响行数判断订单状态是否真的发生了变化。下面是通用伪代码,具体字段和验签方式需要根据实际支付渠道调整:

function handlePaymentCallback(rawBody, headers):
    if not verifySignature(rawBody, headers):
        return HTTP 401

    callback = parseCallback(rawBody)

    if callback.status != "SUCCESS":
        return providerAcknowledgement()

    result = queryPaymentFromProvider(callback.transactionId)

    if result.orderNo != callback.orderNo:
        return HTTP 400

    if result.amount != callback.amount or result.currency != callback.currency:
        return HTTP 400

    begin transaction

    affectedRows = updateOrderToPaidIfUnpaid(
        callback.orderNo,
        callback.transactionId,
        result.paidAt
    )

    if affectedRows == 1:
        createPaymentRecord(callback.transactionId)
        createFulfillmentTaskOnce(callback.orderNo)

    commit transaction

    return providerAcknowledgement()

createFulfillmentTaskOnce 同样必须保证幂等。只保护订单状态并不够,否则重复回调仍可能造成重复发货、重复发送优惠券或重复增加账户余额。

已完成订单应该如何处理

如果订单已经支付成功,且商户订单号、平台交易号、金额、币种和商户身份都一致,可以将这次回调记录为重复通知,不再执行后续业务,并返回支付平台规定的成功响应。

只要有任一关键字段不一致,就应停止自动处理并触发告警。例如:

  • 同一商户订单号对应了不同的平台交易号;
  • 回调金额与订单金额不一致;
  • 回调中的商户号不属于当前系统;
  • 已退款或已关闭的订单再次收到”支付成功”通知;
  • 回调时间戳明显异常;
  • 验签失败次数突然增加。

注意事项

不能因为订单已经完成就跳过验签。否则,攻击者可能利用已知订单号反复请求接口,污染日志,甚至触发有缺陷的后续逻辑。

发现重复订单后,也不要直接返回错误。对于真实且验签通过的重复通知,通常应返回渠道认可的成功响应,避免平台继续重试。具体返回内容必须以相应支付平台的文档为准。

在原因没有查清前,不建议直接封禁支付平台的出口 IP,也不要删除历史消息。应先保存请求证据,确认验签和订单查询结果,再判断问题来自平台补发、内部重复消费还是恶意请求。

备注:内容仅供参考。