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/friendStore.ts |  849 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 849 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/store/friendStore.ts b/src/views/im/home/store/friendStore.ts
new file mode 100644
index 0000000..fbb9ed5
--- /dev/null
+++ b/src/views/im/home/store/friendStore.ts
@@ -0,0 +1,849 @@
+import type { Friend, FriendLite, FriendRequest } from '../types'
+
+import type { ImFriendApi } from '#/api/im/friend'
+import type { ImFriendRequestApi } from '#/api/im/friend/request'
+
+import { CommonStatusEnum } from '#/packages/constants/src'
+
+import { acceptHMRUpdate, defineStore } from 'pinia'
+
+import { blockFriend as apiBlockFriend, deleteFriend as apiDeleteFriend, getFriend as apiGetFriend, getMyFriendList as apiGetMyFriendList, pullMyFriendList as apiPullMyFriendList, unblockFriend as apiUnblockFriend, updateFriend as apiUpdateFriend } from '#/api/im/friend'
+import { agreeFriendRequest as apiAgreeFriendRequest, applyFriendRequest as apiApplyFriendRequest, getMyFriendRequest as apiGetMyFriendRequest, getMyFriendRequestList as apiGetMyFriendRequestList, pullMyFriendRequestList as apiPullMyFriendRequestList, refuseFriendRequest as apiRefuseFriendRequest } from '#/api/im/friend/request'
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import { FRIEND_REQUEST_PAGE_SIZE } from '../../utils/config'
+import { ImConversationType, ImFriendRequestHandleResult } from '../../utils/constants'
+import { getDb, StorageKeys } from '../../utils/db'
+import { runIncrementalPull } from '../../utils/pull'
+import { getFriendDisplayName } from '../../utils/user'
+import { useConversationStore } from './conversationStore'
+
+type PendingRequest = { epoch: number; promise: Promise<void>; userId: number; }
+
+/** 褰撳墠姝e湪杩涜鐨勫ソ鍙嬪垪琛ㄦ媺鍙栵紱澶� dispatcher 鍚屾椂瑙﹀彂鏃跺鐢ㄥ悓涓� Promise锛岄伩鍏嶉洩宕╅噸鎷� */
+let pendingFetchFriends: null | PendingRequest = null
+/** 褰撳墠姝e湪杩涜鐨勫ソ鍙嬬敵璇峰垪琛ㄦ媺鍙栵紱澶氱杩炵画澶氭潯鐢宠鍒拌揪鏃跺鐢ㄥ悓涓� Promise锛岄伩鍏嶉洩宕╅噸鎷� */
+let pendingFetchRequests: null | PendingRequest = null
+/** 褰撳墠姝e湪杩涜鐨勩�屽姞杞芥洿澶氱敵璇枫�嶈姹� */
+let pendingLoadMoreRequests: null | PendingRequest = null
+/** 褰撳墠姝e湪杩涜鐨勫ソ鍙嬭鎯呰姹� */
+const pendingFetchFriendInfos = new Map<string, Promise<void>>()
+
+/** clear() 鏃堕�掑锛涙棫璐﹀彿閭f杩樻病杩斿洖鐨勮姹� resolve 鍚庢瘮瀵逛竴鑷存墠鍐� store锛岄槻璺ㄨ处鍙锋暟鎹硠婕� */
+let storeEpoch = 0
+
+/** 鏋勫缓濂藉弸璇︽儏璇锋眰鍘婚噸 key */
+function getPendingFriendInfoKey(userId: number, friendUserId: number): string {
+  return `${userId}:${friendUserId}`
+}
+
+/** 濂藉弸閫氱煡 payload锛堝榻愬悗绔� BaseFriendNotification + 瀛愮被瑁佸噺鍚庣殑瀛楁锛� */
+export interface FriendNotificationPayload {
+  operatorUserId: number
+  friendUserId: number
+  // FRIEND_REQUEST_* 绯诲垪锛氱敵璇疯褰曠殑鏍稿績瀛楁锛堥伩鍏� payload 鎼哄甫瀹屾暣 DO锛�
+  requestId?: number
+  applyContent?: string
+  handleContent?: string
+  addSource?: number
+  // FRIEND_REQUEST_RECEIVED锛氱敵璇锋柟鑱氬悎瀛楁锛屼緵鍓嶇鐩存帹 push 杩涘垪琛紝鏃犻渶鍥炴媺
+  fromNickname?: string
+  fromAvatar?: string
+  // FRIEND_UPDATE锛氬崟杈瑰睘鎬у彉鏇�
+  displayName?: string
+  silent?: boolean
+  pinned?: boolean
+  // FRIEND_DELETE锛氭槸鍚︾骇鑱旀竻鐞嗘湰绔浉鍏虫暟鎹紙濡傜鑱婁細璇濓級
+  clear?: boolean
+}
+
+/**
+ * IM 濂藉弸 Store
+ *
+ * 璐熻矗锛�
+ * - 鎷夊彇 / 缂撳瓨褰撳墠鐧诲綍鐢ㄦ埛鐨勫ソ鍙嬪垪琛� + 鐢宠鍒楄〃
+ * - 鐢宠-瀹℃壒娴佺▼锛坅pply / agree / refuse锛�+ 澶囨敞 / 鍏嶆墦鎵� / 鑱旂郴浜虹疆椤� / 鎷夐粦
+ * - 鎺ユ敹 WebSocket 1201-1210 娈典綅閫氱煡锛屾寜浜嬩欢鍒嗗彂鍒� friendStore 鍐呴儴鍚� dispatcher
+ */
+export const useFriendStore = defineStore('imFriendStore', {
+  state: () => ({
+    friends: [] as Friend[],
+    // 浠� fetchFriendList 鎴愬姛鍚庣疆浣嶏紱loadFriendData锛圛DB锛変笉缃綅锛屽惁鍒欏悗鍙� SWR 鍒锋柊浼氳缂撳瓨鍛戒腑璺宠繃
+    loaded: false,
+    /** 鎴戠浉鍏崇殑濂藉弸鐢宠鍒楄〃锛堝惈鎴戝彂璧风殑 + 鍒汉鍔犳垜鐨勶紱鍚庣鎸� id 鍊掑簭娓告爣鍒嗛〉锛� */
+    friendRequests: [] as FriendRequest[],
+    /** 鏄惁杩樻湁鏇存棭鐨勭敵璇疯褰曞彲鍔犺浇锛涜繑鍥炰笉婊� page size 鍗崇疆 false */
+    hasMoreFriendRequests: true
+  }),
+
+  getters: {
+    /**
+     * friendUserId 鈫� Friend 鐨� O(1) 绱㈠紩锛涗粠 friends 鏁扮粍娲剧敓锛孭inia 鍦� friends 鍙樺寲鏃惰嚜鍔ㄩ噸绠�
+     *
+     * 娑堟伅娓叉煋闇�瑕佹寜 senderId 鍙嶆煡鍙戦�佷汉澶村儚 / 澶囨敞锛屾瘡鏉℃秷鎭覆鏌撻兘浼� getFriend锛�
+     * 鐩存帴 find 鏃� N 鏉℃秷鎭� 脳 M 濂藉弸 = O(N脳M)锛涘缓绱㈠紩鍚庡崟娆¤ O(1)锛岄噸寤哄彧鍦ㄥ啓濂藉弸锛坒etchFriendList / upsertFriend 绛夛級鏃跺彂鐢�
+     */
+    getFriendMap: (state): Map<number, Friend> => {
+      const map = new Map<number, Friend>()
+      for (const friend of state.friends) {
+        map.set(friend.friendUserId, friend)
+      }
+      return map
+    },
+    /** 鎸� friendUserId 鎵惧ソ鍙嬶紙鍚凡杞垹鐨� DISABLE 璁板綍锛岃皟鐢ㄦ柟鑷鍒ゅ畾锛� */
+    getFriend(): (friendUserId: number) => Friend | undefined {
+      return (friendUserId: number) => this.getFriendMap.get(friendUserId)
+    },
+    /** 褰撳墠鐢熸晥鐨勫ソ鍙嬪垪琛紙杩囨护鎺� DISABLE 杞垹璁板綍锛� */
+    getActiveFriendList: (state): Friend[] => {
+      return state.friends.filter((friend) => friend.status !== CommonStatusEnum.DISABLE)
+    },
+    /** 褰撳墠鐢熸晥濂藉弸鐨� Lite 瑙嗗浘锛圥ickerPanel / 閫変汉寮圭獥鍏辩敤锛岃嚜甯︽嫾闊冲瓧娈典緵鍒嗘《 / 鎼滅储锛� */
+    getActiveFriendLiteList(): FriendLite[] {
+      return this.getActiveFriendList.map((friend: Friend) => ({
+        id: friend.friendUserId,
+        nickname: friend.nickname,
+        nicknamePinyin: friend.nicknamePinyin,
+        avatar: friend.avatar,
+        displayName: friend.displayName,
+        displayNamePinyin: friend.displayNamePinyin
+      }))
+    },
+    /** 鍒ゆ柇瀵规柟鏄惁鏄綋鍓嶇敤鎴风殑鏈夋晥濂藉弸锛堝瓨鍦� + 闈� DISABLE锛� */
+    isActiveFriend() {
+      return (friendUserId: number): boolean => {
+        const entry = this.getFriend(friendUserId)
+        return !!entry && entry.status !== CommonStatusEnum.DISABLE
+      }
+    },
+    /** 鎴戠殑榛戝悕鍗曪紙blocked=true 涓� ENABLE锛� */
+    getBlockedFriendList: (state): Friend[] => {
+      return state.friends.filter(
+        (friend) => friend.status !== CommonStatusEnum.DISABLE && friend.blocked === true
+      )
+    },
+    /** 鏈鐞嗙敵璇锋暟锛堟帴鏀舵柟=鎴戯級鈥斺�� 瀹炴椂娲剧敓锛屻�屾柊鐨勬湅鍙嬨�嶇孩鐐圭敤 */
+    getUnhandledRequestCount: (state): number => {
+      const currentUserId = getCurrentUserId()
+      return state.friendRequests.filter(
+        (request) =>
+          request.handleResult === ImFriendRequestHandleResult.UNHANDLED &&
+          request.toUserId === currentUserId
+      ).length
+    }
+  },
+
+  actions: {
+    // ==================== 鏈湴缂撳瓨 ====================
+
+    /** 浠� IndexedDB 鎭㈠濂藉弸鍜屽ソ鍙嬬敵璇� */
+    async loadFriendData(): Promise<boolean> {
+      try {
+        const [friends, friendRequests] = await Promise.all([
+          getDb().getAll<Friend>('friends'),
+          getDb().getAll<FriendRequest>('friendRequests')
+        ])
+        if (friends.length > 0) {
+          this.friends = friends
+        }
+        if (friendRequests.length > 0) {
+          this.friendRequests = friendRequests.toSorted(
+            (requestA, requestB) => requestB.id - requestA.id
+          )
+          this.hasMoreFriendRequests = friendRequests.length >= FRIEND_REQUEST_PAGE_SIZE
+        }
+        return friends.length > 0
+      } catch (error) {
+        console.warn('[IM friendStore] 鏈湴濂藉弸缂撳瓨璇诲彇澶辫触', error)
+        return false
+      }
+    },
+
+    /** 淇濆瓨濂藉弸鍒楄〃 */
+    saveFriendList(): void {
+      void getDb()
+        .transaction(['friends'], 'readwrite', async (tx) => {
+          const db = getDb()
+          await db.clearStore('friends', tx)
+          for (const friend of this.friends) {
+            if (friend.id) {
+              await db.put('friends', friend, tx)
+            }
+          }
+        })
+        .catch((error) => console.warn('[IM friendStore] 鏈湴濂藉弸缂撳瓨鍐欏叆澶辫触', error))
+    },
+
+    /** 淇濆瓨鍗曚釜濂藉弸 */
+    async saveFriendRecord(friend: Friend | undefined): Promise<void> {
+      if (!friend?.id) {
+        return
+      }
+      await getDb().put('friends', friend)
+    },
+
+    /** 淇濆瓨鍗曚釜濂藉弸 */
+    saveFriend(friend: Friend | undefined): void {
+      void this.saveFriendRecord(friend).catch((error) =>
+        console.warn('[IM friendStore] 鏈湴濂藉弸鍐欏叆澶辫触', error)
+      )
+    },
+
+    /** 淇濆瓨濂藉弸鐢宠鍒楄〃 */
+    saveFriendRequestList(): void {
+      void getDb()
+        .transaction(['friendRequests'], 'readwrite', async (tx) => {
+          const db = getDb()
+          await db.clearStore('friendRequests', tx)
+          for (const request of this.friendRequests) {
+            await db.put('friendRequests', request, tx)
+          }
+        })
+        .catch((error) => console.warn('[IM friendStore] 鏈湴濂藉弸鐢宠缂撳瓨鍐欏叆澶辫触', error))
+    },
+
+    /** 淇濆瓨鍗曟潯濂藉弸鐢宠 */
+    async saveFriendRequestRecord(request: FriendRequest | undefined): Promise<void> {
+      if (!request) {
+        return
+      }
+      await getDb().put('friendRequests', request)
+    },
+
+    /** 淇濆瓨鍗曟潯濂藉弸鐢宠 */
+    saveFriendRequest(request: FriendRequest | undefined): void {
+      void this.saveFriendRequestRecord(request).catch((error) =>
+        console.warn('[IM friendStore] 鏈湴濂藉弸鐢宠鍐欏叆澶辫触', error)
+      )
+    },
+
+    // ==================== 杩滅鎷夊彇 ====================
+
+    /** 浠庡悗绔媺鍙栧苟瑕嗙洊鏈湴鍒楄〃锛堝惈 DISABLE 鍘嗗彶濂藉弸缁欏凡鍒犲璇濆厹搴曪級锛涘彧鍚屾 ENABLE 鐨勪細璇濅俊鎭紝DISABLE 鐨勪笉鍔� 鈥斺�� cascade 娓呬細璇濈敱 WS dispatcher 鎸� payload.clear 澶勭悊锛岄伩鍏� fetchFriendList 瑕嗙洊鐢ㄦ埛銆屼笉娓呯┖鑱婂ぉ璁板綍銆嶇殑閫夋嫨 */
+    async fetchFriendList(force = false) {
+      if (this.loaded && !force) {
+        return
+      }
+      // 蹇収 epoch锛沜lear() 涔嬪悗鍒� .then 涔嬮棿瑙﹀彂鐨� epoch++ 琛ㄧず璐﹀彿宸插垏锛屾棫缁撴灉涓嶈兘鍐欏叆鏂� store
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      if (
+        pendingFetchFriends?.epoch === requestEpoch &&
+        pendingFetchFriends.userId === requestUserId
+      ) {
+        return pendingFetchFriends.promise
+      }
+      const promise = apiGetMyFriendList()
+        .then((list) => {
+          if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+            return
+          }
+          this.friends = (list || []).map((friend) => convertFriend(friend))
+          this.loaded = true
+          const conversationStore = useConversationStore()
+          for (const friend of this.friends) {
+            if (friend.status === CommonStatusEnum.DISABLE) {
+              continue
+            }
+            conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
+              name: getFriendDisplayName(friend),
+              avatar: friend.avatar,
+              silent: friend.silent
+            })
+          }
+          this.saveFriendList()
+        })
+        .finally(() => {
+          if (
+            pendingFetchFriends?.epoch === requestEpoch &&
+            pendingFetchFriends.userId === requestUserId
+          ) {
+            pendingFetchFriends = null
+          }
+        })
+      pendingFetchFriends = { epoch: requestEpoch, userId: requestUserId, promise }
+      return promise
+    },
+
+    /**
+     * 澧為噺鎷夊彇濂藉弸鍙樻洿骞跺悎骞讹細杩涘叆 IM 棣栧睆涓绘暟鎹仮澶嶏紙绌烘父鏍� = 棣栨鍏ㄩ噺澧為噺鎷夛級+ 閲嶈繛 / 绂荤嚎琛ュ伩
+     *
+     * 鍚凡鍒犻櫎濂藉弸锛屾寜 status 璧� upsert
+     */
+    async pullFriends() {
+      // 蹇収 epoch锛涜处鍙峰湪鎷夊彇閫斾腑鍒囨崲锛坈lear() 鈫� epoch++锛夋椂涓㈠純鏃ц处鍙烽偅鍑犻〉缁撴灉锛岄槻璺ㄨ处鍙锋暟鎹硠婕�
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      const isActive = () => requestEpoch === storeEpoch && getCurrentUserId() === requestUserId
+      await runIncrementalPull(
+        StorageKeys.settings.friendPullCursor,
+        apiPullMyFriendList,
+        async (records) => {
+          if (!isActive()) {
+            return false
+          }
+          await Promise.all(records.map((vo) => this.upsertFriendForPull(convertFriend(vo))))
+          return true
+        },
+        isActive
+      )
+      // 缃� loaded锛屼緵閫氳褰曢〉 fetchFriendList(force=false) 澶嶇敤缂撳瓨鑰岄潪閲嶅鍏ㄩ噺鎷�
+      if (isActive()) {
+        this.loaded = true
+      }
+    },
+
+    /** 鎸� friendUserId 鑾峰彇璇︽儏骞跺悎骞跺埌鏈湴锛堜繚璇� nickname / avatar 鏈�鏂帮級 */
+    async fetchFriendInfo(friendUserId: number) {
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      if (!requestUserId) {
+        return
+      }
+      const key = getPendingFriendInfoKey(requestUserId, friendUserId)
+      const inflight = pendingFetchFriendInfos.get(key)
+      if (inflight) {
+        return inflight
+      }
+      const promise = (async () => {
+        try {
+          const data = await apiGetFriend(friendUserId)
+          if (!data) {
+            return
+          }
+          // clear() 宸插垏璐﹀彿锛氭棫璇锋眰鐨勫ソ鍙嬭鎯呬笉鑳藉啀 upsert 杩涙柊璐﹀彿鐨� friends
+          if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+            return
+          }
+          this.upsertFriend(convertFriend(data))
+        } catch (error) {
+          console.warn('[IM friendStore] fetchFriendInfo 澶辫触', error)
+        }
+      })().finally(() => {
+        if (pendingFetchFriendInfos.get(key) === promise) {
+          pendingFetchFriendInfos.delete(key)
+        }
+      })
+      pendingFetchFriendInfos.set(key, promise)
+      return promise
+    },
+
+    // ==================== 鐢宠-瀹℃壒 ====================
+
+    /** 鍙戣捣濂藉弸鐢宠锛氭垚鍔熷悗绛夊緟瀵规柟鍚屾剰锛堜笉鐩存帴钀藉湴涓哄ソ鍙嬶級 */
+    async applyFriendRequest(reqVO: ImFriendRequestApi.FriendRequestApplyReqVO): Promise<null | number> {
+      return await apiApplyFriendRequest(reqVO)
+    },
+
+    /** 鍚屾剰涓�鏉″ソ鍙嬬敵璇凤紱鍚庣浼氬弻鍚戣惤搴� + 鎺� FRIEND_ADD锛屾湰绔瓑閫氱煡鍒拌揪鍐� upsertFriend */
+    async agreeFriendRequest(requestId: number) {
+      await apiAgreeFriendRequest(requestId)
+      await this.applyHandleResult(requestId, ImFriendRequestHandleResult.AGREED)
+    },
+
+    /** 鎷掔粷涓�鏉″ソ鍙嬬敵璇� */
+    async refuseFriendRequest(requestId: number, handleContent?: string) {
+      await apiRefuseFriendRequest(requestId, handleContent)
+      await this.applyHandleResult(requestId, ImFriendRequestHandleResult.REFUSED, handleContent)
+    },
+
+    /** 鎶� handleResult 搴旂敤鍒版湰鍦扮敵璇疯褰曪紱鎵句笉鍒板氨鎸� id 鍗曟煡鍏滃簳 upsert锛岄伩鍏嶇牬鍧� id 鍊掑簭 */
+    async applyHandleResult(
+      requestId: number,
+      result: number,
+      handleContent?: string
+    ): Promise<void> {
+      const request = this.getFriendRequest(requestId)
+      if (request) {
+        request.handleResult = result
+        if (handleContent !== undefined) {
+          request.handleContent = handleContent
+        }
+        request.handleTime = Date.now()
+        this.saveFriendRequest(request)
+        return
+      }
+      await this.fetchFriendRequest(requestId)
+    },
+
+    /** 鎷夊彇銆屾垜鐩稿叧銆嶇殑濂藉弸鐢宠鍒楄〃棣栭〉锛堥〉闈㈡墦寮� / 鏀跺埌 FRIEND_REQUEST_RECEIVED 鏃跺埛鏂帮級锛沺ending 鏈熼棿澶嶇敤鍚屼竴 Promise */
+    async fetchFriendRequestList() {
+      if (pendingFetchRequests) {
+        const currentUserId = getCurrentUserId()
+        if (
+          pendingFetchRequests.epoch === storeEpoch &&
+          pendingFetchRequests.userId === currentUserId
+        ) {
+          return pendingFetchRequests.promise
+        }
+      }
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      const promise = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE)
+        .then((list) => {
+          if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+            return
+          }
+          const items = (list || []).map((request) => convertFriendRequest(request))
+          this.friendRequests = items
+          // 涓嶈冻涓�椤靛嵆娌℃湁鏇村锛涙弧椤靛彲鑳借繕鏈夛紝绛� loadMore 鎷夊埌 0 鏉″啀纭畾
+          this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE
+          this.saveFriendRequestList()
+        })
+        .finally(() => {
+          if (
+            pendingFetchRequests?.epoch === requestEpoch &&
+            pendingFetchRequests.userId === requestUserId
+          ) {
+            pendingFetchRequests = null
+          }
+        })
+      pendingFetchRequests = { epoch: requestEpoch, userId: requestUserId, promise }
+      return promise
+    },
+
+    /** 鍔犺浇鏇村鐢宠锛堟寜鏈湴鏈�鏃� requestId 娓告爣鍒嗛〉锛夛紱鏃犳洿澶� / pending 涓洿鎺ヨ繑鍥� */
+    async loadMoreFriendRequestList() {
+      const requestUserId = getCurrentUserId()
+      const hasSameFetchPending =
+        pendingFetchRequests?.epoch === storeEpoch && pendingFetchRequests.userId === requestUserId
+      if (!this.hasMoreFriendRequests || hasSameFetchPending) {
+        return
+      }
+      if (
+        pendingLoadMoreRequests?.epoch === storeEpoch &&
+        pendingLoadMoreRequests.userId === requestUserId
+      ) {
+        return pendingLoadMoreRequests.promise
+      }
+      const oldest = this.friendRequests[this.friendRequests.length - 1]
+      if (!oldest) {
+        return this.fetchFriendRequestList()
+      }
+      const requestEpoch = storeEpoch
+      const promise = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE, oldest.id)
+        .then((list) => {
+          if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+            return
+          }
+          const items = (list || []).map((request) => convertFriendRequest(request))
+          this.friendRequests.push(...items)
+          this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE
+          this.saveFriendRequestList()
+        })
+        .finally(() => {
+          if (
+            pendingLoadMoreRequests?.epoch === requestEpoch &&
+            pendingLoadMoreRequests.userId === requestUserId
+          ) {
+            pendingLoadMoreRequests = null
+          }
+        })
+      pendingLoadMoreRequests = { epoch: requestEpoch, userId: requestUserId, promise }
+      return promise
+    },
+
+    /** 鎸� id 鏌ョ敵璇疯褰曪紱鍒楄〃鏄寜 id 鍊掑簭鐨勫皬鍒楄〃锛孫(n) find 鍗冲彲锛屼笉鍐嶇淮鎶� Map 绱㈠紩 */
+    getFriendRequest(requestId: number): FriendRequest | undefined {
+      return this.friendRequests.find((request) => request.id === requestId)
+    },
+
+    /** 鎸� id 浠庡悗绔崟鏌ュ苟 upsert 鍒版湰鍦帮紙dispatcher 鍏滃簳鐢紝閬垮厤鍏ㄩ噺閲嶆媺锛夛紱鍚庣甯﹁秺鏉冭繃婊� */
+    async fetchFriendRequest(requestId: number) {
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      const data = await apiGetMyFriendRequest(requestId)
+      if (!data) {
+        return
+      }
+      // clear() 宸插垏璐﹀彿锛氭棫璇锋眰鐨勭敵璇疯褰曚笉鑳藉啀鍐欒繘鏂拌处鍙风殑 friendRequests
+      if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+        return
+      }
+      this.upsertFriendRequest(convertFriendRequest(data))
+    },
+
+    /** 鍚堝苟鍗曟潯濂藉弸鐢宠锛氬凡鏈夊垯鎸� id 瑕嗙洊锛涙柊璁板綍鎸� id 鍊掑簭鎻掑叆锛堟瘮鏈湴鏈�鏃ц繕鑰佸垯璺宠繃锛岀暀缁� loadMore 甯﹀洖锛� */
+    upsertFriendRequest(next: FriendRequest) {
+      void this.upsertFriendRequestForPull(next).catch((error) =>
+        console.warn('[IM friendStore] 鏈湴濂藉弸鐢宠鍐欏叆澶辫触', error)
+      )
+    },
+
+    /** 鍚堝苟鍗曟潯濂藉弸鐢宠 */
+    async upsertFriendRequestForPull(next: FriendRequest): Promise<void> {
+      const existing = this.getFriendRequest(next.id)
+      if (existing) {
+        Object.assign(existing, next)
+        await this.saveFriendRequestRecord(existing)
+        return
+      }
+      // 姣旀湰鍦版渶鏃� id 杩樿�侊細涓嶅叆鍒楄〃锛岃 loadMore 鑷劧甯﹀洖锛岄伩鍏嶇牬鍧� id 鍊掑簭 / 鍚庣画 loadMore 閲嶅 push
+      const oldest = this.friendRequests[this.friendRequests.length - 1]
+      if (oldest && next.id < oldest.id) {
+        return
+      }
+      // 鎸� id 鍊掑簭鎵鹃涓瘮鑷繁灏忕殑浣嶇疆鎻掑叆锛涙壘涓嶅埌鍒欒拷鍔犳湯灏�
+      const insertIndex = this.friendRequests.findIndex((request) => request.id < next.id)
+      if (insertIndex === -1) {
+        this.friendRequests.push(next)
+      } else {
+        this.friendRequests.splice(insertIndex, 0, next)
+      }
+      await this.saveFriendRequestRecord(next)
+    },
+
+    /** 澧為噺鎷夊彇濂藉弸鐢宠鍙樻洿骞跺悎骞讹紙閲嶈繛 / 绂荤嚎琛ュ伩锛夛紱鎸� update_time + id 娓告爣锛屽凡澶勭悊鐨勬寜 handleResult 瑕嗙洊 */
+    async pullFriendRequests() {
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      const isActive = () => requestEpoch === storeEpoch && getCurrentUserId() === requestUserId
+      await runIncrementalPull(
+        StorageKeys.settings.friendRequestPullCursor,
+        apiPullMyFriendRequestList,
+        async (records) => {
+          if (!isActive()) {
+            return false
+          }
+          await Promise.all(
+            records.map((vo) => this.upsertFriendRequestForPull(convertFriendRequest(vo)))
+          )
+          return true
+        },
+        isActive
+      )
+    },
+
+    // ==================== 濂藉弸鍏崇郴鎿嶄綔 ====================
+
+    /** 鍒犻櫎濂藉弸锛堝崟鍚戣蒋鍒狅紝鏈缃� DISABLE锛夛紱clear=true 鏃剁骇鑱旀竻鐞嗘湰鍦扮浉鍏虫暟鎹紙濡傜鑱婁細璇濓級锛屽苟閫忎紶鍚庣缁欏绔悓姝� */
+    async deleteFriend(friendUserId: number, clear: boolean = true) {
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      await apiDeleteFriend(friendUserId, clear)
+      if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+        return
+      }
+      this.removeFriend(friendUserId, clear)
+    },
+
+    /** 鍒囨崲鍏嶆墦鎵帮細鍚屾浼氳瘽鐨� silent 瀛楁锛岄伩鍏嶄細璇濆垪琛� silent 鍥炬爣绛� 1210 鎺ㄥ埌鎵嶆洿鏂� */
+    async setFriendSilent(friendUserId: number, silent: boolean) {
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      await apiUpdateFriend({ friendUserId, silent })
+      if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+        return
+      }
+      const friend = this.getFriend(friendUserId)
+      if (friend) {
+        friend.silent = silent
+        const conversationStore = useConversationStore()
+        conversationStore.updateConversation(ImConversationType.PRIVATE, friendUserId, { silent })
+        this.saveFriend(friend)
+      }
+    },
+
+    /** 鍒囨崲鑱旂郴浜虹疆椤� */
+    async setFriendPinned(friendUserId: number, pinned: boolean) {
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      await apiUpdateFriend({ friendUserId, pinned })
+      if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+        return
+      }
+      const friend = this.getFriend(friendUserId)
+      if (friend) {
+        friend.pinned = pinned
+        this.saveFriend(friend)
+      }
+    },
+
+    /** 鎷夐粦濂藉弸锛氭湰绔箰瑙傛洿鏂� + 璋冩帴鍙o紱鍚庣 FRIEND_BLOCK 鎺ㄥ埌鏃剁敱 dispatcher 鍏滃簳鍚屾澶氱 */
+    async blockFriend(friendUserId: number) {
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      await apiBlockFriend(friendUserId)
+      if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+        return
+      }
+      const friend = this.getFriend(friendUserId)
+      if (friend) {
+        friend.blocked = true
+        this.saveFriend(friend)
+      }
+    },
+
+    /** 绉诲嚭榛戝悕鍗曪細鏈涔愯鏇存柊 + 璋冩帴鍙o紱鍚庣 FRIEND_UNBLOCK 鎺ㄥ埌鏃剁敱 dispatcher 鍏滃簳鍚屾澶氱 */
+    async unblockFriend(friendUserId: number) {
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      await apiUnblockFriend(friendUserId)
+      if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+        return
+      }
+      const friend = this.getFriend(friendUserId)
+      if (friend) {
+        friend.blocked = false
+        this.saveFriend(friend)
+      }
+    },
+
+    /** 淇敼濂藉弸灞曠ず澶囨敞锛堜粎鑷繁鍙锛� */
+    async setFriendDisplayName(friendUserId: number, displayName: string) {
+      const requestEpoch = storeEpoch
+      const requestUserId = getCurrentUserId()
+      const value = displayName.trim()
+      // 鍚庣 displayName 璇箟锛歯ull/undefined = 涓嶆敼锛�"" = 娓呯┖锛屾墍浠ヨ繖閲岀洿鎺ヤ紶 value锛堝彲鑳芥槸绌轰覆锛�
+      await apiUpdateFriend({ friendUserId, displayName: value })
+      if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
+        return
+      }
+      const friend = this.getFriend(friendUserId)
+      if (friend) {
+        friend.displayName = value
+        const conversationStore = useConversationStore()
+        conversationStore.updateConversation(ImConversationType.PRIVATE, friendUserId, {
+          name: getFriendDisplayName(friend)
+        })
+        this.saveFriend(friend)
+      }
+    },
+
+    /** 鏈湴鍚堝苟 / 鏂板鏌愪釜濂藉弸锛圵ebSocket 浜嬩欢 & 鎵嬪姩鍒锋柊閮界敤锛� */
+    upsertFriend(friend: Friend) {
+      void this.upsertFriendForPull(friend).catch((error) =>
+        console.warn('[IM friendStore] 鏈湴濂藉弸鍐欏叆澶辫触', error)
+      )
+    },
+
+    /** 鏈湴鍚堝苟 / 鏂板鏌愪釜濂藉弸 */
+    async upsertFriendForPull(friend: Friend): Promise<void> {
+      const index = this.friends.findIndex(
+        (existing) => existing.friendUserId === friend.friendUserId
+      )
+      if (index === -1) {
+        this.friends.push({
+          ...friend,
+          status: friend.status ?? CommonStatusEnum.ENABLE
+        })
+      } else {
+        this.friends[index] = {
+          ...this.friends[index],
+          ...friend,
+          status: friend.status ?? CommonStatusEnum.ENABLE
+        }
+      }
+      const conversationStore = useConversationStore()
+      const merged = this.getFriend(friend.friendUserId)
+      conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
+        name: merged ? getFriendDisplayName(merged) : friend.nickname,
+        avatar: friend.avatar,
+        silent: friend.silent
+      })
+      await this.saveFriendRecord(merged)
+    },
+
+    /** 鏈湴鏍囪鍒犻櫎锛圵ebSocket FRIEND_DELETE 浜嬩欢瑙﹀彂锛沜lear=true 鏃剁骇鑱旀竻鐩稿叧鏁版嵁濡傜鑱婁細璇濓級 */
+    removeFriend(friendUserId: number, clear: boolean = true) {
+      const friend = this.getFriend(friendUserId)
+      if (friend) {
+        // blocked 涓嶅姩锛岃窡鍚庣 deleteFriend0銆屽垹濂藉弸鏈熼棿淇濈暀鎷夐粦鐘舵�併�嶅榻�
+        friend.status = CommonStatusEnum.DISABLE
+        friend.deleteTime = Date.now()
+      }
+      if (clear) {
+        const conversationStore = useConversationStore()
+        conversationStore.removePrivateConversation(friendUserId)
+      }
+      this.saveFriend(friend)
+    },
+
+    // ==================== WebSocket 浜嬩欢 dispatcher锛�1201-1210 娈碉級 ====================
+
+    /** FRIEND_REQUEST_RECEIVED(1203)锛氭敹鍒版柊鐢宠锛沺ayload 宸插甫鐢宠鏂规樀绉� / 澶村儚锛屾寜 requestId 鐩存帹 push 杩涘垪琛� */
+    applyFriendRequestReceivedNotification(payload: FriendNotificationPayload) {
+      if (!payload.requestId) {
+        return
+      }
+      const currentUserId = getCurrentUserId()
+      const existingIndex = this.friendRequests.findIndex((item) => item.id === payload.requestId)
+      if (existingIndex !== -1) {
+        const existing = this.friendRequests.splice(existingIndex, 1)[0]
+        if (!existing) {
+          return
+        }
+        const next = {
+          ...existing,
+          fromUserId: payload.operatorUserId,
+          toUserId: currentUserId,
+          handleResult: ImFriendRequestHandleResult.UNHANDLED,
+          applyContent: payload.applyContent,
+          addSource: payload.addSource,
+          createTime: Date.now(),
+          fromNickname: payload.fromNickname,
+          fromAvatar: payload.fromAvatar
+        }
+        this.friendRequests.unshift(next)
+        this.saveFriendRequest(next)
+        return
+      }
+      const next = {
+        id: payload.requestId,
+        fromUserId: payload.operatorUserId,
+        toUserId: currentUserId,
+        handleResult: ImFriendRequestHandleResult.UNHANDLED,
+        applyContent: payload.applyContent,
+        addSource: payload.addSource,
+        createTime: Date.now(),
+        fromNickname: payload.fromNickname,
+        fromAvatar: payload.fromAvatar
+      }
+      this.friendRequests.unshift(next)
+      this.saveFriendRequest(next)
+    },
+
+    /** FRIEND_REQUEST_APPROVED(1201)锛氭垜鐨勭敵璇疯鍚屾剰锛涙寜 requestId 鏇存柊鐘舵�侊紙FRIEND_ADD 浼氬彟澶栨帹锛� */
+    applyFriendRequestApprovedNotification(payload: FriendNotificationPayload) {
+      if (!payload.requestId) {
+        return
+      }
+      void this.applyHandleResult(payload.requestId, ImFriendRequestHandleResult.AGREED)
+    },
+
+    /** FRIEND_REQUEST_REJECTED(1202)锛氭垜鐨勭敵璇疯鎷掔粷锛涙寜 requestId 鏇存柊鐘舵�� */
+    applyFriendRequestRejectedNotification(payload: FriendNotificationPayload) {
+      if (!payload.requestId) {
+        return
+      }
+      void this.applyHandleResult(
+        payload.requestId,
+        ImFriendRequestHandleResult.REFUSED,
+        payload.handleContent
+      )
+    },
+
+    /**
+     * FRIEND_ADD(1204)锛氭柊澧炲ソ鍙嬶紱鏈鎷夊彇濂藉弸璇︽儏骞跺叆搴�
+     * peerUserId 鐢� websocketStore 鎸夊抚 sender / receiver 绠楀ソ浼犲叆锛歜ecomeFriends 鍗曟潯鍏ュ簱鍚庡弻鏂规敹鍒板悓涓�浠� payload锛�
+     * 鏈鐪熸鐨勩�屽绔�嶆槸甯т笂鐨勫彟涓�涓敤鎴凤紝涓嶆槸 payload.friendUserId锛坧ayload 閲屽浐瀹氭槸 toUserId锛夈��
+     */
+    applyFriendAddNotification(_payload: FriendNotificationPayload, peerUserId: number) {
+      if (this.isActiveFriend(peerUserId)) {
+        return
+      }
+      void this.fetchFriendInfo(peerUserId)
+    },
+
+    /**
+     * FRIEND_DELETE(1205)锛氬ソ鍙嬭鍒犻櫎锛涙湰绔竻鐞� + 鎸� payload.clear 鍐冲畾鏄惁绾ц仈娓呬細璇濓紙澶氱璺熶富鎿嶄綔绔竴鑷达級
+     * peerUserId 鐢� websocketStore 鎸夊抚 sender / receiver 绠楀ソ浼犲叆锛涗笌 FRIEND_ADD 淇濇寔涓�鑷寸殑 peer 鎺ㄦ柇
+     */
+    applyFriendDeleteNotification(payload: FriendNotificationPayload, peerUserId: number) {
+      this.removeFriend(peerUserId, payload.clear !== false)
+    },
+
+    /** FRIEND_BLOCK(1207)锛氭媺榛戯紱澶氱鍚屾 */
+    applyFriendBlockNotification(payload: FriendNotificationPayload) {
+      const friend = this.getFriend(payload.friendUserId)
+      if (friend) {
+        friend.blocked = true
+        this.saveFriend(friend)
+      }
+    },
+
+    /** FRIEND_UNBLOCK(1208)锛氱Щ鍑洪粦鍚嶅崟锛涘绔悓姝� */
+    applyFriendUnblockNotification(payload: FriendNotificationPayload) {
+      const friend = this.getFriend(payload.friendUserId)
+      if (friend) {
+        friend.blocked = false
+        this.saveFriend(friend)
+      }
+    },
+
+    /** FRIEND_INFO_UPDATED(1209)锛氬ソ鍙嬭祫鏂欏彉鏇达紙鏄电О / 澶村儚锛夛紱閲嶆媺璇︽儏 */
+    applyFriendInfoUpdatedNotification(payload: FriendNotificationPayload) {
+      void this.fetchFriendInfo(payload.friendUserId)
+    },
+
+    /** FRIEND_UPDATE(1210)锛氭壒閲忔洿鏂帮紙澶囨敞 / 鍏嶆墦鎵� / 鑱旂郴浜虹疆椤讹級锛涘绔悓姝� */
+    applyFriendUpdateNotification(payload: FriendNotificationPayload) {
+      const friend = this.getFriend(payload.friendUserId)
+      if (!friend) {
+        return
+      }
+      if (payload.displayName != null) {
+        friend.displayName = payload.displayName
+      }
+      if (payload.silent != null) {
+        friend.silent = payload.silent
+      }
+      if (payload.pinned != null) {
+        friend.pinned = payload.pinned
+      }
+      const conversationStore = useConversationStore()
+      conversationStore.updateConversation(ImConversationType.PRIVATE, payload.friendUserId, {
+        name: getFriendDisplayName(friend),
+        silent: friend.silent
+      })
+      this.saveFriend(friend)
+    },
+
+    /** 娓呯┖濂藉弸鍐呭瓨鐘舵�侊紝骞跺簾寮冩湭杩斿洖璇锋眰锛坧ending Promise 缃┖ + storeEpoch++锛� */
+    clear() {
+      this.friends = []
+      this.friendRequests = []
+      this.loaded = false
+      this.hasMoreFriendRequests = true
+      pendingFetchFriends = null
+      pendingFetchRequests = null
+      pendingLoadMoreRequests = null
+      pendingFetchFriendInfos.clear()
+      storeEpoch++
+    }
+  }
+})
+
+function convertFriend(vo: ImFriendApi.FriendRespVO): Friend {
+  return {
+    id: vo.id,
+    friendUserId: vo.friendUserId,
+    nickname: vo.nickname || String(vo.friendUserId),
+    nicknamePinyin: vo.nicknamePinyin,
+    avatar: vo.avatar,
+    silent: !!vo.silent,
+    displayName: vo.displayName || '',
+    displayNamePinyin: vo.displayNamePinyin,
+    addSource: vo.addSource,
+    pinned: !!vo.pinned,
+    blocked: !!vo.blocked,
+    status: vo.status,
+    addTime: vo.addTime ? new Date(vo.addTime).getTime() : undefined,
+    deleteTime: vo.deleteTime ? new Date(vo.deleteTime).getTime() : undefined
+  }
+}
+
+function convertFriendRequest(vo: ImFriendRequestApi.FriendRequestRespVO): FriendRequest {
+  return {
+    id: vo.id,
+    fromUserId: vo.fromUserId,
+    toUserId: vo.toUserId,
+    handleResult: vo.handleResult,
+    applyContent: vo.applyContent,
+    handleContent: vo.handleContent,
+    addSource: vo.addSource,
+    handleTime: vo.handleTime ? new Date(vo.handleTime).getTime() : undefined,
+    createTime: vo.createTime ? new Date(vo.createTime).getTime() : 0,
+    fromNickname: vo.fromNickname,
+    fromAvatar: vo.fromAvatar,
+    toNickname: vo.toNickname,
+    toAvatar: vo.toAvatar
+  }
+}
+
+export const useFriendStoreWithOut = () => useFriendStore()
+
+// dev: 璁� Pinia 鐨� actions / state 鏀瑰姩鏀寔 HMR锛岄伩鍏嶆瘡娆℃敼 store 閮藉緱纭埛
+if (import.meta.hot) {
+  import.meta.hot.accept(acceptHMRUpdate(useFriendStore, import.meta.hot))
+}

--
Gitblit v1.9.3