修复两个严重崩溃:群管理页面查询键冲突导致整个会话崩溃、禁言列表读取

不存在的嵌套字段崩溃

- ConversationView.tsx和GroupAdminModal.tsx对群禁言列表用了完全相同的
  React Query缓存键['group-mutes', topicId],但分别调用getGroupMutes
  (单页,{items:[]}包装对象)和getAllGroupMutes(全部页,展开后的纯数组)
  两个返回形状不同的函数。两个组件同时挂载时后写入的一方会覆盖缓存,
  另一方读到错误形状后对undefined取.length直接崩溃,导致只要打开某个
  群的"群管理"面板整个会话界面就白屏。改成两处统一用getAllGroupMutes,
  真正共享同一份、同一种形状的数据
- GroupMute类型声明的是嵌套的user:{user_id,display_name},但真实响应
  是扁平的{user_id,muted_by,muted_at,reason,status,version},且完全没有
  display_name字段。这个字段错配从来没被暴露过,因为此前从未有真实账号
  真正禁言过人后去看这个列表——本轮实测"禁言→查看列表"时才第一次真正
  用真实数据渲染,立刻崩溃。改成扁平字段+交叉查群成员列表解析真实姓名
  (同一模式ConversationView.tsx的memberByUserId、GroupFilesModal的
  uploader_id都已经在用)

用全新账号在全新群里完整实测:禁言成员1人正确显示头像+姓名"systester2"
(不再是原始uid或崩溃),解除禁言后列表正确清零,两处此前必崩的入口
(群管理面板本身、禁言列表渲染)全部验证通过。
This commit is contained in:
Your Name
2026-09-16 16:23:06 +08:00
parent 8013c1675f
commit 92892e3e7b
3 changed files with 49 additions and 21 deletions
+14 -3
View File
@@ -17,7 +17,7 @@ import { GroupAnnouncementModal } from '@/features/groups/GroupAnnouncementModal
import { JoinRequestsModal } from '@/features/groups/JoinRequestsModal';
import { useBootstrap } from '@/lib/api/hooks';
import { favoriteMessages, type MediaType, recallMessage } from '@/lib/api/messages';
import { getAllGroupMembers, getGroup, getGroupGovernance, getGroupMutes, getGroupSummary } from '@/lib/api/groups';
import { getAllGroupMembers, getAllGroupMutes, getGroup, getGroupGovernance, getGroupSummary } from '@/lib/api/groups';
import { getMe } from '@/lib/api/profile';
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
import { FETCH_PAGE_TRIGGER, MESSAGES_PAGE } from '@/lib/config';
@@ -301,9 +301,20 @@ export function ConversationView() {
queryFn: () => getGroupGovernance(topicId || ''),
enabled: isGroup,
});
// Real, live-confirmed crash (not theoretical): this used to call `getGroupMutes` (single page,
// `{items, next_cursor}` shape) under query key `['group-mutes', topicId]` -- the EXACT SAME key
// `GroupAdminModal.tsx` uses for `getAllGroupMutes` (all pages, flat array). Both mounted together
// (opening 群管理 from a group's own conversation screen, which is the only way to reach it) means
// whichever query resolves last overwrites the shared cache entry with ITS shape; the other
// consumer then reads `array.items.length` -- `.items` is undefined on a plain array, and unlike
// `mutesQuery.data?.items.length`'s leading `?.`, nothing guards THAT specific property access, so
// it throws and takes down the whole conversation screen. Matching `getAllGroupMutes` here (same
// function, same shape, same key) turns this from two competing queries into one correctly-shared
// one -- also fixes this subline's own under-count for any group with more muted members than a
// single page holds, the same truncation bug `getAllGroupMutes`'s own doc comment already flags.
const mutesQuery = useQuery({
queryKey: ['group-mutes', topicId],
queryFn: () => getGroupMutes(topicId || ''),
queryFn: () => getAllGroupMutes(topicId || ''),
enabled: isGroup,
});
const memberByUserId = useMemo(() => new Map((membersQuery.data || []).map((m) => [m.user_id, m])), [membersQuery.data]);
@@ -311,7 +322,7 @@ export function ConversationView() {
const isGroupAdmin = myMember?.role === 'owner' || myMember?.role === 'admin';
const groupSublineParts = [
governanceQuery.data?.join_approval ? '已开启进群验证' : null,
mutesQuery.data?.items.length ? `${mutesQuery.data.items.length} 位成员被禁言` : null,
mutesQuery.data?.length ? `${mutesQuery.data.length} 位成员被禁言` : null,
].filter(Boolean);
if (!topicId) return null;