协同办公

Odoo Discuss 未读数为什么不是“总消息数”:new_message_separator、seen_message_id 和置顶状态背后的读取模型

很多人以为 Discuss 的未读数就是“新消息条数”。但从官方源码看,Odoo 真正在维护的是一套成员级阅读游标、分隔线和置顶兴趣状态。理解这套模型,才能真正看懂聊天列表为什么会那样变化。

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

先说结论

在 Odoo Discuss 里,未读数并不是“这个频道里新来了多少条消息”这么简单。

官方源码真正维护的是一套成员级阅读状态,核心字段至少有这几个:

  • seen_message_id:我最后真正看到哪条消息
  • fetched_message_id:客户端已经拉到哪条消息
  • new_message_separator:界面上的“新消息分隔线”应该放在哪
  • message_unread_counter:对这个成员来说,还算未读的消息数
  • last_interest_dt / unpin_dt:这个会话还应不应该继续被置顶显示

一句话说:

Odoo Discuss 不是按频道统一算未读,而是按“成员自己的阅读游标”来算。

这也是为什么同一个频道里,不同人的未读数、置顶状态、分隔线位置都可能不一样。


源码从哪里看

这套逻辑最直接的入口,在官方源码:

  • addons/mail/models/discuss/discuss_channel_member.py

这个模型不是“频道本身”,而是某个成员在某个频道里的那一份状态

这点非常关键。

因为协同系统里真正需要保存的,不只是“这个群里有什么消息”,还包括:

  • 我读到哪里了
  • 我是不是把它静音了
  • 我有没有把它取消置顶
  • 我是否还对这个会话保持兴趣

这些都不是频道公共属性,而是用户私有状态。


为什么 Odoo 要把阅读状态放在 discuss.channel.member

很多 IM 产品刚上手时,大家直觉会把“未读数”理解成频道自己的字段。

但 Odoo 没这样做。

因为一个频道可以有很多成员,而每个人的阅读进度都不同。

所以源码里把下面这些状态放在 discuss.channel.member 上:

  • seen_message_id
  • fetched_message_id
  • new_message_separator
  • message_unread_counter
  • custom_notifications
  • mute_until_dt
  • unpin_dt
  • last_interest_dt

这意味着 Odoo 从模型层就承认了一件事:

讨论频道是共享空间,但阅读体验是每个成员各自维护的。

这比“群里有多少未读”要精细得多,也更符合真实协作场景。


message_unread_counter 为什么不是简单的“新消息总数”

discuss_channel_member.py 里,_compute_message_unread() 的 SQL 很值得看。

它并不是按“消息创建时间晚于上次打开时间”来算,而是按:

  • 消息属于当前 discuss.channel
  • message_type 不能是 notificationuser_notification
  • 消息 id >= new_message_separator

也就是说,Odoo 的未读数更接近:

从“新消息分隔线”往后的、真正算聊天内容的消息数。

这立刻解释了几个常见现象:

1. 系统通知不一定算进未读

像某些纯通知型消息,源码明确排除了。

2. 未读数和“最后看见哪条”不是一个字段直接减出来

它依赖的是 new_message_separator,而不是单纯的 seen_message_id

3. 你把分隔线往后推,未读数就会跟着变化

因为它本质上是“从哪开始算新消息”。

所以未读数不是总量,而是带有界面语义的计数


seen_message_idnew_message_separator 为什么要分开

这是很多人第一次读源码时最容易忽略的设计。

直觉上,好像只要一个“最后已读消息 ID”就够了。

但 Odoo 还是把它拆成两个概念:

seen_message_id

表示:

  • 我最后真正读到哪条消息
  • 这个字段更像“阅读游标”

new_message_separator

表示:

  • UI 上“新消息从哪里开始”
  • 这个字段更像“界面断点”

_mark_as_read() 里,Odoo 会先 _set_last_seen_message(last_message),再 _set_new_message_separator(last_message.id + 1)

这说明官方设计并不是简单复用一个字段,而是把:

  • 阅读事实
  • 界面呈现

刻意分开。

这很聪明。

因为协同产品里的“我看到了哪”与“UI 应该从哪画一条新旧分界线”,虽然相关,但并不完全等价。


为什么 Odoo 要额外记录 fetched_message_id

fetched_message_id 容易被忽略,但它其实很有产品意义。

它表示的是:

  • 客户端至少已经拉取到哪条消息

这和“用户认真读过”不是一回事。

比如一个聊天窗口被打开了,前端可能已经把消息取回来了,但用户未必真正滚动到最底部,也未必完成“已读”动作。

所以 Odoo 把它拆开:

  • fetched_message_id:数据层面拿到了
  • seen_message_id:阅读层面确认看到了

这正是一个成熟协同系统该有的区分。


置顶为什么也不是一个简单布尔值

很多人会以为“是否置顶”只要一个 is_pinned 字段就好了。

但源码里真正存的是:

  • unpin_dt
  • last_interest_dt
  • 频道自己的 last_interest_dt

然后通过 _compute_is_pinned() 动态算出 is_pinned

判断逻辑大意是:

  • 如果从未取消置顶,那就算 pinned
  • 如果成员最近还有兴趣事件,且时间晚于取消置顶时间,那又会恢复成 pinned
  • 如果频道最近也有值得关注的事件,同样可能重新变成 pinned

这背后体现的不是“我点没点图钉”,而是:

Odoo 在区分“手工取消置顶”与“这个频道后来又重新变得重要”。

这非常像真实办公协作:

  • 某个群你昨天不想看了
  • 但今天又有关键互动发生
  • 它就值得重新回到你视野里

为什么会有 last_interest_dt

last_interest_dt 的注释写得很直白:

它记录“最近一次有趣事件”的时间,比如:

  • 创建
  • 加入
  • 置顶

看起来这只是体验细节,实际上它是会话排序和浮现机制的一部分。

也就是说,Odoo 并不是只按最后一条消息排序会话,而是在成员层面维护一个“这条会话对我最近还有没有价值”的信号。

所以 Discuss 的会话列表,本质上并不是简单消息流,而是协同优先级列表


静音也不是“完全不收消息”

custom_notificationsmute_until_dt 也很值得一起看。

源码里支持:

  • all:所有消息
  • mentions:只关心提及
  • no_notif:不接收提醒

再加上 mute_until_dt,说明 Odoo 在通知设计上不是粗暴的“开 / 关”,而是允许:

  • 永久偏好
  • 临时静音
  • 只保留高优先级提醒

这对办公协同尤其重要。

因为工作群最常见的问题不是“消息太少”,而是“消息太多”。

Odoo 的思路很清楚:

读取状态、未读计数、通知策略,是同一套成员状态模型的不同侧面。


这套设计对业务协同有什么现实意义

如果你把 Discuss 仅仅当聊天工具,就会觉得这些字段太复杂。

但一旦把它看成协同中枢,这套设计就很合理:

1. 每个人都能维护自己的阅读上下文

同一个频道,不同岗位的人关注点不一样。

2. 界面能稳定表现“哪里开始是新的”

这能降低来回切换上下文的成本。

3. 重要频道可以被再次浮现

不是所有取消置顶都意味着永久沉底。

4. 通知不会粗暴一刀切

只提及、临时静音、完全关闭,都有明确支撑。


做 Discuss 定制时最容易犯的错

1. 只看 seen_message_id,忽略 new_message_separator

结果你算出来的“未读”跟前端看到的不一致。

2. 把置顶当成一个静态布尔开关

结果会把官方那套“兴趣恢复”逻辑整没了。

3. 误把 fetchedseen

客户端取到了,不代表用户真的看完了。

4. 忽略 message_type 的过滤

这样你会把不该算进聊天未读的系统消息也算进去。


一句话记忆法

Odoo Discuss 的未读数,本质上不是频道消息总量,而是“某个成员从自己的新消息分隔线之后,还有多少真正算聊天内容的消息没读”。

理解这一句,你就能看懂 Discuss 列表、分隔线、静音和置顶为什么会以现在这种方式联动。

DISCUSSION

评论区

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