先说结论
在 Odoo 里,message_post() 和 message_notify() 看起来都像“发消息”,但它们面向的对象根本不同。
一句话概括:
message_post()是把消息挂到某个业务记录的协作线程里;message_notify()是直接向用户发一条通知型消息。
如果把这两个 API 混着用,最常见的后果就是:
- 本来只是想提醒某个人,结果把消息写进了业务记录 Chatter;
- 本来应该形成业务上下文,结果只发出一条孤立通知;
- 自动订阅作者、followers、reply-to、附件归档等行为和预期不一致;
- 后续邮件回复、线程追踪、权限判断都开始跑偏。
这篇主要看哪里
核心源码在:
addons/mail/models/mail_thread.py
最关键的几段:
message_post()message_notify()_notify_thread()_notify_thread_by_inbox()_notify_thread_by_email()
其中 message_post() 的注释直接写得很明确:
- 应该发在业务文档上;
user_notification这种类型不要从这里发;- 如果只是给用户发通知,应使用
message_notify()。
这已经不是“最佳实践建议”,而是源码层面的接口边界。
第一层差异:消息有没有业务归属
message_post() 一进来就 ensure_one(),并且明确要求:
- 不能在空记录上发;
- 不能在纯
mail.thread根对象上发; - 必须挂在一个真实业务对象上。
源码里甚至直接抛错:
Posting a message should be done on a business document. Use message_notify to send a notification to an user.
这说明 Odoo 的设计不是“所有消息都一样,最后再决定展示到哪”。
而是从 API 层就把两条路分开:
message_post()
适合这些场景:
- 给任务、商机、订单、项目等业务对象留下一条 Chatter 记录;
- 让消息成为该业务对象历史的一部分;
- 让后续回复、邮件线程、followers 通知都围绕该记录展开。
message_notify()
适合这些场景:
- 某条失败告警要直接提醒某个用户;
- 某个后台动作要发一个“你需要知道”的通知,但不想污染业务线程;
- 系统内部兜底提醒,比如定时消息发送失败后告诉原作者。
第二层差异:作者、线程和 reply-to 的语义不同
message_post() 会把消息值补齐为一条真正的业务线程消息:
modelres_idparent_idreply_torecord_alias_domain_idrecord_company_id
这些字段不是装饰品。
它们决定了:
- 这条消息属于哪个记录;
- 邮件回复以后能不能回到原线程;
- 多公司下 reply-to 和 alias 域从哪里取;
- 后续消息是继续接楼,还是另起一条孤立通知。
这也是为什么 message_post() 特别像“往业务协作历史里写一条正式消息”。
而 message_notify() 的重点不是业务线程历史,而是“把通知送出去”。
它当然也会创建 mail.message,但它的语义更像一条通知载体,而不是一段业务协作上下文。
第三层差异:自动订阅不是所有消息都会发生
message_post() 里有两个很容易被忽略的动作。
1. 可能自动订阅显式收件人
如果上下文里有 mail_post_autofollow,而且传了 partner_ids,源码会调用:
message_subscribe()
也就是说:
你以为自己只是“这次顺手提醒一下某几个人”,Odoo 可能把他们变成这个记录的长期关注者。
2. 手工评论的作者也可能被自动订阅
message_post() 在符合条件时会把真实作者 _message_subscribe() 进去。
但它只在特定条件下才做:
- 不是
notification/user_notification/auto_comment/out_of_office; - subtype 是
mail.mt_comment; - 作者是内部用户。
这说明 Odoo 的思路很清楚:
- 正常人工协作评论,作者应当被纳入线程,方便后续接收回复;
- 纯系统通知、自动回复、离岗回复,不应该顺手把作者订阅进来。
这也是 message_post() 和 message_notify() 最大的协作差异之一。
第四层差异:通知分发是共用后端,但入口语义不同
很多人会说:
“反正最后都会进 _notify_thread(),那不就是一回事吗?”
不对。
确实,两者最终都会走通知后端,例如:
_notify_thread_by_inbox()_notify_thread_by_email()_notify_thread_by_web_push()
但共用分发引擎,不代表入口语义相同。
你可以把它理解成:
message_post():先把消息定义成“业务线程消息”,再决定怎么通知;message_notify():先把消息定义成“通知型消息”,再决定通过哪些通道送达。
所以决定行为差异的,不只是最后发了 inbox 还是 email,而是前面那层:
- 是否挂业务记录;
- 是否形成线程上下文;
- 是否会引入 followers 语义;
- 是否会影响后续协作历史。
第五层差异:源码里已经给了最典型的使用案例
mail.scheduled.message 的失败兜底逻辑非常有代表性。
在 _post_message() 里,如果定时消息发送失败,源码不是去某个业务记录上 message_post() 一条“失败了”的评论,而是调用:
self.env['mail.thread'].message_notify(...)
给原作者直接发通知。
这恰好说明 Odoo 官方自己的设计取向:
- 原定时消息本身,属于业务线程,应该
message_post()到目标记录; - 发送失败提醒,只是一个系统通知,不应该污染目标业务对象的 Chatter。
这个例子几乎是教科书式边界。
为什么很多二开会把这件事搞反
因为从开发者视角看,这两个 API 都很方便,参数也有重合:
bodysubjectpartner_ids
于是很容易产生一个错觉:
“只要把参数填对,哪个都行。”
但源码设计恰恰在反对这种想法。
典型误用 1:提醒负责人时直接 message_post()
如果你只是想提醒负责人处理一件事,却把消息直接贴到记录 Chatter:
- 会留下业务协作痕迹;
- 可能把负责人顺手变成 follower;
- 后续回复可能继续绑定到该记录线程;
- 消息历史开始和真实业务协作混在一起。
典型误用 2:本来需要留痕,却用了 message_notify()
这样做的结果是:
- 用户可能收到了提醒;
- 但记录本身没有形成清晰的上下文历史;
- 其他协作者后来回看 Chatter 时,不知道当时到底发生过什么。
实战判断标准:到底该用哪个
可以直接用下面这套判断。
用 message_post(),如果你想要的是:
- 让消息成为业务记录历史的一部分;
- 让回复围绕记录线程继续发生;
- 让 followers / subtype / reply-to / alias 机制参与;
- 把这次沟通视为“业务协作事件”。
用 message_notify(),如果你想要的是:
- 给某个用户发一条提醒;
- 提醒本身不应污染业务记录 Chatter;
- 它是失败告警、系统提示、兜底通知,而不是业务协作正文;
- 你不希望无意触发自动订阅和线程副作用。
一句话记住
很多协同系统的问题,不是“消息没发出去”,而是“消息发到了错误的语义层”。
在 Odoo 里:
message_post()解决的是业务线程协作,message_notify()解决的是用户级提醒。
把这两个入口分清,你才不会在 Chatter、followers、邮件回复链和用户收件体验之间制造一堆隐性副作用。
如果你在做项目、审批、销售、客服类二开,这个边界越早搞清楚,后面返工越少。
DISCUSSION
评论区