框架深潜

Odoo Peppol 接入为什么最怕的不是注册,而是 token 失步、Webhook 保活与参与方状态机

Peppol 项目里最容易被低估的,不是把公司注册成发送方或接收方,而是数据库恢复后 token 为什么会失步、为什么 Odoo 要把 webhook 当成持续保活链路,以及 not_registered、sender、smp_registration、receiver 这些状态背后到底在保护什么。

框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

很多人把 Odoo Peppol 接入理解成“把公司注册上网络,然后就能发收电子发票”。

/home/ubuntu/odoo-temp/addons/account_peppol 里的源码告诉你,真正复杂的部分并不是第一次注册,而是连接生命周期管理

  1. 数据库一旦从备份恢复或被复制,Peppol token 可能失步;
  2. Odoo 不把 webhook 当成一次性回调,而是把它当成长期保活基础设施;
  3. 公司状态不是简单的已开通/未开通,而是一个业务含义很强的状态机;
  4. 注册向导不仅采集资料,还在实时判断公司是否已在外部接入点上注册。

所以 Peppol 在 Odoo 里不是“发票格式模块”,而是一套受外部网络状态驱动的连接控制平面

最危险的问题:数据库复制后 token 失步

account_edi_proxy_user.py 里有一段非常重要的错误提示:

This might happen if you restored a database from a backup or copied it without neutralization.

这句话其实已经把最大风险说穿了。

Peppol 连接不是一个只存在于本地数据库里的配置,它同时依赖:

  • 本地数据库记录;
  • 远端代理服务器上的连接状态;
  • 当前数据库身份与 token 同步版本。

如果你做了以下操作:

  • 用生产库克隆测试库;
  • 从旧备份恢复一份数据库;
  • 多个环境共享了同一套 Peppol 连接信息;

那么远端服务看到的“谁才是当前有效数据库”与本地数据库自以为的状态,可能已经不一致。

这就是源码里 is_token_out_of_synctoken_sync_version 存在的原因。

为什么 Odoo 对失步反应这么强硬?

因为电子发票网络不是普通 OAuth 登录。

一旦两份数据库都以为自己拥有发送/接收权:

  • 可能重复收消息;
  • 可能错误更新状态;
  • 可能把同一企业身份在多个环境中并发使用;
  • 审计边界会迅速崩塌。

所以 Odoo 的策略不是“尽量继续跑”,而是:

  • 检测到 invalid_signature 时,直接 _mark_connection_out_of_sync()
  • is_token_out_of_sync = True
  • 清空 refresh_token
  • 要求管理员去设置里点 Reconnect this database

这是非常正确的保守策略。

“Reconnect this database” 不是修复按钮,而是重新宣告所有权

源码里的 _peppol_out_of_sync_reconnect_this_database() 做了几件关键事:

  1. 断言当前确实处于失步状态;
  2. token_sync_version += 1
  3. 调用 /api/peppol/1/resync_connection
  4. 拿到新的 refresh_token
  5. 清除失步标记;
  6. 异步触发参与方状态更新 cron。

这里的重点是:它不是简单重试旧 token

它是在告诉远端代理:

现在这份数据库才是新一轮同步基线,请重新发放一致的连接状态。

这也解释了为什么如果远端返回 connection_superseded,Odoo 会直接走 _peppol_out_of_sync_disconnect_this_database(),把当前连接软重置掉。

换句话说,Peppol 连接所有权在设计上就是排他的。

为什么公司状态不是布尔值,而是状态机?

res.company 里的 account_peppol_proxy_state 定义了:

  • not_registered
  • sender
  • smp_registration
  • receiver
  • rejected

这不是为了 UI 好看,而是因为 Peppol 的接入过程天然不是一步到位。

这些状态分别在表达什么?

not_registered

还没有建立有效网络身份,既不能稳定发送,也不能接收。

sender

已经具备发送能力,但接收侧还没完全打通。

这通常意味着:

  • 可以发出文档;
  • 但还不能作为成熟接收端从网络收件。

smp_registration

这是最值得注意的中间态。

源码注释写得很清楚:

  • “Can send, pending registration to receive”

也就是说,发送能力先可用,接收能力仍在 SMP 注册流程中等待确认

这很现实,因为在 Peppol 世界里,“能发”和“能被别人发现并向你投递”不是完全同一件事。

receiver

发送与接收都完整开通,系统进入成熟运行态。

rejected

注册或审核流程被外部网络拒绝,不是临时同步问题,而是业务层面没通过。

这套状态机之所以重要,是因为它让 Odoo 能对外部网络的渐进式开通做出诚实表达,而不是把所有过程都伪装成“已启用”。

注册向导不是单纯收集表单,而是在实时判断网络位置

peppol_registration.py 很有意思。

它不是把用户填的:

  • EAS
  • Endpoint
  • 联系邮箱
  • 手机号

直接提交完事,而是在 _compute_smp_registration_external_provider() 里实时查:

  • 当前标识是否已经在 Peppol 网络上;
  • 如果已存在,是不是已经挂在别的 external provider 上。

然后得出:

  • smp_registration = not is_company_on_peppol
  • peppol_external_provider = external_provider

这意味着 Odoo 注册向导在做两层工作:

  1. 本地表单校验
  2. 外部网络占位检查

因此它不是“填资料向导”,更像“企业身份接入协调器”。

Odoo 为什么频繁触发 participant status cron?

_cron_peppol_get_participant_status() 有个细节非常关键:

  • 如果仍然存在 smp_registration 状态的公司,它会在一小时后再次触发自己。

这说明 Odoo 很清楚,Peppol 参与方状态不是一次请求就能稳定落地的。

尤其在注册中间态,系统需要持续追踪:

  • 外部注册是否完成;
  • 是否从 smp_registration 进入 receiver
  • 本地数据库状态是否仍与远端一致。

这也再次说明:Peppol 集成不是同步按钮,而是持续对账型连接

为什么 webhook 不是锦上添花,而是核心链路?

controllers/webhooks.py 定义了三个公开 POST 入口:

  • /peppol/webhook/new-message
  • /peppol/webhook/message-state-update
  • /peppol/webhook/user-state-update

它们本身不直接做大量业务处理,而是去触发对应 cron:

  • 拉新文档
  • 拉消息状态
  • 拉参与方状态

这套设计非常稳

因为 webhook 本来就不该承载复杂事务:

  • 外部服务调用时间不可控;
  • 重试可能发生;
  • 入口要尽量短平快;
  • 真实业务处理放到内部任务队列/cron 更安全。

Odoo 在这里做的是一个典型的“回调触发拉取”模型:

  • webhook 负责通知;
  • Odoo 自己决定何时、如何批量同步;
  • 内部处理失败,也不会把外部回调链条拖死。

为什么还要专门做 webhook keepalive?

_cron_peppol_webhook_keepalive() 会定期调用 _peppol_reset_webhook(),重新把:

  • webhook_url
  • token

推到代理服务上。

这说明 webhook 并不是“一次设置永久有效”的配置,而是带过期与续租语义的连接部件。

源码里甚至提到 webhook token 的 TTL 是 30 天。

这非常合理:

  • 降低长期暴露风险;
  • 给 URL / token 轮换留空间;
  • 减少数据库搬迁后旧回调继续打进来的风险。

很多系统做 webhook,只做“注册成功”那一步;Odoo 这里更成熟,它把 webhook 当作需要长期维护的租约。

分支公司与父公司连接复用:不是共享按钮,而是治理选择

注册向导里还有一条经常被忽略的逻辑:

  • 分公司有机会复用父公司的连接;
  • 也可以选择自己注册;
  • 但如果分支和主公司使用同一个 Peppol ID,会被阻止。

这意味着 Odoo 不是简单假设“一个公司树只有一个电子发票身份”,而是承认企业集团内部可能存在两种治理方式:

  1. 集中式接入:从母公司统一发送;
  2. 分布式接入:分支独立注册自己的网络身份。

这是非常接近真实组织结构的设计。

实战里最该盯的 5 个信号

1. 数据库是否被复制过

只要 Peppol 连着远端代理,复制数据库就不是小事。测试环境克隆生产库时尤其要谨慎。

2. 当前状态是不是卡在 smp_registration

如果长期卡住,问题不一定在 Odoo 本地,也可能在外部网络登记流程尚未完成。

3. webhook 有没有持续刷新

能注册成功不代表长期可用。Webhook 续租失败,会导致后续状态变更通知逐渐失效。

4. 是否出现 invalid_signatureconnection_superseded

这两类错误别当偶发网络抖动看,它们通常在提示连接身份边界出了问题。

5. 采购日记账是否已设置

接收文档时,如果公司没配置 Peppol purchase journal,本地导入链路也会卡住。网络打通不等于会计落账链路完整。

最后一句

Peppol 模块最值得学习的,不是它会发电子发票,而是它对“连接所有权”这件事足够严肃:

  • token 会失步;
  • 状态要轮询;
  • webhook 要续租;
  • 数据库复制不能装作没发生;
  • 中间态必须被诚实表达。

这才是企业级外部网络集成该有的样子:不是一次配置成功,而是长期保持一致。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。