支付平台在超时或未收到 2xx 时会重试回调,同一事件也可能并发到达两个 Web 节点。如果代码每次都 create invoice,就会重复邮件、重复 capture 或产生金额异常。

先固定复现条件

按订单号汇总 webhook 访问日志、payment transaction、invoice 和订单历史,确认重复请求的事件 ID、到达间隔与响应码。日志不能保存完整签名密钥或卡数据。

SELECT transaction_id,parent_txn_id,txn_type,is_closed,created_at FROM sales_payment_transaction WHERE order_id=123;
SELECT entity_id,increment_id,state,grand_total,created_at FROM sales_invoice WHERE order_id=123;
SELECT parent_id,status,comment,created_at FROM sales_order_status_history WHERE parent_id=123;

根据结果定位根因

只检查订单 status 不足以防并发,因为两个请求可能同时读到 processing。缺少唯一事件表、未锁订单行、网关交易 ID 未设唯一约束或异常后返回 500,都会触发重复。

修复时保留回滚路径

先验签,再以 provider+event_id 建唯一记录;事务内锁定订单并检查 payment transaction 与 canInvoice。已处理事件立即返回 2xx,永久错误进入人工队列,可重试错误保留明确状态。

上线后的验收

在测试环境并发发送同一回调五次,只能产生一张发票、一次邮件和一个 capture。乱序、延迟与退款回调也不能破坏最终订单状态。

生产环境操作前的检查

针对“Magento 2 支付回调重复导致订单重复开票”进行处理时,先记录 Magento 版本、部署模式、问题 Store View、复现时间和最近一次发布。所有 SQL 默认先执行 SELECT;需要 UPDATE、DELETE、补偿命令或目录删除时,必须先确认命中范围并保留可恢复备份。多 Web 节点环境还要比较代码版本、app/etc/env.php 摘要和当前流量节点,避免只修复其中一台。

php bin/magento --version
php bin/magento deploy:mode:show
php bin/magento maintenance:status
php bin/magento indexer:status
php bin/magento cache:status

命令应由 Magento 文件所有者在项目根目录执行。生产站不要同时运行多个全量索引或静态部署任务;若涉及数据库结构、库存、支付或订单,先在脱敏的生产数据副本验证,再安排维护窗口。

建立可比较的排查记录

每次实验只改变一个变量,并保存“操作前值、执行命令、开始时间、结束时间、结果、回滚方式”。至少准备一个正常对象和一个异常对象作对照,例如两个 SKU、两张订单或访客与登录客户。若修复后仅当前对象恢复,而新建对象仍会复现,说明根因尚未消除。

检查点需要记录通过标准
数据层 主键、Store/Website、更新时间、关联行数 关系完整且无孤儿记录
应用层 异常堆栈、模块、Cron/消费者状态 无新异常并可重复执行
缓存与索引 索引模式、版本、源站和公网响应 按预期周期自动更新
业务回归 正常路径、失败路径、重复请求 结果一致且没有重复副作用

修复后的观察窗口

上线后不要只刷新一次页面就结束。至少观察一轮 Cron、队列消费和索引周期,并在两台节点、无痕浏览器及目标 Store View 重复验证。对日志、数据库行数、错误率和响应时间设置临时观察项;确认它们稳定后再移除调试日志。调试日志可能包含客户、订单或令牌信息,采集时要脱敏,问题结束后及时关闭。