协同办公

Odoo 发消息为什么分 `message_post()` 和 `message_notify()`:业务线程、用户通知与自动订阅边界讲透

很多人把 Odoo 里“发一条消息”理解成同一件事,但源码里 `message_post()` 和 `message_notify()` 从入口约束到通知语义都不是一回事。看清它们的边界,才能避免 Chatter 污染、通知错发和关注者异常增长。

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

先说结论

在 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() 会把消息值补齐为一条真正的业务线程消息:

  • model
  • res_id
  • parent_id
  • reply_to
  • record_alias_domain_id
  • record_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 都很方便,参数也有重合:

  • body
  • subject
  • partner_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

评论区

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