现象
在一个已被关注的 channel 下,某子区(thread)已被归档(status=Archived)。用户在该归档子区里发了一条新消息,服务端把子区自动复活为 Active。此时:
- ✅ 右侧「活跃区 thread 列表」:子区立刻回到列表正确位置。
- ❌ 左侧「关注」folder 下该 channel 节点:子区不出现,且整页刷新后依然不出现。
根因
两侧列表的数据源不同,不是同一张表的两种过滤:
右侧「活跃区 thread 列表」 — 直接查 thread 表:
thread.DB.QueryByGroupNoWithStatus(groupNo, [Active]) // modules/thread/db.go:102
// SELECT * FROM thread WHERE group_no=? AND status IN (Active)
只认子区自身的 status,与"谁关注了""有没有关注记录"无关。所以 thread.status 一翻回 Active 立刻可见。
左侧「关注 folder 下该 channel 的子区」 — 查 user_conversation_ext 表:
convext.DB.ListThreadExts(uid, spaceID) // modules/conversation_ext/db.go:238
// SELECT * FROM user_conversation_ext WHERE uid=? AND space_id=? AND target_type=5
一个子区要出现在左侧关注列表,前置条件是该 (uid, thread) 必须存在一条 ext 行。thread.status=Active 不是充分条件——没有 ext 行,子区再活跃也进不了左侧。
缺口:ext 行(target_type=5)的创建路径不覆盖"复活"
ext 行目前只有两条创建路径:
FollowChannel 物化(modules/conversation_ext/service.go Phase 2):关注父群时通过 EnumerateActiveShortIDs(modules/message/1module.go:326)枚举子区,底层是 QueryByGroupNoWithStatus([Active]),只枚举 active 子区,archived 子区被跳过。
OnThreadCreated fanout(modules/conversation_ext/service.go:653):仅在子区被创建时触发,给 auto_follow_threads=1 AND group_unfollowed=0 的用户补 ext 行。
没有第三条。 子区从 archived 被消息复活时,没有任何逻辑给关注了父群的用户补 ext 行。
消息复活路径实锤(modules/thread/api.go:47 onMessages)只做两件事:
RecordMessageAndReactivate → UPDATE thread SET status=Active(modules/thread/db.go:784)——只动 thread 表;
JoinThread → 写 thread_member。
完全没碰 user_conversation_ext。 没有 fanout。
复现时序(解释全部症状)
- 某子区曾被归档。
- 用户关注了父 channel,但物化(
EnumerateActiveShortIDs)发生在该子区已归档之后 → 该子区没有 ext 行。
- 用户在归档子区发新消息 →
thread.status 翻回 Active。
- 右侧读
thread 表 → 立刻显示。
- 左侧读
user_conversation_ext → 该子区没有 ext 行 → 不在 sidebar/sync 返回里 → 左侧空白,整页刷新照样空白(DB 里就没这行,刷新不产生新行)。
建议修复(方案 A)
复活是一次"隐式的重新变活跃"事件,理应触发与 OnThreadCreated 对称的 ext 物化:当子区从 archived 复活为 active 时,若其父 channel 处于被关注状态,则自动把该子区也补成被关注态。
落点在 modules/thread/api.go 的 onMessages 里、确认确实发生了 archived → active 解档之后:
- 新增
OnThreadReactivated(groupNo, shortID),复用 OnThreadCreated 的 fanout 逻辑:按 auto_follow_threads=1 AND group_unfollowed=0 给活跃父群成员 INSERT IGNORE 一条 thread ext 行,并 bump follow_version。
- 让
RecordMessageAndReactivate 回传一个 reactivated bool(当前它把"本次是否真的解档"这个信息吞掉了,无法区分 archived→active 分支和纯统计分支),onMessages 据此仅在真解档时触发 fanout,避免每条消息都 fanout。
优点:
- 左右两侧语义收敛到同一套 fanout;
INSERT IGNORE 天然幂等,与已存在的 ext 行不冲突;
follow_version bump 让在线客户端自动重刷 sidebar/sync,无需整页刷新。
验证方式
复现后查该用户该子区的 ext 行是否缺失:
SELECT * FROM user_conversation_ext
WHERE uid = '<用户uid>' AND target_type = 5
AND target_id = '<groupNo>____<shortId>';
结果为空即实锤本 issue 的根因。
影响范围
- 仓库:
octo-server
- 相关文件:
modules/thread/api.go、modules/thread/db.go、modules/conversation_ext/service.go
- 用户可感知:被关注 channel 下的归档子区被消息复活后,在关注列表中永久缺失,直到用户重新关注父群或该子区有新建(fanout)事件。
现象
在一个已被关注的 channel 下,某子区(thread)已被归档(
status=Archived)。用户在该归档子区里发了一条新消息,服务端把子区自动复活为Active。此时:根因
两侧列表的数据源不同,不是同一张表的两种过滤:
右侧「活跃区 thread 列表」 — 直接查
thread表:只认子区自身的
status,与"谁关注了""有没有关注记录"无关。所以thread.status一翻回Active立刻可见。左侧「关注 folder 下该 channel 的子区」 — 查
user_conversation_ext表:一个子区要出现在左侧关注列表,前置条件是该
(uid, thread)必须存在一条 ext 行。thread.status=Active不是充分条件——没有 ext 行,子区再活跃也进不了左侧。缺口:ext 行(target_type=5)的创建路径不覆盖"复活"
ext 行目前只有两条创建路径:
FollowChannel物化(modules/conversation_ext/service.goPhase 2):关注父群时通过EnumerateActiveShortIDs(modules/message/1module.go:326)枚举子区,底层是QueryByGroupNoWithStatus([Active]),只枚举 active 子区,archived 子区被跳过。OnThreadCreatedfanout(modules/conversation_ext/service.go:653):仅在子区被创建时触发,给auto_follow_threads=1 AND group_unfollowed=0的用户补 ext 行。没有第三条。 子区从 archived 被消息复活时,没有任何逻辑给关注了父群的用户补 ext 行。
消息复活路径实锤(
modules/thread/api.go:47onMessages)只做两件事:RecordMessageAndReactivate→UPDATE thread SET status=Active(modules/thread/db.go:784)——只动thread表;JoinThread→ 写thread_member。完全没碰
user_conversation_ext。 没有 fanout。复现时序(解释全部症状)
EnumerateActiveShortIDs)发生在该子区已归档之后 → 该子区没有 ext 行。thread.status翻回Active。thread表 → 立刻显示。user_conversation_ext→ 该子区没有 ext 行 → 不在sidebar/sync返回里 → 左侧空白,整页刷新照样空白(DB 里就没这行,刷新不产生新行)。建议修复(方案 A)
复活是一次"隐式的重新变活跃"事件,理应触发与
OnThreadCreated对称的 ext 物化:当子区从 archived 复活为 active 时,若其父 channel 处于被关注状态,则自动把该子区也补成被关注态。落点在
modules/thread/api.go的onMessages里、确认确实发生了archived → active解档之后:OnThreadReactivated(groupNo, shortID),复用OnThreadCreated的 fanout 逻辑:按auto_follow_threads=1 AND group_unfollowed=0给活跃父群成员INSERT IGNORE一条 thread ext 行,并 bumpfollow_version。RecordMessageAndReactivate回传一个reactivated bool(当前它把"本次是否真的解档"这个信息吞掉了,无法区分 archived→active 分支和纯统计分支),onMessages据此仅在真解档时触发 fanout,避免每条消息都 fanout。优点:
INSERT IGNORE天然幂等,与已存在的 ext 行不冲突;follow_versionbump 让在线客户端自动重刷sidebar/sync,无需整页刷新。验证方式
复现后查该用户该子区的 ext 行是否缺失:
结果为空即实锤本 issue 的根因。
影响范围
octo-servermodules/thread/api.go、modules/thread/db.go、modules/conversation_ext/service.go