先说结论
很多人理解 Odoo follower 时,只停在“这个人是不是关注者”。
但源码真正精细的地方不在“有没有订阅”,而在:
- 订阅了哪几类消息
- 某个业务动作会触发哪个 subtype
- 子记录的通知怎样映射到父记录
也就是说,follower 不是一个单纯的 yes / no 开关。
在 Odoo 里,follower 决定你是否进入协同回路,subtype 决定你在这个回路里究竟听见什么。
这才是很多“为什么有人收到、有人没收到、同样关注却反应不同”的根因。
一、为什么只看 follower 往往会误判
很多排查现场都会这样开始:
- A 和 B 都在 followers 里
- 为什么 A 收到了阶段变化通知,B 没收到?
- 为什么项目上看得到任务动态,有些人却只看到一部分?
如果只看 mail.followers 这张订阅关系,就会觉得系统很随机。
但 Odoo 的设计其实更像:
- follower 决定“是否进入订阅集合”
- subtype 决定“订阅集合里具体关心哪些事件”
所以 followers 相同,不代表消息范围相同。
二、mail.message.subtype 到底解决什么问题
addons/mail/models/mail_message_subtype.py 里把 subtype 定义得很清楚。
它不是消息正文模板,而是:
- 一类更精确的消息类型
- 一套给 follower 调整通知范围的开关
- 一种把业务动作映射成通知语义的中间层
比如:
- 新建
- 阶段变化
- 完成
- 变更请求
- 审批通过
这些都不是“消息内容”,而是“消息在协作上的意义”。
这一步很关键。
因为协同系统真正难的,不是“能不能发消息”,而是:
怎样让不同角色只收到自己需要的那类变化,而不是被所有变化刷屏。
subtype 就是 Odoo 为这个问题准备的粒度控制层。
三、默认订阅并不等于订阅全部
在 mail.message.subtype 里,default 字段决定某类 subtype 是否在订阅时默认激活。
这说明 Odoo 的默认思路不是:
- 只要关注,就接收所有事
而是:
- 先给一组默认合理的通知范围
- 再允许用户或业务模型继续细分
因此“默认关注”其实已经隐含了一套产品判断:
- 哪些事件大多数参与者应该知道
- 哪些事件只适合更窄范围的人知道
这也是为什么 subtype 设计得好时,Chatter 会显得很顺;设计得粗时,团队就会觉得通知噪音特别大。
四、父子 subtype 映射,才是跨对象协作的关键
很多人第一次读 mail_message_subtype.py,会被 parent_id 和 relation_field 绕住。
但其实它表达的是一个非常实用的协同能力:
子对象上的事件,如何映射成父对象上的通知语义。
源码里的典型例子就是项目与任务。
在 addons/project/data/mail_message_subtype_data.xml 里你会看到:
project.task上有mt_task_done、mt_task_approved、mt_task_stage等 subtypeproject.project上又有对应的mt_project_task_done、mt_project_task_approved、mt_project_task_stage- 它们通过
parent_id和relation_field=project_id关联起来
这套设计的价值非常大。
因为项目参与者未必直接跟每一条任务,但他们可能仍然需要知道:
- 某任务已完成
- 某任务已批准
- 某任务请求修改
Odoo 不是靠“把所有人都加到所有任务 followers”来解决这个问题, 而是靠 subtype 语义映射。
这比简单粗暴地扩散关注者更优雅,也更省噪音。
五、_get_auto_subscription_subtypes() 真正干了什么
mail.message.subtype 里的 _get_auto_subscription_subtypes(model_name) 很值得读。
它会把当前模型相关的几类 subtype 一次整理出来:
- 当前模型自身的 subtype
- 通用 subtype
- 默认 subtype
- internal-only subtype
- 父子 subtype 的映射关系
- 以及父对象需要通过哪个 relation field 取到
换句话说,这个方法并不是简单“列出有哪些 subtype”。
它是在预计算一张通知语义地图:
- 这个模型有哪些事件类别
- 哪些默认打开
- 哪些属于内部消息
- 如果这个模型是子对象,哪些消息还能往父对象投影
所以 Odoo 的通知体系并不是临时拼出来的,而是有一层比较完整的分类索引。
六、业务动作为什么会打到不同 subtype
通知粒度要成立,还得有另一半:
- 谁来决定这次变化属于哪个 subtype
这个职责主要落在各业务模型的 _track_subtype() 上。
mail.thread 默认实现直接返回 False,意思很明确:
- 框架只提供机制
- 具体业务语义由业务模型自己决定
比如项目任务里:
- 阶段变化走
project.mt_task_stage state=03_approved走project.mt_task_approvedstate=02_changes_requested走project.mt_task_changes_requestedstate=04_waiting_normal走project.mt_task_waiting
所以同样是字段变了,系统不是一律发“有更新”,而是尽量翻译成更像业务语言的通知事件。
这一步对协同特别重要。
因为人不关心“数据库字段改了”,人关心的是:
- 任务通过了
- 任务被打回了
- 任务卡住在等待别人
subtype 正是在把字段变化翻译成协作语义。
七、为什么这比“多建几个消息模板”更高级
很多定制会误把通知设计成:
- 某状态发一封邮件模板
- 某动作再发一条系统消息
这样短期能跑,但长期很容易失控。
因为它缺少一层统一语义。
subtype 的价值正好在这里:
- 一次变化先被归类
- 订阅关系再决定谁该看到
- 最后才落到 inbox、email、web push 等具体通知渠道
所以 subtype 是“协同语义层”, 而邮件模板更多是“通知内容层”。
先有语义层,内容层才不会乱。
八、实战里最容易踩的坑
1. 只会加 follower,不会设计 subtype
结果所有人都被一锅端通知。
2. 看到父子 subtype 就以为是重复定义
其实它是在解决子对象事件如何传到父对象的问题。
3. 把字段追踪和 subtype 混为一谈
追踪是“记录改了什么”,subtype 是“这次变化属于什么协作事件”。
4. 把所有 subtype 都设成默认
这样等于放弃通知粒度控制。
5. 想让项目知道任务变化,就把项目成员全订阅到每条任务
这通常会制造更大的噪音,远不如用 parent subtype 映射。
九、做协同类定制时的实用判断
如果你在设计 Odoo 协作通知,我建议先问三个问题:
1. 这个人需要“长期跟进对象”,还是“只关心某类变化”?
前者偏 follower,后者一定要想 subtype。
2. 这个变化是对象内事件,还是要向上汇报给父对象?
如果要向上汇报,就该考虑 parent subtype + relation field。
3. 这个通知是在表达字段变化,还是在表达业务语义?
如果是后者,就别只停留在 tracking,要把 subtype 设计清楚。
一句话记忆法
在 Odoo 里,follower 决定你是否进频道,subtype 决定你听哪一路;真正把通知做细的,不是“有没有关注”,而是“关注后对哪些业务语义敏感”。
DISCUSSION
评论区