协同办公

Odoo 为什么既有定时消息又有定时通知:`mail.scheduled.message` 与 `mail.message.schedule` 两套延迟机制讲透

在 Odoo 里,“稍后发送”并不是一个单点功能。源码把“延迟发正文”和“正文先生成、通知稍后送达”拆成了两条链:`mail.scheduled.message` 和 `mail.message.schedule`。这正是协同体验能兼顾留痕、提醒和失败兜底的关键。

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

先说结论

很多人会把 Odoo 的“稍后发送”理解成一个功能按钮背后的单一队列。

其实不是。

从官方源码看,Odoo 把“延迟”拆成了两种完全不同的事:

  1. 消息正文本身晚点再发
  2. 消息现在就入线程,但通知晚点再送达

对应两条模型链路:

  • mail.scheduled.message
  • mail.message.schedule

一句话说:

Odoo 不是简单地把“发送时间”做成一个字段,而是把“什么时候形成业务消息”和“什么时候打扰收件人”拆开处理。

这正是协同系统设计得比较成熟的表现。


这篇主要看哪里

核心源码在:

  • addons/mail/models/mail_scheduled_message.py
  • addons/mail/models/mail_thread.py

可以重点看这些方法:

  • mail.scheduled.message.create()
  • mail.scheduled.message._post_message()
  • mail.scheduled.message._post_messages_cron()
  • mail.thread._notify_thread()
  • mail.thread._is_notification_scheduled()

只要把这几段连起来,Odoo 的“延迟协同”思路就很清楚了。


第一条链:mail.scheduled.message 是把正文延后生成

mail.scheduled.message 的模型注释写得很直白:

它保存的是由 composer 生成的发帖参数,目的是把消息的 posting 延后。

这里的 posting 不是“延迟发邮件附件通知”那么简单,而是:

  • 目标业务记录上暂时还没有那条消息;
  • 到了时间,系统再以创建者身份调用 message_post()
  • 那时消息才真正进入业务线程。

这意味着:

在触发时间到来之前

  • 业务对象 Chatter 里还没有这条消息;
  • 其他协作者也看不到它;
  • 附件只是先挂在 mail.scheduled.message 上。

到了触发时间

源码会:

  • create_uid 重新检查权限;
  • 把附件转入真实消息;
  • 用原作者、原正文、原收件人去 message_post()
  • 成功后再删掉 scheduled message 本身。

所以 mail.scheduled.message 的本质是:

延迟创建业务线程消息。


第二条链:mail.message.schedule 是消息先存在,通知晚点送

另一条链藏在 mail_thread.py_notify_thread() 里。

这里会先算好 recipients_data,然后检查:

  • scheduled_date
  • _is_notification_scheduled()

如果判定通知时间在未来,它不会立刻走:

  • inbox
  • email
  • web push

而是创建一条:

  • mail.message.schedule

注意这和 mail.scheduled.message 完全不是一回事。

这里的前提是:

  • 消息已经创建出来了
  • 它已经是业务线程的一部分
  • 只是通知动作延后执行。

也就是说,协作留痕和打扰用户,被拆成了两个时点。

这在团队协作里很有价值。

比如:

  • 你想先把消息写进任务历史;
  • 但不想现在打扰所有人;
  • 于是让通知在更合适的时间发出去。

为什么 Odoo 要故意拆成两套,而不是做一个统一延迟队列

因为这两件事在协作语义上完全不同。

延迟正文

关注的是:

  • 什么时候把事件正式写入业务历史;
  • 什么时候让协作者在 Chatter 里看到它;
  • 什么时候建立 reply-to、subtype、followers 等上下文。

延迟通知

关注的是:

  • 这条消息现在已经存在;
  • 但 inbox / email / push 什么时候送更合适;
  • 如何减少打扰而不牺牲可追溯性。

如果把两者强行揉成一种机制,系统会很难同时满足:

  • 留痕准确;
  • 提醒节奏合理;
  • 权限与失败处理清晰。

mail.scheduled.message 为什么要以创建者身份重新发

源码里 _post_message() 有一个很关键的动作:

  • scheduled_message.with_user(message_creator)._check()
  • 然后目标记录 .with_user(message_creator).message_post(...)

这说明 Odoo 不是“到点后由系统超级用户代发”。

它故意保留原创建者视角,重新检查:

  • 这个人现在还有没有发帖权限;
  • 附件是否还能正确归档;
  • 原上下文是否仍可用。

这背后是很现实的协作问题:

  • 人可能离职了;
  • 权限可能被撤掉了;
  • 记录状态可能变了;
  • 计划中的消息不一定还应该被允许发送。

所以 Odoo 的默认设计是:

定时消息不是“系统替你完成意图”,而是“在未来时点再次验证你是否仍有资格完成这次发帖”。


失败时为什么不用 message_post(),而改用 message_notify()

这也是一处非常漂亮的边界设计。

如果定时消息发送失败,源码会给原作者发:

  • self.env['mail.thread'].message_notify(...)

而不是往目标记录上再 message_post() 一条“发送失败”。

原因很合理:

  • 失败提醒是系统告警,不是业务正文;
  • 不应该污染目标记录 Chatter;
  • 原作者才是最需要知道的人。

这也再次说明 Odoo 在协同设计上很在意正文语义提醒语义的分层。


这两套机制分别适合什么场景

mail.scheduled.message 思路,适合:

  • 晚点再把评论正式发到任务、项目、商机;
  • 让某条留言在将来某个时刻才进入业务历史;
  • 希望在发送前保留可编辑、可取消空间。

mail.message.schedule 思路,适合:

  • 消息现在就应该入线程;
  • 但通知不该立刻打扰所有人;
  • 希望把留痕和提醒节奏解耦。

如果业务上分不清这两种需求,协同体验通常会出问题:

  • 要么历史留得太晚;
  • 要么用户被提醒得太早;
  • 要么失败以后根本不知道该通知谁。

开发时最容易犯的误区

误区一:把“稍后通知”做成“稍后创建消息”

结果就是:

  • 业务线程在关键时间点上缺历史;
  • 别人看不到已经确定的协作内容;
  • 后续动作难以引用这条消息。

误区二:把“稍后发正文”做成“先发再静默”

这样做虽然看上去也减少了打扰,但会让业务对象提前出现本不该出现的内容。

误区三:忽略未来权限变化

Odoo 在定时正文发送时重新检查创建者权限,这一点很多二开没做到。

如果直接用系统权限兜底,短期省事,长期就会留下非常难解释的安全和责任问题。


一句话记住

在 Odoo 的协同世界里,“稍后发送”至少有两层含义:

是让消息晚点进入业务历史,还是让消息先存在、提醒晚点抵达。

mail.scheduled.message 解决前者,mail.message.schedule 解决后者。

看懂这组拆分,你会更容易设计出既不吵人、又不丢上下文的协作流程。

DISCUSSION

评论区

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