先说结论
很多人看到 Odoo Discuss 的消息表情反应,会把它当成一个“锦上添花”的前端交互。
但从源码看,它远不只是加个 emoji 按钮。
Odoo 为 reaction 单独建了 mail.message.reaction 模型,并且明确处理:
- internal partner 和 guest 的身份差异;
- 同一人对同一消息同一表情只能有一条反应;
- 反应不是逐条全量推送,而是按“消息 + emoji”分组回传;
- 登录用户走 bus 实时同步,未登录场景可走 Store 回填。
一句话说:
Odoo 把 reaction 设计成一层轻量但严肃的协作信号,而不是无状态的前端点赞动画。
主要看哪里
核心源码在:
addons/mail/models/mail_message_reaction.pyaddons/mail/models/mail_message.pyaddons/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_idcontentpartner_idguest_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_idcontent
去找一组 reactions,然后输出为一个 reaction group。
mail.message.reaction._to_store() 也不是简单把每条数据库记录吐出去,而是聚合成:
contentcountguestspartnersmessagesequence
这说明前端真正需要的不是“第 123 条 reaction 记录”,而是:
这条消息上,这个表情现在有多少人点了,分别是谁。
这种分组设计非常适合协同场景:
- 展示简单;
- 更新成本低;
- 用户能快速看到共识信号;
- 又保留了查看参与人的能力。
第四层:为什么 sequence 取的是最小 id
聚合后的 reaction group 里有一个:
sequence = min(reactions.ids)
这不是随手写的。
它意味着 Odoo 在给“这组 reaction”一个稳定顺序锚点。
因为当同一表情被多人连续点亮时,前端需要一个相对稳定的分组顺序,不然列表会不停跳动。
对于协同产品来说,微小的排序抖动也会影响阅读体验。
Odoo 用最早那条 reaction 的 id 作为序列基准,是一个很轻巧但很实用的做法。
第五层:为什么登录用户和未登录场景走的同步策略不完全一样
_message_reaction() 在处理 add/remove 后会做两件事:
- 如果传入
store,就把 reaction group 填进 store; - 然后无论如何都
_bus_send_reaction_group(content)。
源码注释已经说了:
- Store 回填主要给非登录 portal 场景;
- bus 推送用于登录用户的实时同步。
这说明 Odoo 不是简单“前端再全量刷新一次消息”。
它把不同身份场景拆开优化:
- 已登录用户,走实时推送,更新快;
- 其他场景,走 Store 补数据,仍能拿到正确聚合结果。
这正是一个多人协同界面该有的实时性分层。
第六层:reaction 的产品意义,其实是“低成本确认”
为什么 Odoo 要把这件小事做得这么认真?
因为在协作里,reaction 解决的是很常见的一类需求:
- 我看到了;
- 我同意;
- 我收到;
- 我笑了;
- 我不想再发一条占频道的新消息。
如果没有 reaction,团队经常只能靠:
- “收到”
- “+1”
- “OK”
- “哈哈”
这种低信息量消息把线程刷长。
所以 reaction 虽然轻,但它降低的是协作摩擦,不是装饰气氛。
一句话记住
Odoo 的消息表情反应不是前端糖,而是一层认真设计的协作信号系统:
它把身份区分、唯一约束、分组聚合和实时同步都建模好了,好让“轻反馈”既轻量,又不会失真。
如果你在做聊天室、项目讨论、内部协作中心,这个设计非常值得抄。
DISCUSSION
评论区