一次批量开票后,财务发现中国站的发票号里混进了欧洲站前缀。订单归属没有错,税率也没有错,只有发票 increment ID 选错了序列。这个现象最危险的地方在于:单独重试一张订单往往正常,只有管理员连续处理不同网站订单时才出现。

发票号不是从订单号截出来的

Magento 的订单、发票、发货和退款单各有自己的 sales sequence。创建发票时,系统会根据实体类型和 Store 选择对应 sequence profile,然后生成下一个编号。先查映射,不要先改已经生成的号码:

SELECT meta_id, entity_type, store_id, sequence_table
FROM sales_sequence_meta
WHERE entity_type = 'invoice'
ORDER BY store_id;

SELECT profile_id, meta_id, prefix, suffix, start_value, step
FROM sales_sequence_profile
WHERE is_active = 1
ORDER BY meta_id;

这里要回答两个问题:每个 store_id 是否指向预期的 sequence table;活动 profile 的前缀是否真的属于该店铺。配置本身若已经串了,任何代码都会稳定地产生错误号码;配置正确而问题偶发,才应该继续追运行时上下文。

问题通常发生在“订单店铺”和“当前店铺”分开的时候

前台下单时上下文比较单纯,订单属于哪个 Store,创建后续单据时通常仍沿用它。后台批量操作、Cron 或第三方 ERP 回调就不同:代码可能拿到管理员当前 Store、默认 Store,甚至上一个循环遗留的模拟环境。

重点审查自定义开票代码里有没有这些味道:

  • 用 StoreManagerInterface::getStore() 代替 $order->getStoreId();
  • 循环中调用 store emulation,却没有在 finally 中结束;
  • 把 Invoice Factory 或 Sequence Manager 做成带状态的长生命周期对象;
  • ERP 只传订单号,代码随后按当前网站重新加载订单相关对象。

用一条审计日志抓住偶发串号

不要把整个订单对象写进日志。创建发票之前记录订单实体 ID、订单 increment ID、订单 store_id、当前 Store ID、将要使用的 sequence meta/profile,以及本次任务的批次 ID。出现下一张错号时,就能判断上下文是在什么时候改变的。

$orderStoreId = (int) $order->getStoreId();
$currentStoreId = (int) $storeManager->getStore()->getId();

$logger->info('invoice_sequence_context', [
    'order_id' => (int) $order->getEntityId(),
    'order_increment_id' => $order->getIncrementId(),
    'order_store_id' => $orderStoreId,
    'current_store_id' => $currentStoreId,
    'batch_id' => $batchId,
]);

如果两个 Store ID 不同,并不自动代表有错;后台本来就可能处于 Admin Store。关键是后续 sequence 选择是否明确使用订单所属 Store,而不是依赖隐式的全局当前值。

已经生成的错号不能直接 UPDATE

发票号可能已经进入邮件、支付平台、ERP、会计凭证和客户下载文件。直接改 sales_invoice.increment_id 只会让各系统记录互相矛盾。先确认当地财税规则和外部系统是否允许作废重开;如果必须修复,要把 Magento、ERP、邮件/文档和审计记录作为一次受控变更处理。

同时检查错误序列的当前值,避免“修好了选择逻辑,却在下次生成时撞号”。不要手动把计数器调回一个看似顺眼的数字,除非已经查清所有已占用编号并准备好唯一性校验。

我会怎样复测这个问题

准备两个前缀不同的 Store,各下两张订单。先交替手工开票,再用批量任务按 A1、B1、A2、B2 的顺序处理,最后让消费者或 ERP 回调再跑一轮。每张发票都核对订单 store_id、序列表、前缀和连续性。只有跨 Store 交替执行仍稳定,才说明那个“偶尔串号”的上下文泄漏真正消失了。