协同办公

Odoo 表情反应为什么不是前端小玩具:partner/guest 唯一约束、反应分组与实时总线同步讲透

在 Odoo Discuss 里,给消息点个表情看起来很轻,但源码把 reaction 做成了独立模型、按 partner/guest 分开唯一约束,并通过 Store 与 bus 做增量同步。它不是 UI 糖,而是一个认真建模的协作信号层。

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

先说结论

很多人看到 Odoo Discuss 的消息表情反应,会把它当成一个“锦上添花”的前端交互。

但从源码看,它远不只是加个 emoji 按钮。

Odoo 为 reaction 单独建了 mail.message.reaction 模型,并且明确处理:

  • internal partner 和 guest 的身份差异;
  • 同一人对同一消息同一表情只能有一条反应;
  • 反应不是逐条全量推送,而是按“消息 + emoji”分组回传;
  • 登录用户走 bus 实时同步,未登录场景可走 Store 回填。

一句话说:

Odoo 把 reaction 设计成一层轻量但严肃的协作信号,而不是无状态的前端点赞动画。


主要看哪里

核心源码在:

  • addons/mail/models/mail_message_reaction.py
  • addons/mail/models/mail_message.py
  • addons/mail/static/src/core/common/*reaction*

重点方法和结构:

  • mail.message._message_reaction()
  • mail.message._reaction_group_to_store()
  • mail.message._bus_send_reaction_group()
  • mail.message.reaction._to_store()

第一层:reaction 是独立模型,不是挂在消息 JSON 里的临时数组

mail.message.reaction 这个模型本身就说明 Odoo 很认真在建模这件事。

字段很简单:

  • message_id
  • content
  • partner_id
  • guest_id

但越简单越说明它在刻意表达一个稳定事实:

  • 某条消息;
  • 某个表情;
  • 某个反应人;
  • 一次明确反应。

这意味着 reaction 在 Odoo 里是可追踪、可同步、可聚合的数据实体,而不是“页面上临时加个数字”。


第二层:为什么 partner 和 guest 要分开建唯一约束

源码里有两个独立唯一索引:

  • (message_id, content, partner_id)
  • (message_id, content, guest_id)

并且还有约束确保:

  • 一条 reaction 要么来自 partner,要么来自 guest,不能同时都有,也不能都没有。

这看起来只是数据库细节,实际上体现了 Odoo 很重要的协同现实:

  • 参与讨论的人不一定都是登录内部用户;
  • guest 也是协作空间的一等参与者;
  • 但他们的身份边界不能和 partner 混成一锅。

如果你把 guest 也偷懒映射成 partner,后续:

  • 展示身份;
  • 去重;
  • 访问控制;
  • presence / 匿名协作

都会开始混乱。


第三层:为什么返回的是“反应分组”,不是逐条把所有 reaction 丢给前端

_reaction_group_to_store() 会按:

  • message_id
  • content

去找一组 reactions,然后输出为一个 reaction group。

mail.message.reaction._to_store() 也不是简单把每条数据库记录吐出去,而是聚合成:

  • content
  • count
  • guests
  • partners
  • message
  • sequence

这说明前端真正需要的不是“第 123 条 reaction 记录”,而是:

这条消息上,这个表情现在有多少人点了,分别是谁。

这种分组设计非常适合协同场景:

  • 展示简单;
  • 更新成本低;
  • 用户能快速看到共识信号;
  • 又保留了查看参与人的能力。

第四层:为什么 sequence 取的是最小 id

聚合后的 reaction group 里有一个:

  • sequence = min(reactions.ids)

这不是随手写的。

它意味着 Odoo 在给“这组 reaction”一个稳定顺序锚点。

因为当同一表情被多人连续点亮时,前端需要一个相对稳定的分组顺序,不然列表会不停跳动。

对于协同产品来说,微小的排序抖动也会影响阅读体验。

Odoo 用最早那条 reaction 的 id 作为序列基准,是一个很轻巧但很实用的做法。


第五层:为什么登录用户和未登录场景走的同步策略不完全一样

_message_reaction() 在处理 add/remove 后会做两件事:

  1. 如果传入 store,就把 reaction group 填进 store;
  2. 然后无论如何都 _bus_send_reaction_group(content)

源码注释已经说了:

  • Store 回填主要给非登录 portal 场景;
  • bus 推送用于登录用户的实时同步。

这说明 Odoo 不是简单“前端再全量刷新一次消息”。

它把不同身份场景拆开优化:

  • 已登录用户,走实时推送,更新快;
  • 其他场景,走 Store 补数据,仍能拿到正确聚合结果。

这正是一个多人协同界面该有的实时性分层。


第六层:reaction 的产品意义,其实是“低成本确认”

为什么 Odoo 要把这件小事做得这么认真?

因为在协作里,reaction 解决的是很常见的一类需求:

  • 我看到了;
  • 我同意;
  • 我收到;
  • 我笑了;
  • 我不想再发一条占频道的新消息。

如果没有 reaction,团队经常只能靠:

  • “收到”
  • “+1”
  • “OK”
  • “哈哈”

这种低信息量消息把线程刷长。

所以 reaction 虽然轻,但它降低的是协作摩擦,不是装饰气氛。


一句话记住

Odoo 的消息表情反应不是前端糖,而是一层认真设计的协作信号系统:

它把身份区分、唯一约束、分组聚合和实时同步都建模好了,好让“轻反馈”既轻量,又不会失真。

如果你在做聊天室、项目讨论、内部协作中心,这个设计非常值得抄。

DISCUSSION

评论区

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