Skip to content

归档子区被新消息复活后,未同步补关注(ext)行 → 左侧「关注」列表永久缺失该子区 #586

Description

@pkuWMH

现象

在一个已被关注的 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 行目前只有两条创建路径:

  1. FollowChannel 物化modules/conversation_ext/service.go Phase 2):关注父群时通过 EnumerateActiveShortIDsmodules/message/1module.go:326)枚举子区,底层是 QueryByGroupNoWithStatus([Active])只枚举 active 子区,archived 子区被跳过
  2. OnThreadCreated fanoutmodules/conversation_ext/service.go:653):仅在子区被创建时触发,给 auto_follow_threads=1 AND group_unfollowed=0 的用户补 ext 行。

没有第三条。 子区从 archived 被消息复活时,没有任何逻辑给关注了父群的用户补 ext 行。

消息复活路径实锤(modules/thread/api.go:47 onMessages)只做两件事:

  1. RecordMessageAndReactivateUPDATE thread SET status=Activemodules/thread/db.go:784)——只动 thread 表;
  2. JoinThread → 写 thread_member

完全没碰 user_conversation_ext 没有 fanout。

复现时序(解释全部症状)

  1. 某子区曾被归档。
  2. 用户关注了父 channel,但物化(EnumerateActiveShortIDs)发生在该子区已归档之后 → 该子区没有 ext 行
  3. 用户在归档子区发新消息 → thread.status 翻回 Active
  4. 右侧读 thread 表 → 立刻显示。
  5. 左侧读 user_conversation_ext → 该子区没有 ext 行 → 不在 sidebar/sync 返回里 → 左侧空白,整页刷新照样空白(DB 里就没这行,刷新不产生新行)。

建议修复(方案 A)

复活是一次"隐式的重新变活跃"事件,理应触发与 OnThreadCreated 对称的 ext 物化:当子区从 archived 复活为 active 时,若其父 channel 处于被关注状态,则自动把该子区也补成被关注态。

落点在 modules/thread/api.goonMessages 里、确认确实发生了 archived → active 解档之后:

  1. 新增 OnThreadReactivated(groupNo, shortID),复用 OnThreadCreated 的 fanout 逻辑:按 auto_follow_threads=1 AND group_unfollowed=0 给活跃父群成员 INSERT IGNORE 一条 thread ext 行,并 bump follow_version
  2. 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.gomodules/thread/db.gomodules/conversation_ext/service.go
  • 用户可感知:被关注 channel 下的归档子区被消息复活后,在关注列表中永久缺失,直到用户重新关注父群或该子区有新建(fanout)事件。

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:backendBackend areaauto-triagedAuto-triaged by automation; process marker + re-triage lockbug-verifiedBug confirmed via code analysispriority:P2Medium — next sprinttype:bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions