Invocie

Tech

设计跨区域电子发票 API:来自一线的经验教训

如果你正在搭建同时服务 clearance 与 post-audit 国家的基础设施,你只有一次机会把抽象做对。下面是我们以最艰难方式学到的东西。

Invocie Team · 2025年12月4日 · 6 分钟阅读


我们把 Invocie 的合规引擎做成一份代码同时处理 MENA 的 clearance、欧盟的 Peppol,以及全球的 post-audit。架构经过三轮迭代才稳下来。下面是最终留下来的设计选择。

1. 在国家层面做 Strategy Pattern,不是区域层面

区域太粗了。沙特(clearance、ZATCA、15%)和阿联酋(interoperable、FTA、5%)都是「MENA」,但架构上完全不一样。我们先按 ISO 国家代码分发,再按区域,最后是默认兜底。

function pickStrategy(t: { country_code: string; tax_region: string }) {
  if (MENA.includes(t.country_code)) return new MENAStrategy();
  if (EU.includes(t.country_code))   return new EUStrategy();
  if (t.tax_region === "MENA") return new MENAStrategy();
  if (t.tax_region === "EU")   return new EUStrategy();
  return new DefaultStrategy();
}

2. 永远 Decimal,永远不要 float

性能关键路径上我们用 BigInt 配合定点缩放(10000x,4 位小数精度),其他地方用 decimal.js,因为它的 SDK 更顺手。货币值唯一禁用的类型就是 JavaScript 的 number —— 上线前我们抓到三个舍入 bug,根因都是不小心冒出的 Number()。

3. 默认异步,例外时才同步

创建发票的 HTTP 路由从不阻塞在政府 API 上。它写库、入队、然后返回 201。worker 负责政府那边的提交,带重试(指数退避、最多 6 次、加抖动)。这把面向用户的 API 从政府侧延迟里隔离出来,也让重试在 ZATCA 状态不好的下午依然安全。

4. 把规范模型和线协议分开

我们内部的 Invoice 类型不论国家都长一个样。每种 strategy 的 buildArtifacts() 把它转换成各国的线格式(TLV QR、UBL 2.1 XML,或者只是默认的 JSON)。规范模型从不知道 XML 的存在,XML 也从不离开 strategy。这让以后加印度 IRP 变成单文件改动。

5. 日志是契约的一部分

每一次政府交互都向 ComplianceLog 写一行,带 prev_hash → hash 链式链接。审计师上门的时候(他们一定会来),你不会想从应用日志里重建发生了什么。数据库里的那一行就是收据。


相关阅读

在每一个市场发出合规发票

ZATCA、FTA、Peppol 与全球后审计 —— 一个 API。

联系我们的团队