先说结论
IAP 在 Odoo 里看起来像“在线服务余额”或“买积分”,但从 /home/ubuntu/odoo-temp/addons/iap/models/iap_account.py 看,官方真正维护的是一条服务账号关系链,而不是一个简单余额字段。
这条链至少包括:
- 每个 service 对应的
iap.account如何找到或创建。 - token 如何生成、何时不能明文外传。
- 如果调用 IAP 服务会触发回滚,账号创建怎样不被一起回滚掉。
- 中和数据库为什么要自动把 token 变成
+disabled。 - 多公司环境里,默认该拿哪一个 account。
所以最短结论是:
Odoo IAP 管的不是“余额”,而是“本库如何以受控身份去消费 Odoo 在线服务”。
iap.account 不是钱包,而是“服务身份”
iap.account 上最关键的字段不是 balance,而是:
service_idaccount_tokencompany_idsstate
这说明一条 IAP account 记录的本质不是资金账户,而是:
- 这个数据库 / 公司,
- 对某个 IAP service,
- 拿什么身份去调用。
所以 balance 只是外部服务回传的动态信息,真正稳定存在的是服务身份映射。
这和传统“充值中心”思路不同。IAP 在 Odoo 里更像:
一个受服务名驱动的远程能力凭证容器。
为什么 token 默认生成,但又被限制在系统组可见
account_token 默认来自 uuid.uuid4().hex,而且字段权限是:
groups="base.group_system"
这两个细节放在一起,很能说明官方的安全思路:
- token 需要一开始就有,因为很多服务调用都依赖它;
- 但 token 绝不能被普通用户当作一般字段看到。
因为一旦 token 泄露,本质上就等于:
- 别人可以冒这套库 / 这家公司去消费对应在线服务;
- 甚至可能查看、购买、消耗与你业务相关的服务能力。
所以把 token 视作“认证密钥”而不是“配置字段”,这个理解非常重要。
为什么 get() 要用额外 cursor 创建账号
get(service_name) 是整篇源码里最值得看的方法之一。
如果当前数据库里还没有该 service 对应的 account,Odoo 不会简单在当前事务里 create() 完事,而是用:
- 新 cursor;
- 先
flush_all(); - 再在独立上下文中创建 account;
- 把
account_token手动塞回当前 env cache。
注释写得很直白:
- 因为后续 IAP 调用可能抛
NoCreditError; - 这类错误会回滚当前事务;
- 如果 account 也是在同一事务里创建的,就会被一起抹掉。
这非常有代表性。
Odoo 在这里防的不是“创建失败”,而是:
第一次接触某个 IAP service 时,不能因为后续业务异常,导致身份记录永远建不起来。
这是一种典型的“把基础设施对象从业务事务里解耦”的写法。
为什么还要清理没有 token 的 account
get() 里还有一段很务实:
- 先找到所有符合 domain 的 accounts;
- 把
account_token为空的记录筛出来; - 用 sudo 和独立 cursor 删除它们。
这说明官方承认现实里可能出现“半拉子 IAP account”:
- 记录建了;
- token 没建成;
- 或某次异常留下了残缺状态。
如果不清,后面 get() 很容易拿到一个名义上存在、实际上没法认证的脏 account。
所以这里不是在做“洁癖清理”,而是在保证:
- 服务账号要么可用,要么不存在;
- 不要让半残记录卡住后续自动修复。
为什么中和数据库会把 token 改成 +disabled
create() 里有个非常关键的分支:
- 如果
database.is_neutralized为真; - 新建 account 后就把 token 改成
原 token + "+disabled"。
这一步特别有现实价值。
很多团队会把生产库复制到:
- 测试;
- 培训;
- 演示;
- UAT。
如果复制后的数据库还能沿用生产 IAP token,风险会非常大:
- 测试环境可能继续消耗真实积分;
- 演示数据可能误打远程服务;
- 非生产库行为会污染真实计费和调用轨迹。
Odoo 的做法非常克制:
- 不粗暴删除 token;
- 而是用
+disabled标记让它在逻辑上失效。
而 _hash_iap_token() 又会先把 + 后缀剥掉再哈希,这说明官方有意识地区分:
- 底层 token 主体;
- 本地环境附加的状态后缀。
这是很成熟的环境隔离设计。
为什么买积分 URL 只带哈希 token
get_credits_url() 不会把明文 account_token 拼进购买链接,而是:
- 先
_hash_iap_token(account_token); - 然后参数里带
hashed=1。
这一步很重要。
如果购买入口带的是明文 token,那么:
- 浏览器历史;
- 代理日志;
- 引荐头;
- 截图;
- 外部排障记录;
都有机会把它泄露出去。
而哈希后的 token 至少把风险降低到了:
- URL 可识别 account;
- 但不直接暴露原始认证秘密。
这说明 Odoo 即使在“把用户送去充值页面”这种看上去很普通的动作里,也没有放弃对 credential 暴露面的控制。
多公司环境为什么优先选带 company_ids 的 account
get() 末尾有个不显眼但很重要的选择顺序:
- 先找符合当前 companies 的记录;
- 如果有带
company_ids的 account,优先返回它; - 否则才退到 company 为空的全局 account。
这意味着 Odoo 认为 IAP account 既可以:
- 是全局共享;
- 也可以公司专属。
而默认策略是:
有更具体的公司级身份,就不要回退到全局身份。
这和权限系统、财务归属、服务配额都密切相关。尤其当不同公司:
- 消耗不同服务;
- 单独付费;
- 需要独立控制 alert recipient;
公司级 account 就比全局 account 更稳。
warning alert 为什么要同步到 IAP 远端
write() 里只要改了:
warning_thresholdwarning_user_ids
Odoo 就会去调用 /iap/1/update-warning-email-alerts。
也就是说,这些并不是纯本地 UI 配置,而是远程服务也需要知道的账号告警规则。
这说明 IAP account 并不是“本地缓存一个余额后就算完”,而是:
- 本地和远端共同维护一套账号元数据。
它让“积分不足提醒发给谁、低于多少提醒”这种设置,真正成为服务账户的一部分,而不是某台数据库里的一段孤立设置。
一句话理解 Odoo IAP 的真正边界
如果要用一句话概括这套设计,我会说:
IAP 不是支付中心,而是 Odoo 为各种在线能力维护的一层服务身份与额度边界。
所以理解 IAP 最该抓住的不是“怎么买积分”,而是下面这些边界:
- token 是认证秘密,不是普通字段;
- 新 account 要能在回滚环境里存活;
- 中和库必须自动失效;
- 外跳链接尽量不泄露明文 token;
- 多公司优先具体身份而不是全局身份。
这也是为什么 IAP 虽然表面很轻,但平台味道非常重。
DISCUSSION
评论区