这是一类必须先对账、再补偿的故障。最危险的处理方式,是在后台把订单状态直接改成 Processing:这样页面看起来正常,但 Magento 可能仍没有 Capture transaction、Invoice 和网关交易关联,后续退款会继续出错。

先冻结自动重试,建立一张订单的时间线

记录 Magento increment_id、entity_id、支付平台 transaction/payment ID、币种、金额和三个时间点:客户提交、网关成功、Webhook 到达。所有日志都应脱敏,不记录卡号、密钥或完整签名。

SELECT o.entity_id, o.increment_id, o.state, o.status,
       o.grand_total, o.order_currency_code,
       p.method, p.last_trans_id
FROM sales_order o
JOIN sales_order_payment p ON p.parent_id = o.entity_id
WHERE o.increment_id = '000001234';

SELECT transaction_id, parent_id, txn_id, parent_txn_id,
       txn_type, is_closed, created_at
FROM sales_payment_transaction
WHERE order_id = 1234
ORDER BY transaction_id;

网关成功但本地没有 capture/authorization 记录,说明回调或返回页没有完成业务动作;本地已有 transaction 但订单仍 Pending,查支付扩展的状态映射和异常回滚。

确认 Webhook 到没到服务器

grep 'PAYMENT_EVENT_ID' var/log/*.log
grep '/payment/webhook' /var/log/nginx/access.log | tail -n 50

访问日志没有请求:查网关后台的投递记录、DNS、防火墙和回调 URL。出现 301/302 时尤其要修复,很多网关不会对跨域跳转保留 POST body。出现 401/403:检查签名 secret、时间容差和 WAF;500:用 request ID 对应 exception.log。

在测试环境重放,不要伪造事件

优先使用支付平台提供的“重发 Webhook”功能,它会生成合法签名。若平台提供 CLI,使用同一 event ID 重放到预发布回调地址。不要从日志复制 JSON 后自己计算一个假签名,更不能在生产临时关闭签名校验。

回调处理至少需要下面的幂等边界:

// 伪代码:以网关 event_id 建立唯一记录
if ($eventRepository->exists($eventId)) {
    return; // 已处理事件直接确认,不重复开票
}

$this->transaction->begin();
$this->verifyAmountCurrencyAndOrder($payload, $order);
$this->captureOrRegisterPayment($order, $gatewayTransactionId);
$this->eventRepository->markProcessed($eventId);
$this->transaction->commit();

安全补单应该调用支付模块支持的服务

确认网关交易真实成功后,使用扩展提供的同步/重放命令或服务完成授权/捕获、Invoice、订单历史与邮件。若扩展没有补偿入口,应先在预发布写一次性脚本,通过 Repository 和 Payment API 处理,并以订单 ID + 网关交易 ID 做幂等。不要直接 UPDATE sales_order.state

补偿后做四方对账

  • Magento 订单金额、币种和已付金额正确;
  • Invoice 只生成一张,数量与金额正确;
  • sales_payment_transaction 中 txn_id 可追溯到网关;
  • 重复投递同一个 Webhook 不再开第二张 Invoice 或发第二封邮件。

最后修复导致漏回调的根因,并对故障时间范围内所有 Pending Payment 订单按网关交易批量对账。单独修一张订单不是结束。