From 27cd042df9aca0383a49f3514bc21958dd890912 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 15:42:23 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加IM即时通讯模块

---
 src/views/im/home/store/groupStore.ts | 1017 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 1,017 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/store/groupStore.ts b/src/views/im/home/store/groupStore.ts
new file mode 100644
index 0000000..c9e2786
--- /dev/null
+++ b/src/views/im/home/store/groupStore.ts
@@ -0,0 +1,1017 @@
+import type { Group, GroupDO, GroupMember, Message } from '../types'
+
+import type { ImGroupApi } from '#/api/im/group'
+import type { ImGroupMemberApi } from '#/api/im/group/member'
+
+import { CommonStatusEnum } from '#/packages/constants/src'
+
+import { acceptHMRUpdate, defineStore } from 'pinia'
+
+import { getGroup as apiGetGroup, getMyGroupList as apiGetMyGroupList } from '#/api/im/group'
+import { getGroupMember as apiGetGroupMember, getGroupMemberList as apiGetGroupMemberList, updateGroupMember as apiUpdateGroupMember } from '#/api/im/group/member'
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import {
+  ImContentType,
+  ImConversationType,
+  ImGroupMemberRole,
+  ImMessageStatus
+} from '../../utils/constants'
+import { getDb } from '../../utils/db'
+import { type GroupNotificationPayload } from '../../utils/message'
+import { getGroupDisplayName } from '../../utils/user'
+import { useConversationStore } from './conversationStore'
+import { useGroupRequestStore } from './groupRequestStore'
+
+/** clear() 鏃堕�掑锛涙棫璐﹀彿 in-flight 鐨勬垚鍛樿姹傝繑鍥炲悗姣斿涓�鑷存墠鍐� store */
+let storeEpoch = 0
+
+/**
+ * fetchGroupMemberList 骞跺彂鍘婚噸琛細鍚� groupId 鍚屾椂杩涚殑璇锋眰鍏辩敤涓�涓� Promise
+ *
+ * key 蹇呴』甯� userId鈥斺�旇处鍙峰垏鎹㈡椂 A 鐨勮姹備笉鑳借 B 澶嶇敤锛屽惁鍒� IIFE 鍐呴儴鐨� saveGroupMemberList 浼氭妸 A 鐨勬垚鍛樻暟鎹啓杩� B 鐨� IDB 妗�
+ */
+const pendingMemberFetches = new Map<string, Promise<GroupMember[]>>()
+const pendingMemberKey = (userId: number, groupId: number) => `${userId}:${groupId}`
+
+/**
+ * fetchGroupMember 鍗曟垚鍛樺苟鍙戝幓閲嶈〃锛氬悓 (groupId, memberUserId) 鍚屾椂杩涚殑璇锋眰鍏辩敤涓�涓� Promise
+ *
+ * 璺熸暣缇よ〃鍒嗗紑锛氬崟鎴愬憳 fetch 璺熸暣缇� fetch 璇箟涓嶅悓锛堝崟鎴愬憳涓嶅洖濉� me 鐨� silent锛夛紝涓嶈兘浜掔浉浠f浛
+ */
+const pendingSingleMemberFetches = new Map<string, Promise<GroupMember | null>>()
+
+const pendingSingleMemberKey = (userId: number, groupId: number, memberUserId: number) =>
+  `${userId}:${groupId}:${memberUserId}`
+
+/** 鏋勫缓缇� IndexedDB 璁板綍 */
+function buildGroupDO(group: Group): GroupDO {
+  const {
+    activeCallExpired: _activeCallExpired,
+    activeCallLoaded: _activeCallLoaded,
+    infoLoaded: _infoLoaded,
+    members: _members,
+    membersLoaded: _membersLoaded,
+    membersExpired: _membersExpired,
+    ...record
+  } = group
+  return record
+}
+
+/** 鍒ゆ柇褰撳墠鐢ㄦ埛鏄惁鍦� payload.memberUserIds 閲岋紙GROUP_CREATE / INVITE / KICK 鑷垽鐢級 */
+function isSelfInPayloadMembers(payload: GroupNotificationPayload): boolean {
+  const selfUserId = getCurrentUserId()
+  return !!selfUserId && (payload.memberUserIds || []).includes(selfUserId)
+}
+
+/** 鍒锋柊鎴戠鐞嗙殑缇ょ敵璇风孩鐐� */
+function refreshUnhandledGroupRequests(): void {
+  useGroupRequestStore()
+    .fetchUnhandledGroupRequestList()
+    .catch(() => undefined)
+}
+
+/**
+ * IM 缇� Store
+ *
+ * 璐熻矗锛�
+ * - 鎷夊彇 / 缂撳瓨褰撳墠鐧诲綍鐢ㄦ埛鍔犲叆鐨勭兢鍒楄〃
+ * - 鎸� groupId 鎳掑姞杞界兢鎴愬憳锛堜緵 ConversationGroupSide / MentionPicker / MessageReadStatus 娑堣垂锛�
+ * - 鎴愬憳"宸茶 / 鏈"绛夎仛鍚堟煡璇㈢敱 MessageReadStatus 鍙﹁缁勫悎
+ */
+export const useGroupStore = defineStore('imGroupStore', {
+  state: () => ({
+    groups: [] as Group[],
+    loaded: false, // 浠� fetchGroupList 鎴愬姛鍚庣疆浣嶏紱loadGroupList锛圛DB锛変笉缃綅锛屽惁鍒欏悗鍙� SWR 鍒锋柊浼氳缂撳瓨鍛戒腑璺宠繃
+    groupMembersExpired: false // 杩涘叆 IM / 閲嶈繛鍚庣疆浣嶏紱IDB 閲岀殑鎴愬憳妗跺欢杩熷姞杞藉埌鍐呭瓨鏃讹紝涔熻鎸夎繃鏈熷鐞�
+  }),
+
+  getters: {
+    getGroup:
+      (state) =>
+      (id: number): Group | undefined => {
+        return state.groups.find((g) => g.id === id)
+      },
+    /** 缇ゆ垚鍛� userId 鈫� GroupMember 绱㈠紩锛涜皟鐢ㄦ柟鎸� userId 鍙嶆煡鏄电О / 澶村儚绛夊厓淇℃伅 */
+    getGroupMemberMap:
+      (state) =>
+      (id: number): Map<number, GroupMember> => {
+        const group = state.groups.find((g) => g.id === id)
+        return new Map((group?.members || []).map((m) => [m.userId, m]))
+      }
+  },
+
+  actions: {
+    // ==================== 鏈湴缂撳瓨 ====================
+
+    /** 浠� IndexedDB 鎭㈠缇ゅ垪琛� */
+    async loadGroupList(): Promise<boolean> {
+      try {
+        const cached = await getDb().getAll<GroupDO>('groups')
+        if (!cached || cached.length === 0) {
+          return false
+        }
+        this.groups = cached
+        return true
+      } catch (error) {
+        console.warn('[IM groupStore] 鏈湴缇ょ紦瀛樿鍙栧け璐�', error)
+        return false
+      }
+    },
+
+    /** 淇濆瓨缇ゅ垪琛� */
+    saveGroupList(): void {
+      void getDb()
+        .transaction(['groups'], 'readwrite', async (tx) => {
+          const db = getDb()
+          await db.clearStore('groups', tx)
+          for (const group of this.groups) {
+            await db.put('groups', buildGroupDO(group), tx)
+          }
+        })
+        .catch((error) => console.warn('[IM groupStore] 鏈湴缇ょ紦瀛樺啓鍏ュけ璐�', error))
+    },
+
+    /** 淇濆瓨鍗曚釜缇� */
+    async saveGroupRecord(group: Group | undefined): Promise<void> {
+      if (!group) {
+        return
+      }
+      await getDb().put('groups', buildGroupDO(group))
+    },
+
+    /** 淇濆瓨鍗曚釜缇� */
+    saveGroup(group: Group | undefined): void {
+      void this.saveGroupRecord(group).catch((error) =>
+        console.warn('[IM groupStore] 鏈湴缇ゅ啓鍏ュけ璐�', error)
+      )
+    },
+
+    /** 浠� IndexedDB 鎭㈠鎸囧畾缇ゆ垚鍛� */
+    async loadGroupMemberList(groupId: number): Promise<GroupMember[] | null> {
+      // in-memory 宸�"瀹屾暣"鍔犺浇锛坒etchGroupMemberList 璺戣繃鎴栦笂娆″喎鍚姩浠� IDB 鏁存《鎭㈠杩囷級锛氱洿鎺ュ鐢紱
+      // 鍗曟垚鍛樿ˉ榻愶紙fetchGroupMember锛夊啓杩涚殑 partial members 涓嶅湪姝よ繑鍥炵紦瀛樷�斺�斿叾 membersLoaded=false
+      const cachedGroup = this.getGroup(groupId)
+      if (cachedGroup?.members && cachedGroup.membersLoaded) {
+        return cachedGroup.members
+      }
+      try {
+        const cached = await getDb().getAllByIndex<GroupMember>(
+          'groupMembers',
+          'groupId',
+          groupId
+        )
+        if (!cached || cached.length === 0) {
+          return null
+        }
+        // 鎶� IDB 鎷垮埌鐨勬垚鍛樿惤鍒板搴� group
+        const group = this.getGroup(groupId)
+        if (group) {
+          group.members = cached
+          group.memberCount = cached.length
+          group.membersLoaded = true
+          group.membersExpired = this.groupMembersExpired
+        } else {
+          // group 杩樻病灏变綅锛氫粎 in-memory 鍗犱綅锛坣ame='' 琛ㄧず鏈煡锛夛紝涓嶈皟 upsertGroup 鈥斺�� 閬垮厤鎶婂亣鍚嶇亴杩� conversation.name + groups IDB 妗讹紱
+          // 鍚庣画锛岀瓑 fetchGroupList 娴呭悎骞舵椂锛岃鐪熷悕瑕嗙洊
+          this.groups.push({
+            id: groupId,
+            name: '',
+            members: cached,
+            memberCount: cached.length,
+            membersLoaded: true,
+            membersExpired: this.groupMembersExpired
+          })
+        }
+        return cached
+      } catch (error) {
+        console.warn('[IM groupStore] 鏈湴缇ゆ垚鍛樼紦瀛樿鍙栧け璐�', { groupId }, error)
+        return null
+      }
+    },
+
+    /** 淇濆瓨鎸囧畾缇ゆ垚鍛� */
+    saveGroupMemberList(groupId: number): void {
+      const members = this.getGroup(groupId)?.members
+      if (!members) {
+        return
+      }
+      void getDb()
+        .transaction(['groupMembers'], 'readwrite', async (tx) => {
+          const db = getDb()
+          await db.deleteByIndex('groupMembers', 'groupId', groupId, tx)
+          for (const member of members) {
+            if (member.id) {
+              await db.put('groupMembers', member, tx)
+            }
+          }
+        })
+        .catch((error) =>
+          console.warn(`[IM groupStore] 鏈湴缇ゆ垚鍛樼紦瀛樺啓鍏ュけ璐� (groupId=${groupId})`, error)
+        )
+    },
+
+    // ==================== 杩滅鎷夊彇 ====================
+
+    /** 鎷夊彇缇ゅ垪琛紱鍚屾鍒锋柊瀵瑰簲缇よ亰浼氳瘽鐨勫睍绀哄悕 / 澶村儚 + 钀� IDB */
+    async fetchGroupList(force = false) {
+      if (this.loaded && !force) {
+        return
+      }
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      // 鎷夊彇褰撳墠鐧诲綍鐢ㄦ埛鍔犲叆鐨勬墍鏈夌兢锛堜笉甯︽垚鍛橈紱鎴愬憳鎸夐渶鍐嶈蛋 fetchGroupMemberList锛�
+      const list = await apiGetMyGroupList()
+      if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+        return
+      }
+      const fresh = (list || []).map((group) => convertGroup(group))
+      // 鍚堝苟鑰岄潪鍏ㄩ噺鏇挎崲锛氭垚鍛樼紦瀛樺彧鍦ㄦ垚鍛樺垪琛ㄦ帴鍙g淮鎶わ紝缇や釜浜鸿缃互缇ゅ垪琛ㄦ帴鍙d负鍑�
+      const groupMap = new Map(this.groups.map((group) => [group.id, group]))
+      this.groups = fresh.map((group) => {
+        const existing = groupMap.get(group.id)
+        if (!existing) {
+          return { ...group, activeCallExpired: true, infoLoaded: true }
+        }
+        return {
+          ...group,
+          infoLoaded: true,
+          activeCallExpired: existing.activeCallExpired,
+          activeCallLoaded: existing.activeCallLoaded,
+          members: existing.members,
+          memberCount: existing.memberCount ?? group.memberCount,
+          membersLoaded: existing.membersLoaded,
+          membersExpired: existing.membersExpired
+        }
+      })
+      this.loaded = true
+      const conversationStore = useConversationStore()
+      for (const group of this.groups) {
+        conversationStore.updateConversation(ImConversationType.GROUP, group.id, {
+          name: getGroupDisplayName(group),
+          avatar: group.avatar,
+          silent: group.silent
+        })
+      }
+      this.saveGroupList()
+      this.preloadMembersForEmptyAvatarGroups()
+    },
+
+    /** 澶辨晥鍏ㄩ儴缇よ鎯呯紦瀛� */
+    markAllGroupInfoExpired() {
+      for (const group of this.groups) {
+        group.infoLoaded = false
+      }
+    },
+
+    /** 棰勫姞杞界┖缇ゅご鍍忕殑鎴愬憳鍒楄〃锛屼緵 GroupAvatar 寮傛鍚堟垚缇ゅご鍍� */
+    preloadMembersForEmptyAvatarGroups() {
+      for (const group of this.groups) {
+        if (
+          group.avatar ||
+          group.joinStatus === CommonStatusEnum.DISABLE ||
+          (group.membersLoaded && !group.membersExpired && group.members?.length)
+        ) {
+          continue
+        }
+        const force = !!group.membersLoaded && !group.membersExpired && !group.members?.length
+        this.fetchGroupMemberList(group.id, force).catch((error) => {
+          console.warn('[IM groupStore] 棰勫姞杞界兢澶村儚鎴愬憳澶辫触', { groupId: group.id }, error)
+        })
+      }
+    },
+
+    /** 澶辨晥鍏ㄩ儴缇ゆ垚鍛樼紦瀛� */
+    markAllGroupMembersExpired() {
+      this.groupMembersExpired = true
+      for (const group of this.groups) {
+        if (group.membersLoaded) {
+          group.membersExpired = true
+        }
+      }
+    },
+
+    /** 澶辨晥鍏ㄩ儴缇ら�氳瘽鎺㈡祴缂撳瓨 */
+    markAllGroupActiveCallsExpired() {
+      for (const group of this.groups) {
+        group.activeCallExpired = true
+      }
+    },
+
+    /** 鏍囪缇ら�氳瘽鎺㈡祴宸插姞杞� */
+    markGroupActiveCallLoaded(groupId: number) {
+      const group = this.getGroup(groupId)
+      if (!group) {
+        return
+      }
+      group.activeCallLoaded = true
+      group.activeCallExpired = false
+    },
+
+    /** 鍒ゆ柇缇ら�氳瘽鏄惁闇�瑕侀噸鏂版帰娴� */
+    isGroupActiveCallExpired(groupId: number): boolean {
+      const group = this.getGroup(groupId)
+      return !group?.activeCallLoaded || !!group.activeCallExpired
+    },
+
+    /** 澶辨晥鎸囧畾缇ゆ垚鍛樼紦瀛� */
+    markGroupMembersExpired(groupId: number) {
+      const group = this.getGroup(groupId)
+      if (group?.membersLoaded) {
+        group.membersExpired = true
+      }
+    },
+
+    /** 鍗曠兢鍒锋柊锛氱敤 /im/group/get 鎷変竴浠芥渶鏂板厓鏁版嵁鍐� upsert锛屽父鐢ㄤ簬 GROUP_UPDATE 鎺ㄩ�佸悗鎴栨墜鍔� reload */
+    async fetchGroupInfo(groupId: number, force = false) {
+      const cached = this.getGroup(groupId)
+      if (cached?.infoLoaded && !force) {
+        return
+      }
+      try {
+        const data = await apiGetGroup(groupId)
+        if (!data) {
+          return
+        }
+        this.upsertGroup({ ...convertGroup(data), infoLoaded: true })
+      } catch (error) {
+        console.warn('[IM groupStore] fetchGroupInfo 澶辫触', error)
+      }
+    },
+
+    /** 鎸夌兢鎷夊彇鎴愬憳锛坕n-memory 缂撳瓨 + 骞跺彂鍘婚噸锛宖orce=true 寮哄埛锛�+ 钀� IDB */
+    fetchGroupMemberList(groupId: number, force = false): Promise<GroupMember[]> {
+      // in-memory "瀹屾暣"鍔犺浇杩囨墠鍛戒腑鈥斺�斿崟鎴愬憳琛ラ綈鍐欏叆鐨� partial members 涓嶅湪姝よ繑鍥烇紙membersLoaded=false锛�
+      const cached = this.getGroup(groupId)
+      if (cached && cached.members && cached.membersLoaded && !cached.membersExpired && !force) {
+        return Promise.resolve(cached.members)
+      }
+      // 鏈櫥褰曪細涓嶅彂璧疯姹備篃涓嶇櫥璁� in-flight锛岄伩鍏嶆薄鏌撳崟椋炶〃
+      const requestUserId = getCurrentUserId()
+      if (!requestUserId) {
+        return Promise.resolve([])
+      }
+      const requestEpoch = storeEpoch
+      // 鍚� (userId, groupId) 宸茬粡鏈夋鍦ㄩ鐨勮姹傦細鐩存帴澶嶇敤锛岄伩鍏嶉噸澶嶆墦鎺ュ彛
+      const key = pendingMemberKey(requestUserId, groupId)
+      const inflight = pendingMemberFetches.get(key)
+      if (inflight) {
+        return inflight
+      }
+      const promise = (async () => {
+        // 鎷夋帴鍙� + 鍗� pass 杞崲锛氬悓鏃舵崟鑾� me 鐨勫師濮� VO锛岀粰涓嬮潰鍥炲~ user-per-group 瀛楁锛坰ilent / groupRemark锛夌敤
+        const list = await apiGetGroupMemberList(groupId)
+        if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+          return []
+        }
+        let meRaw: ImGroupMemberApi.GroupMemberRespVO | undefined
+        const members = (list || []).map((member) => {
+          if (member.userId === requestUserId) {
+            meRaw = member
+          }
+          return convertGroupMember(member, groupId)
+        })
+        const silent = !!meRaw?.silent
+        const groupRemark = meRaw?.groupRemark || ''
+
+        // 蹇呴』 await 涔嬪悗閲嶆柊 getGroup锛岄伩鍏� fetchGroupList 宸插苟鍙戝啓鍏ョ湡瀹� group 鐨� race
+        const group = this.getGroup(groupId)
+        const isPlaceholder = !group
+        let groupFieldsChanged = false
+        if (group) {
+          group.members = members
+          group.memberCount = members.length
+          group.membersLoaded = true
+          group.membersExpired = false
+          // silent / groupRemark 浠讳竴鍙樺寲鎵嶅悓姝ュ埌 conversation 鍜� IDB锛沢roupRemark 鍙樺寲瑕侀『甯﹀埛浼氳瘽鍚�
+          if (group.silent !== silent || group.groupRemark !== groupRemark) {
+            group.silent = silent
+            group.groupRemark = groupRemark
+            groupFieldsChanged = true
+            const conversationStore = useConversationStore()
+            conversationStore.updateConversation(ImConversationType.GROUP, groupId, {
+              name: getGroupDisplayName(group),
+              silent
+            })
+          }
+        } else {
+          // group 杩樻病灏变綅锛氫粎 in-memory push 鍗犱綅锛坣ame='' 琛ㄧず鏈煡锛夛紝涓嶈皟 upsertGroup鈥斺�旈伩鍏嶆妸鍋囧悕鐏岃繘 conversation.name + groups IDB 妗讹紱
+          // 鍚庣画锛岀瓑 fetchGroupList 娴呭悎骞舵椂锛岃鐪熷悕瑕嗙洊
+          this.groups.push({
+            id: groupId,
+            name: '',
+            members,
+            memberCount: members.length,
+            silent,
+            groupRemark,
+            membersLoaded: true,
+            membersExpired: false
+          })
+        }
+
+        // groups 妗朵粎鍦� user-per-group 瀛楁瀹為檯鍙樺寲鏃跺啓鈥斺�旈伩鍏嶄竴娆℃壒閲忚繘缇ゅ紩鍙戝娆℃暣妗堕噸鍐�
+        this.saveGroupMemberList(groupId)
+        if (!isPlaceholder && groupFieldsChanged) {
+          this.saveGroup(group)
+        }
+        return members
+        // 鏃犺鎴愬姛 / 澶辫触閮借浠庡崟椋炶〃娓呮帀锛屽惁鍒欏悗缁悓 group 璇锋眰姘歌繙鎷垮埌杩欎釜 stale Promise
+      })().finally(() => pendingMemberFetches.delete(key))
+
+      // 鎶� Promise 鐧昏杩涘崟椋炶〃锛岃姝ゅ悗鐭椂闂村唴鐨勫悓 (userId, groupId) 璇锋眰澶嶇敤
+      pendingMemberFetches.set(key, promise)
+      return promise
+    },
+
+    /**
+     * 鎸� (groupId, memberUserId) 鍗曟垚鍛樿ˉ榻愨�斺�攄eriveLastSenderDisplayName 鍏滃簳鍦烘櫙鐢�
+     *
+     * 璺� fetchGroupMemberList 鍖哄埆锛氬彧鎷夎繖涓�涓垚鍛橈紝涓嶅姩 me 鐨� silent / groupRemark锛堜笉鏄� me 鐨勮瘽鎷夸笉鍒帮級锛�
+     * 鍛戒腑鏃舵妸鎴愬憳 upsert 杩� group.members 鏁扮粍骞惰惤 IDB锛岃鍚庣画娓叉煋鑳界敤 displayUserName
+     */
+    fetchGroupMember(groupId: number, memberUserId: number): Promise<GroupMember | null> {
+      // in-memory 鍛戒腑鐩存帴杩斿洖锛屼笉鎵撴帴鍙�
+      const cached = this.getGroup(groupId)?.members?.find((m) => m.userId === memberUserId)
+      if (cached) {
+        return Promise.resolve(cached)
+      }
+      // 鏈櫥褰曪細涓嶅彂璧疯姹備篃涓嶇櫥璁� in-flight锛岄伩鍏嶆薄鏌撳崟椋炶〃
+      const requestUserId = getCurrentUserId()
+      if (!requestUserId) {
+        return Promise.resolve(null)
+      }
+      const requestEpoch = storeEpoch
+      // 鍚� (userId, groupId, memberUserId) 宸茬粡鏈夋鍦ㄩ鐨勮姹傦細鐩存帴澶嶇敤
+      const key = pendingSingleMemberKey(requestUserId, groupId, memberUserId)
+      const inflight = pendingSingleMemberFetches.get(key)
+      if (inflight) {
+        return inflight
+      }
+      const promise = (async () => {
+        const data = await apiGetGroupMember(groupId, memberUserId)
+        if (!data) {
+          return null
+        }
+        if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+          return null
+        }
+        const member = convertGroupMember(data, groupId)
+        // 鎶婅繖涓�鏉� upsert 杩� group.members 浠呬緵 in-memory 娓叉煋鍏滃簳锛沢roup 杩樻病灏变綅鍒欑敤 placeholder
+        // 娉ㄦ剰锛氫笉鍐� IDB鈥斺�旀垚鍛樻《璇箟鏄�"鍏ㄩ噺"锛屽瓨"1 浜烘《"浼氭薄鏌撲笅娆″喎鍚姩鐨� loadGroupMemberList
+        const group = this.getGroup(groupId)
+        if (group) {
+          const memberList = group.members ?? []
+          const index = memberList.findIndex((m) => m.userId === memberUserId)
+          if (index === -1) {
+            memberList.push(member)
+          } else {
+            memberList[index] = member
+          }
+          group.members = memberList
+        } else {
+          // memberCount 涓嶈锛氬悗缁� fetchGroupList 鍚堝苟 `existing.memberCount ?? fresh.memberCount` 鏃讹紝
+          // 鍗犱綅鍊间細椤舵浛鐪熷疄鍊硷紙fresh 涓嶅甫 memberCount锛夛紝绛� fetchGroupMemberList 璺戣繃鎵嶈兘鎷垮埌鐪熷疄鏁�
+          this.groups.push({
+            id: groupId,
+            name: '',
+            members: [member]
+          })
+        }
+        return member
+      })().finally(() => pendingSingleMemberFetches.delete(key))
+      pendingSingleMemberFetches.set(key, promise)
+      return promise
+    },
+
+    /** 鎸� id 鎻掑叆鎴栧悎骞剁兢锛堝懡涓垯娴呭悎骞朵繚鐣欐棫瀛楁锛屾湭鍛戒腑鍒欒拷鍔狅級锛屽悓姝ユ妸灞曠ず鍚� / 澶村儚 / 鍏嶆墦鎵版帹鍒板搴斾細璇� */
+    upsertGroup(group: Group) {
+      void this.upsertGroupAndSave(group).catch((error) =>
+        console.warn('[IM groupStore] 鏈湴缇ゅ啓鍏ュけ璐�', error)
+      )
+    },
+
+    /** 鎸� id 鎻掑叆鎴栧悎骞剁兢 */
+    async upsertGroupAndSave(group: Group): Promise<void> {
+      const index = this.groups.findIndex((g) => g.id === group.id)
+      if (index === -1) {
+        this.groups.push(group)
+      } else {
+        this.groups[index] = { ...this.groups[index], ...group }
+      }
+      // 鍚屾鎺ㄥ埌 conversation锛氱兢鍚� / 澶村儚 / 鍏嶆墦鎵版槸浼氳瘽鍒楄〃灞曠ず鐢ㄧ殑锛屽繀椤荤揣闅� group 鍙樻洿
+      const merged = this.getGroup(group.id) ?? group
+      const conversationStore = useConversationStore()
+      conversationStore.updateConversation(ImConversationType.GROUP, group.id, {
+        name: getGroupDisplayName(merged),
+        avatar: merged.avatar,
+        silent: merged.silent
+      })
+      // 鎸佷箙鍖栧埌 IDB锛坒ire-and-forget锛�
+      await this.saveGroupRecord(merged)
+    },
+
+    /** 鏈湴绉婚櫎缇ょ紦瀛樺拰缇や細璇濓紱缇よВ鏁o紙GROUP_DEL锛夈�侀��缇ゃ�佽韪㈤兘澶嶇敤 */
+    removeGroup(id: number) {
+      // 鏈湴纭垹锛堝尯鍒簬濂藉弸鍒犻櫎鐨勮蒋鍒犱繚鐣欒褰曪級锛涚骇鑱旀竻缇よ亰浼氳瘽閬垮厤鍒楄〃閲岀暀姝荤兢
+      this.groups = this.groups.filter((g) => g.id !== id)
+      const conversationStore = useConversationStore()
+      conversationStore.removeGroupConversation(id)
+      void getDb()
+        .transaction(['groups', 'groupMembers'], 'readwrite', async (tx) => {
+          const db = getDb()
+          await db.delete('groups', id, tx)
+          await db.deleteByIndex('groupMembers', 'groupId', id, tx)
+        })
+        .catch((error) => console.warn(`[IM groupStore] 缇ょ紦瀛樺垹闄ゅけ璐� (groupId=${id})`, error))
+    },
+
+    /** 鍒囨崲鍏嶆墦鎵帮細鎺ㄥ悗绔� + 钀芥湰鍦� + 鍚屾浼氳瘽鍒楄〃鐨� silent锛岄伩鍏� silent 鍥炬爣 / 鎬绘湭璇� / 鎻愮ず闊冲垽鏂笌璁剧疆婕傜Щ锛涘拰 friendStore.setFriendSilent 瀵归綈 */
+    async setGroupSilent(id: number, silent: boolean) {
+      await apiUpdateGroupMember({ groupId: id, silent })
+      const group = this.getGroup(id)
+      if (!group) {
+        return
+      }
+      group.silent = silent
+      const conversationStore = useConversationStore()
+      conversationStore.updateConversation(ImConversationType.GROUP, id, { silent })
+      this.saveGroup(group)
+    },
+
+    /** 鎵归噺鏇存柊缇ゆ垚鍛樿鑹诧紱鏈湴涓嶅懡涓垯蹇界暐锛岀瓑 fetchGroupMemberList 鍏滃簳 */
+    updateGroupMemberRoleList(groupId: number, userIds: number[], role: number) {
+      const group = this.getGroup(groupId)
+      if (!group?.members?.length) {
+        return
+      }
+      // 鍛戒腑鐩爣涓旇鑹插凡鍙樺寲鎵嶆爣璁� changed锛岄伩鍏嶆棤鍙樺寲鏃舵暣鏁扮粍閲嶅缓瑙﹀彂鍝嶅簲寮�
+      const idSet = new Set(userIds)
+      let changed = false
+      const newMembers = group.members.map((member) => {
+        if (!idSet.has(member.userId) || member.role === role) {
+          return member
+        }
+        changed = true
+        return { ...member, role }
+      })
+      // 鏈夊彉鍖栨墠鏁寸粍鏇挎崲锛岃鍝嶅簲寮忓彧鍦ㄧ湡鏈夋洿鏂版椂閫氱煡涓嬫父
+      if (changed) {
+        group.members = newMembers
+        this.saveGroupMemberList(groupId)
+      }
+    },
+
+    /** 缇や富杞锛氱兢琛� ownerUserId 鏀逛负鏂板�硷紱鏃х兢涓� role 鈫� NORMAL锛涙柊缇や富 role 鈫� OWNER */
+    transferGroupOwner(groupId: number, oldOwnerId: number, newOwnerId: number) {
+      const group = this.getGroup(groupId)
+      if (!group) {
+        return
+      }
+      if (group.ownerUserId !== newOwnerId) {
+        group.ownerUserId = newOwnerId
+      }
+      this.updateGroupMemberRoleList(groupId, [oldOwnerId], ImGroupMemberRole.NORMAL)
+      this.updateGroupMemberRoleList(groupId, [newOwnerId], ImGroupMemberRole.OWNER)
+      this.saveGroup(group)
+    },
+
+    /** 鏈湴鍓旈櫎缇ゆ垚鍛橈紙GROUP_MEMBER_QUIT / KICK 浜嬩欢锛夛紱涓嶅懡涓垯绛� fetchGroupMemberList 鍏滃簳 */
+    removeLocalGroupMemberList(groupId: number, userIds: number[]) {
+      const group = this.getGroup(groupId)
+      if (!group?.members?.length || userIds.length === 0) {
+        return
+      }
+      const idSet = new Set(userIds)
+      const next = group.members.filter((member) => !idSet.has(member.userId))
+      if (next.length === group.members.length) {
+        return
+      }
+      group.members = next
+      group.memberCount = next.length
+      this.saveGroupMemberList(groupId)
+    },
+
+    /** 鏈湴鏇存柊缇ゆ垚鍛樼殑 status锛堣嚜宸遍��缇� / 琚涪鐨勬湰鍦伴缃紱璁� isMember 绔嬪嵆鏀舵暃鍒� stranger锛屼笉渚濊禆 removeGroup 鐨勬暣缇ょЩ闄わ級 */
+    updateGroupMemberStatus(groupId: number, userId: number, status: number) {
+      const group = this.getGroup(groupId)
+      const member = group?.members?.find((m) => m.userId === userId)
+      if (!member || member.status === status) {
+        return
+      }
+      member.status = status
+      this.saveGroupMemberList(groupId)
+    },
+
+    /** 鏈湴鏇存柊缇ゆ垚鍛樼殑 displayUserName锛圙ROUP_MEMBER_NICKNAME_UPDATE 浜嬩欢锛夛紱涓嶅懡涓垯绛� fetchGroupMemberList 鍏滃簳 */
+    updateGroupMemberDisplayUserName(groupId: number, userId: number, displayUserName: string) {
+      const group = this.getGroup(groupId)
+      const member = group?.members?.find((m) => m.userId === userId)
+      if (!member || member.displayUserName === displayUserName) {
+        return
+      }
+      member.displayUserName = displayUserName
+      this.saveGroupMemberList(groupId)
+    },
+
+    /** 灞�閮ㄦ洿鏂扮兢瀛楁锛坣ame / notice / avatar 绛夛級锛涙湭鍛戒腑鏈湴缂撳瓨鏃堕潤榛樺拷鐣ワ紝绛� fetchGroupList 鍏滃簳锛涙柊鍊艰窡鏃у�奸兘鐩稿悓鏃惰烦杩囧搷搴斿紡 + IDB 鍐� */
+    updateGroupFields(groupId: number, fields: Partial<Group>) {
+      const group = this.getGroup(groupId)
+      if (!group) {
+        return
+      }
+      const changed = (Object.keys(fields) as (keyof Group)[]).some((k) => group[k] !== fields[k])
+      if (!changed) {
+        return
+      }
+      Object.assign(group, fields)
+      const conversationStore = useConversationStore()
+      conversationStore.updateConversation(ImConversationType.GROUP, groupId, {
+        name: getGroupDisplayName(group),
+        avatar: group.avatar,
+        silent: group.silent
+      })
+      this.saveGroup(group)
+    },
+
+    /**
+     * 鎺ユ敹 GROUP_* 缇ゅ箍鎾簨浠讹紝鎸� type 鍒嗗彂鍒板搴旂鏈� action
+     *
+     * WebSocket 瀹炴椂鏀惰蛋 messageStore.insertMessage 鏃佽矾璋冪敤
+     * store 閲屾病缂撳瓨鐨勭兢闈欓粯蹇界暐锛岀瓑 fetchGroupList 鍏滃簳
+     */
+    applyGroupNotification(groupId: number, type: number, content?: string) {
+      if (!groupId) {
+        return
+      }
+      let payload: GroupNotificationPayload
+      try {
+        payload = content ? JSON.parse(content) : {}
+      } catch (error) {
+        console.warn(
+          '[IM groupStore] applyGroupNotification 瑙f瀽 content 澶辫触',
+          { groupId, type, contentLength: content?.length ?? 0 },
+          error
+        )
+        return
+      }
+      switch (type) {
+        case ImContentType.GROUP_ADMIN_ADD: {
+          this.updateGroupMemberRoleList(
+            groupId,
+            payload.memberUserIds || [],
+            ImGroupMemberRole.ADMIN
+          )
+          this.markGroupMembersExpired(groupId)
+          // 鑷繁琚姞涓虹鐞嗗憳锛屽師鏈湅涓嶅埌鐨勭兢涓嬫湭澶勭悊鐢宠鐜板湪鍙樺彲瑙侊紝閲嶆柊鎷変竴娆� unhandledList
+          if (isSelfInPayloadMembers(payload)) {
+            refreshUnhandledGroupRequests()
+          }
+          break
+        }
+        case ImContentType.GROUP_ADMIN_REMOVE: {
+          this.updateGroupMemberRoleList(
+            groupId,
+            payload.memberUserIds || [],
+            ImGroupMemberRole.NORMAL
+          )
+          this.markGroupMembersExpired(groupId)
+          if (isSelfInPayloadMembers(payload)) {
+            refreshUnhandledGroupRequests()
+          }
+          break
+        }
+        case ImContentType.GROUP_BANNED: {
+          this.updateGroupFields(groupId, { banned: !!payload.banned })
+          break
+        }
+        case ImContentType.GROUP_CANCEL_MUTED: {
+          this.updateGroupFields(groupId, { mutedAll: false })
+          break
+        }
+        case ImContentType.GROUP_CREATE: {
+          this.applyGroupCreateNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_DISSOLVE: {
+          this.removeGroup(groupId)
+          break
+        }
+        case ImContentType.GROUP_INFO_UPDATE: {
+          this.applyGroupInfoUpdateNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MEMBER_CANCEL_MUTED: {
+          this.applyGroupMemberCancelMutedNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MEMBER_ENTER: {
+          this.applyGroupMemberEnterNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MEMBER_INVITE: {
+          this.applyGroupMemberInviteNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MEMBER_KICK: {
+          this.applyGroupMemberKickNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MEMBER_MUTED: {
+          this.applyGroupMemberMutedNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MEMBER_NICKNAME_UPDATE: {
+          this.applyGroupMemberNicknameUpdateNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MEMBER_QUIT: {
+          this.applyGroupMemberQuitNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MESSAGE_PIN: {
+          this.applyGroupMessagePinNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MESSAGE_UNPIN: {
+          this.applyGroupMessageUnpinNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_MUTED: {
+          this.updateGroupFields(groupId, { mutedAll: true })
+          break
+        }
+        case ImContentType.GROUP_NAME_UPDATE: {
+          this.applyGroupNameUpdateNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_NOTICE_UPDATE: {
+          this.applyGroupNoticeUpdateNotification(groupId, payload)
+          break
+        }
+        case ImContentType.GROUP_OWNER_TRANSFER: {
+          this.applyGroupOwnerTransferNotification(groupId, payload)
+          break
+        }
+      }
+    },
+
+    /** 鍒涘缓缇ゅ箍鎾細缇ゆ湭灏变綅鏃舵媺缇よ鎯� */
+    async applyGroupCreateNotification(groupId: number, payload: GroupNotificationPayload) {
+      if (!isSelfInPayloadMembers(payload)) {
+        return
+      }
+      const selfUserId = getCurrentUserId()
+      const selfIsOperator = !!selfUserId && payload.operatorUserId === selfUserId
+      if (selfIsOperator && this.getGroup(groupId)) {
+        return
+      }
+      await this.fetchGroupInfo(groupId, true)
+    },
+
+    /** 缇ゅ悕鍙樻洿锛氭寜 newName 灞�閮ㄦ洿鏂版湰鍦扮兢鍚� */
+    applyGroupNameUpdateNotification(groupId: number, payload: GroupNotificationPayload) {
+      if (payload.newName) {
+        this.updateGroupFields(groupId, { name: payload.newName })
+      }
+    },
+
+    /** 缇ゅ叕鍛婂彉鏇达細鎸� newNotice 灞�閮ㄦ洿鏂帮紙鍏佽绌轰覆浣滀负銆屾竻绌哄叕鍛娿�嶏級 */
+    applyGroupNoticeUpdateNotification(groupId: number, payload: GroupNotificationPayload) {
+      this.updateGroupFields(groupId, { notice: payload.newNotice ?? '' })
+    },
+
+    /** 缇や俊鎭彉鏇达細鍚屾澶村儚銆佽繘缇ゅ鎵� */
+    applyGroupInfoUpdateNotification(groupId: number, payload: GroupNotificationPayload) {
+      const fields: Partial<Group> = {}
+      if (payload.newAvatar) {
+        fields.avatar = payload.newAvatar
+      }
+      if (payload.newJoinApproval != null) {
+        fields.joinApproval = payload.newJoinApproval
+      }
+      if (Object.keys(fields).length > 0) {
+        this.updateGroupFields(groupId, fields)
+      }
+    },
+
+    /** 鎴愬憳鍔犲叆锛氳閭�璇疯�呮湰绔� group 鏈氨浣嶅厛 fetchGroupInfo 鍒濇鎷夊彇锛涙墍鏈変汉閮藉埛鎴愬憳鍒楄〃锛堟柊鎴愬憳 nickname / avatar 涓嶅湪 payload锛� */
+    async applyGroupMemberInviteNotification(groupId: number, payload: GroupNotificationPayload) {
+      // 鑷繁鍒氳鎷夎繘鏉ワ細蹇呴』 await fetchGroupInfo 璁╃兢鍏� state.groups锛屽惁鍒� fetchGroupMemberList 鐨� guard 浼氬厹绌�
+      if (isSelfInPayloadMembers(payload) && !this.getGroup(groupId)) {
+        await this.fetchGroupInfo(groupId, true)
+      }
+      this.markGroupMembersExpired(groupId)
+      this.fetchGroupMemberList(groupId, true).catch(() => undefined)
+    },
+
+    /** 鑷敱杩涚兢锛氳繘缇よ�呮湰绔� group 鏈氨浣嶅厛 fetchGroupInfo 鍒濇鎷夊彇锛涙墍鏈変汉閮藉埛鎴愬憳鍒楄〃 */
+    async applyGroupMemberEnterNotification(groupId: number, payload: GroupNotificationPayload) {
+      const selfUserId = getCurrentUserId()
+      // 鑷繁鑷敱杩涚兢锛氬繀椤� await fetchGroupInfo 璁╃兢鍏� state.groups锛屽惁鍒� fetchGroupMemberList 鐨� guard 浼氬厹绌�
+      if (selfUserId && payload.entrantUserId === selfUserId && !this.getGroup(groupId)) {
+        await this.fetchGroupInfo(groupId, true)
+      }
+      this.markGroupMembersExpired(groupId)
+      this.fetchGroupMemberList(groupId, true).catch(() => undefined)
+    },
+
+    /** 鎴愬憳閫�缇わ細閫�缇よ�呮湰浜哄厛鎶� self.status 缃� DISABLE 鍐� removeGroup锛堜繚鐣欑姸鎬佽涔� + 缁存寔 groups 鍒楄〃骞插噣锛夛紱鍏朵粬鎴愬憳浠庢湰鍦板垪琛ㄧЩ闄� quitter */
+    applyGroupMemberQuitNotification(groupId: number, payload: GroupNotificationPayload) {
+      const selfUserId = getCurrentUserId()
+      if (selfUserId && payload.operatorUserId === selfUserId) {
+        this.updateGroupMemberStatus(groupId, selfUserId, CommonStatusEnum.DISABLE)
+        this.removeGroup(groupId)
+      } else if (payload.operatorUserId) {
+        this.removeLocalGroupMemberList(groupId, [payload.operatorUserId])
+        this.markGroupMembersExpired(groupId)
+      }
+    },
+
+    /** 鎴愬憳琚Щ鍑猴細琚涪鑰呮湰浜哄厛鎶� self.status 缃� DISABLE 鍐� removeGroup锛涘叾浠栨垚鍛樹粠鏈湴鍒楄〃绉婚櫎琚涪鑰� */
+    applyGroupMemberKickNotification(groupId: number, payload: GroupNotificationPayload) {
+      const memberIds = payload.memberUserIds || []
+      const selfUserId = getCurrentUserId()
+      if (isSelfInPayloadMembers(payload)) {
+        if (selfUserId) {
+          this.updateGroupMemberStatus(groupId, selfUserId, CommonStatusEnum.DISABLE)
+        }
+        this.removeGroup(groupId)
+      } else if (memberIds.length > 0) {
+        this.removeLocalGroupMemberList(groupId, memberIds)
+        this.markGroupMembersExpired(groupId)
+      }
+    },
+
+    /** 鎴愬憳鏄电О鍙樻洿锛氭寜 operatorUserId 灞�閮ㄦ洿鏂板搴� member.displayUserName */
+    applyGroupMemberNicknameUpdateNotification(groupId: number, payload: GroupNotificationPayload) {
+      if (payload.operatorUserId) {
+        this.updateGroupMemberDisplayUserName(
+          groupId,
+          payload.operatorUserId,
+          payload.displayUserName ?? ''
+        )
+        this.markGroupMembersExpired(groupId)
+      }
+    },
+
+    /** 缇や富杞锛氭棫缇や富 鈫� NORMAL锛屾柊缇や富 鈫� OWNER锛涙柊缇や富鑷繁渚ч噸鏂版媺鐢宠鍒楄〃 */
+    applyGroupOwnerTransferNotification(groupId: number, payload: GroupNotificationPayload) {
+      if (payload.operatorUserId && payload.newOwnerUserId) {
+        this.transferGroupOwner(groupId, payload.operatorUserId, payload.newOwnerUserId)
+        this.markGroupMembersExpired(groupId)
+      }
+      // 鑷繁鎺ョ缇や富锛氬師鏈湅涓嶅埌鐨勭兢涓嬫湭澶勭悊鐢宠鐜板湪鍙樺彲瑙侊紝閲嶆柊鎷変竴娆� unhandledList
+      const selfUserId = getCurrentUserId()
+      if (selfUserId && payload.newOwnerUserId === selfUserId) {
+        refreshUnhandledGroupRequests()
+      } else if (selfUserId && payload.operatorUserId === selfUserId) {
+        refreshUnhandledGroupRequests()
+      }
+    },
+
+    /** 缇ゆ秷鎭疆椤讹細浠� payload 鍙栨秷鎭睍绀烘暟鎹姞鍏ョ疆椤跺垪琛� */
+    applyGroupMessagePinNotification(groupId: number, payload: GroupNotificationPayload) {
+      const message = payload.message
+      if (!message) {
+        return
+      }
+      const group = this.getGroup(groupId)
+      if (!group) {
+        return
+      }
+      // 骞傜瓑锛氬凡瀛樺湪鍚� messageId 涓嶉噸澶� push
+      const existing = group.pinnedMessages || []
+      if (existing.some((msg) => msg.id === message.id)) {
+        return
+      }
+      group.pinnedMessages = [
+        ...existing,
+        {
+          id: message.id,
+          clientMessageId: '',
+          senderId: message.senderId,
+          type: message.type,
+          content: message.content,
+          status: ImMessageStatus.NORMAL,
+          sendTime: new Date(message.sendTime).getTime(),
+          targetId: message.groupId || groupId,
+          selfSend: message.senderId === getCurrentUserId(),
+          atUserIds: message.atUserIds ? [...message.atUserIds] : [],
+          receiverUserIds: message.receiverUserIds ? [...message.receiverUserIds] : []
+        }
+      ]
+      this.saveGroup(group)
+    },
+
+    /** 缇ゆ秷鎭彇娑堢疆椤讹細鎸� messageId 浠庢湰鍦扮疆椤跺垪琛ㄤ腑绉婚櫎 */
+    applyGroupMessageUnpinNotification(groupId: number, payload: GroupNotificationPayload) {
+      if (!payload.messageId) {
+        return
+      }
+      const group = this.getGroup(groupId)
+      if (!group?.pinnedMessages?.length) {
+        return
+      }
+      const newPinnedMessages = group.pinnedMessages.filter((m) => m.id !== payload.messageId)
+      if (newPinnedMessages.length === group.pinnedMessages.length) {
+        return
+      }
+      group.pinnedMessages = newPinnedMessages
+      this.saveGroup(group)
+    },
+
+    /** 鍗曟垚鍛樼瑷�锛氭洿鏂扮洰鏍囨垚鍛樼殑 muteEndTime */
+    applyGroupMemberMutedNotification(groupId: number, payload: GroupNotificationPayload) {
+      const group = this.getGroup(groupId)
+      const member = group?.members?.find((m) => m.userId === payload.mutedUserId)
+      if (member && payload.muteEndTime) {
+        member.muteEndTime = payload.muteEndTime
+        this.saveGroupMemberList(groupId)
+        this.markGroupMembersExpired(groupId)
+      }
+    },
+
+    /** 鍗曟垚鍛樺彇娑堢瑷�锛氭竻绌虹洰鏍囨垚鍛樼殑 muteEndTime */
+    applyGroupMemberCancelMutedNotification(groupId: number, payload: GroupNotificationPayload) {
+      const group = this.getGroup(groupId)
+      const member = group?.members?.find((m) => m.userId === payload.mutedUserId)
+      if (member) {
+        member.muteEndTime = undefined
+        this.saveGroupMemberList(groupId)
+        this.markGroupMembersExpired(groupId)
+      }
+    },
+
+    /** 鍒囪处鍙锋椂浠呮竻 in-memory锛孖DB 鎸� userId 鍒嗘《澶╃劧闅旂锛屽洖鍒囩寮� */
+    clear() {
+      this.groups = []
+      this.loaded = false
+      this.groupMembersExpired = false
+      // 璐﹀彿鍒囨崲锛氶�掑 epoch 搴熷純鏃ц处鍙� in-flight 鐨勬垚鍛樿姹�
+      storeEpoch++
+      // 鍗曢琛ㄨ窡 in-memory state 涓�璧烽噸缃紱鏃ц处鍙� in-flight 鐨勮姹� finally 涔熶細鑷繁 delete key锛屾彁鍓嶆竻绌哄彧鏄洿骞茶剢
+      pendingMemberFetches.clear()
+      pendingSingleMemberFetches.clear()
+    }
+  }
+})
+
+function convertGroup(group: ImGroupApi.GroupRespVO): Group {
+  return {
+    id: group.id,
+    name: group.name,
+    avatar: group.avatar,
+    notice: group.notice,
+    ownerUserId: group.ownerUserId,
+    pinnedMessages: group.pinnedMessages?.map((message) => convertGroupMessageVO(message)),
+    mutedAll: group.mutedAll,
+    banned: group.banned,
+    joinApproval: group.joinApproval,
+    joinStatus: group.joinStatus,
+    groupRemark: group.groupRemark,
+    silent: group.silent
+  }
+}
+
+/** 鍚庣 ImGroupMessageApi.GroupMessageRespVO -> 鍓嶇 Message锛氳ˉ targetId / selfSend / sendTime 绛夋淳鐢熷瓧娈� */
+function convertGroupMessageVO(
+  message: NonNullable<ImGroupApi.GroupRespVO['pinnedMessages']>[number]
+): Message {
+  const currentUserId = getCurrentUserId()
+  return {
+    id: message.id,
+    clientMessageId: message.clientMessageId || '',
+    type: message.type,
+    content: message.content,
+    status: message.status,
+    sendTime: new Date(message.sendTime).getTime(),
+    senderId: message.senderId,
+    targetId: message.groupId,
+    selfSend: !!currentUserId && message.senderId === currentUserId,
+    atUserIds: message.atUserIds || [],
+    receiverUserIds: message.receiverUserIds || [],
+    receiptStatus: message.receiptStatus,
+    readCount: message.readCount
+  }
+}
+
+function convertGroupMember(member: ImGroupMemberApi.GroupMemberRespVO, groupId: number): GroupMember {
+  return {
+    id: member.id,
+    userId: member.userId,
+    groupId,
+    nickname: member.nickname || String(member.userId),
+    avatar: member.avatar,
+    displayUserName: member.displayUserName,
+    status: member.status,
+    role: member.role,
+    muteEndTime: member.muteEndTime
+  }
+}
+
+export const useGroupStoreWithOut = () => useGroupStore()
+
+// dev: 璁� Pinia 鐨� actions 鏀瑰姩鏀寔 HMR锛屽厤鍘绘瘡娆℃敼 store 閮借纭埛
+if (import.meta.hot) {
+  import.meta.hot.accept(acceptHMRUpdate(useGroupStore, import.meta.hot))
+}

--
Gitblit v1.9.3