企业 移动推送

Odoo 企业版 Mobile Push 为什么不是“开了推送就行”:@mention 分流、4KB payload 与深链回退链路讲透

基于 mail_mobile 源码,讲清 Odoo 企业版移动推送如何先做设备注册,再按 @mention 与普通消息拆分投递,并在 4KB payload、Discuss 会话 subject 重写与 Firebase dynamic link 回退之间维持可点开的移动通知体验。

企业 协同办公
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多团队以为企业版移动推送是一个后台开关:启用以后,消息来了,手机自然会响。

enterprise/mail_mobile 的实现说明,事情远没这么简单。Odoo 真正在处理的是一整条链:

  1. 这台设备有没有被登记进 OCN;
  2. 当前消息是不是应该进入移动额外通知;
  3. 这条通知属于普通消息、私聊,还是 @mention
  4. payload 能不能在 FCM 4KB 限制里保留足够上下文;
  5. 用户点开以后,是回到 App,还是退回浏览器。

所以这篇文章的重点不是“怎么开推送”,而是:

Odoo 企业版 Mobile Push 本质上是在统一消息通知流水线里,额外挂了一条面向移动端的路由层。

一、第一步根本不是发消息,而是把设备登记成“可投递目标”

入口在 enterprise/mail_mobile/models/res_config_settings.py

这里有两个特别关键的方法:

  • get_fcm_project_id()
  • register_device(fcm_token, device_name, fcm_token_old=None)

前者不是单纯读配置,而是在启用 mail_mobile.enable_ocn 后,必要时主动去 OCN 服务注册当前数据库实例,拿回 odoo_ocn.project_id。换句话说,系统要先把“这套 Odoo 实例是谁”说清楚,后面才有资格发推送。

后者则更直接:移动端提交 FCM token 后,Odoo 会把设备信息送去 OCN 服务,并把返回值写到 partner.ocn_token

这就决定了一个常被忽略的事实:

  • 移动推送面向的并不是 user,而是 partner 维度的可投递身份;
  • 没有 ocn_token,后面的推送逻辑根本不会把你当成移动端接收者。

所以排查移动消息问题时,第一步通常不是去看消息有没有创建,而是看目标 partner 有没有完成设备注册。

二、移动推送不是独立消息中心,而是 mail.thread 上的额外通知分支

enterprise/mail_mobile/models/mail_thread.py 覆盖了 _notify_thread()

它的做法很克制:

  • 先调用 super()._notify_thread(...) 完成原本的 mail 通知编排;
  • 再在“不是 scheduled_date 延后通知”的情况下,调用 _notify_thread_by_ocn(...)

这说明移动推送不是旁路系统,而是建立在原始 recipients 编排结果之上的额外投递

这个设计特别重要,因为它天然继承了 Odoo 原有的消息判断能力:

  • 谁本来就该收到通知;
  • 谁只是 follower;
  • 哪些消息是延迟发送,不能立刻打到手机上。

也正因为它挂在统一通知流水线上,移动推送才能和桌面 bus、Discuss 线程、邮件通知并存,而不是互相打架。

三、真正的“路由”发生在 _notify_thread_by_ocn()_notify_by_ocn_send()

_notify_thread_by_ocn() 先检查两个硬条件:

  • odoo_ocn.project_id 已存在;
  • mail_mobile.enable_ocn 已启用。

两者缺一,直接退出。这是企业实现里很典型的“宁可不推,也不做半配置推送”。

真正把消息发出去的是 _notify_by_ocn_send()。这里有三个关键动作。

1. 先筛出真正有移动设备的接收者

方法会对 partner 再过滤一遍,只保留 ocn_token != False 的接收者。

所以某个用户收不到推送,不一定是通知流程没走到,也可能只是这个 partner 当前没有任何已注册设备。

2. 再把同一批接收者拆成两类 token

源码会调用 _at_mention_analyser() 扫描 HTML body,把 <a ... data-oe-model="res.partner" data-oe-id="...">@名字</a> 这样的 mention 链接解析出来。

随后接收者被拆成两组:

  • 普通接收者:走默认 payload;
  • @mention 的接收者:单独走一个 android_channel_id = 'AtMention' 的 payload。

这不是 UI 小细节,而是消息优先级设计。

对协同办公来说,@mention 的语义和“你关注的线程里有人说话了”完全不同。Odoo 不只是多发一条通知,而是显式把两类消息放进不同 Android channel,方便移动端做不同提醒策略。

3. 最后才调用 OCN 服务批量发送

每个 chunk 都通过 /iap/ocn/send 发出去。也就是说,Odoo 服务器本身并不直接跟 FCM 打交道,而是先把路由结果和 payload 交给 OCN。

这层抽象让数据库实例不用自己管理厂商推送细节,但代价是:如果 OCN 配置、实例注册或 partner token 任一环节出问题,你前面的消息逻辑都可能看起来“正常”,最后却没有推送落地。

四、Discuss 会话为什么要重写 subject、type 和 model

如果消息来自 discuss.channelenterprise/mail_mobile/models/discuss_channel.py 会继续覆写 _notify_by_ocn_prepare_payload()

它没有推翻父类,而是在父类生成基础 payload 后,按频道类型做细分:

  • chat
  • subject = author_name
  • type = 'chat'
  • android_channel_id = 'DirectMessage'
  • channel
  • subject = #频道名 - 作者名
  • android_channel_id = 'ChannelMessage'
  • 其他:保留 #record_name 风格

更关键的一点是,源码最后把 payload['model'] 强制写成了 mail.channel,并注明:

移动 App 仍使用旧的 mail.channel 模型名,iOS 侧暂时不能改。

这句话透露了一个非常实际的边界:

  • 服务端模型可能已经演进;
  • 但为了移动端兼容,payload 仍要维持旧契约。

所以你看到的 subjecttypemodel 并不只是展示字段,而是客户端能否正确打开会话的协议字段

五、为什么 _notify_by_ocn_prepare_payload() 要死盯 4KB

mail_thread.py 里,payload 默认至少会带:

  • android_channel_id
  • author_name
  • model
  • res_id
  • db_id
  • subject

然后才尝试把消息正文塞进 body。塞之前源码先计算字节长度,并明确写了注释:FCM data payload 上限是 4000 bytes。

于是 Odoo 做了几件很务实的事:

  1. 把 HTML 正文转成纯文本;
  2. 再拼上 tracking message;
  3. 最后按剩余空间裁掉 body

这意味着移动推送的目标从来不是“把整条消息原封不动搬到通知里”,而是:

在严格的体积限制下,优先保留能让用户识别上下文并点进去的最小信息集。

这也是为什么很多团队会误解“通知怎么被截断了”。它不是 bug,而是 FCM 载荷预算下的主动取舍。

另一个容易被忽略的点,是 _notify_get_action_link()

它先调用父类拿到原始链接,然后在满足条件时把链接包装成:

  • https://redirect-url.email/?link=...&apn=com.odoo.mobile&afl=...&ibi=com.odoo.mobile&ifl=...

也就是 Firebase Dynamic Links 风格的跳转链接。

这么做的目的很明确:

  • 手机上装了 Odoo App,尽量直接回到 App;
  • 没装 App,至少还能回到普通浏览器页面。

但这里有两个安全/兼容边界同样值得注意:

1. 带敏感 token 的链接不会被包出去

源码里有一个 BLACK_LIST_PARAM,包括:

  • access_token
  • auth_signup_token

如果 action link 里带这些参数,就直接返回原始链接,不再交给第三方跳转服务,以免敏感参数泄露到 Firebase 这类外部服务。

mail_mobile.disable_redirect_firebase_dynamic_link 为真时,也直接返回原始链接。

这很适合某些企业环境:

  • 内网跳转要求严格;
  • Firebase 跳转在当地网络环境不稳定;
  • 安全团队希望所有链接都保持站点直链。

所以 deep link 在 Odoo 里不是“必须用 Firebase”,而是一个可关闭、带黑名单过滤的增强层

七、最容易踩坑的三个误解

1. “只要启用了 Push,谁都能收到”

不对。至少还要同时满足:

  • 实例已向 OCN 注册;
  • partner 有 ocn_token
  • 消息进入了额外通知接收者集合;
  • 当前消息不是只做 scheduled queue。

2. “@mention 只是正文里多了个名字”

不对。源码把它当成一类单独路由的移动通知,甚至给了不同 Android channel。

3. “通知正文被截断说明推送坏了”

也不对。更常见的原因是 payload 故意在 4KB 预算里裁剪,只保留最必要的上下文。

八、实战排查顺序

如果企业客户反馈“Discuss 手机通知不稳定”,我会按这个顺序查:

  1. mail_mobile.enable_ocnodoo_ocn.project_id 是否已生效;
  2. 看目标 partner 上是否有 ocn_token
  3. 看消息是否真的进入 _notify_thread_by_ocn()
  4. 看正文里是否包含 @mention,从而触发不同 channel;
  5. 看 payload 是否因为内容过长被裁掉了关键上下文;
  6. 看 deep link 是否因为黑名单参数或配置被退回原始浏览器链接。

这个顺序比“先问手机有没有开通知权限”更接近 Odoo 源码真实链路。

九、结论

Odoo 企业版 Mobile Push 的价值,不在于“帮你把消息弹到手机上”,而在于它把移动端通知做成了一条可控路由:

  • 前面用设备注册与 ocn_token 确认谁可达;
  • 中间用 mail.threaddiscuss.channel 细分消息语义;
  • 后面在 4KB payload 和 dynamic link 回退之间,尽量保证用户还能点回正确线程。

所以它从来不是一个简单开关,而是一套围绕投递目标、通知优先级、客户端兼容与安全回退组织起来的移动协同机制。

DISCUSSION

评论区

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