gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
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; }
/** å½“前正在进行的好友列表拉取;多 dispatcher åŒæ—¶è§¦å‘时复用同一 Promise,避免雪崩重拉 */
let pendingFetchFriends: null | PendingRequest = null
/** å½“前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise,避免雪崩重拉 */
let pendingFetchRequests: null | PendingRequest = null
/** å½“前正在进行的「加载更多申请」请求 */
let pendingLoadMoreRequests: null | PendingRequest = null
/** å½“前正在进行的好友详情请求 */
const pendingFetchFriendInfos = new Map<string, Promise<void>>()
/** clear() æ—¶é€’增;旧账号那次还没返回的请求 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
 *
 * è´Ÿè´£ï¼š
 * - æ‹‰å– / ç¼“存当前登录用户的好友列表 + ç”³è¯·åˆ—表
 * - ç”³è¯·-审批流程(apply / agree / refuse)+ å¤‡æ³¨ / å…æ‰“扰 / è”系人置顶 / æ‹‰é»‘
 * - æŽ¥æ”¶ WebSocket 1201-1210 æ®µä½é€šçŸ¥ï¼ŒæŒ‰äº‹ä»¶åˆ†å‘到 friendStore å†…部各 dispatcher
 */
export const useFriendStore = defineStore('imFriendStore', {
  state: () => ({
    friends: [] as Friend[],
    // ä»… fetchFriendList æˆåŠŸåŽç½®ä½ï¼›loadFriendData(IDB)不置位,否则后台 SWR åˆ·æ–°ä¼šè¢«ç¼“存命中跳过
    loaded: false,
    /** æˆ‘相关的好友申请列表(含我发起的 + åˆ«äººåŠ æˆ‘çš„ï¼›åŽç«¯æŒ‰ id å€’序游标分页) */
    friendRequests: [] as FriendRequest[],
    /** æ˜¯å¦è¿˜æœ‰æ›´æ—©çš„申请记录可加载;返回不满 page size å³ç½® false */
    hasMoreFriendRequests: true
  }),
  getters: {
    /**
     * friendUserId â†’ Friend çš„ O(1) ç´¢å¼•;从 friends æ•°ç»„派生,Pinia åœ¨ friends å˜åŒ–时自动重算
     *
     * æ¶ˆæ¯æ¸²æŸ“需要按 senderId åæŸ¥å‘送人头像 / å¤‡æ³¨ï¼Œæ¯æ¡æ¶ˆæ¯æ¸²æŸ“都会 getFriend;
     * ç›´æŽ¥ find æ—¶ N æ¡æ¶ˆæ¯ Ã— M å¥½å‹ = O(N×M);建索引后单次读 O(1),重建只在写好友(fetchFriendList / 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 è§†å›¾ï¼ˆPickerPanel / é€‰äººå¼¹çª—共用,自带拼音字段供分桶 / æœç´¢ï¼‰ */
    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;clear() ä¹‹åŽåˆ° .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;账号在拉取途中切换(clear() â†’ 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 æ—¶åˆ·æ–°ï¼‰ï¼›pending æœŸé—´å¤ç”¨åŒä¸€ 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 å€’序的小列表,O(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)
      }
    },
    /** æ‹‰é»‘好友:本端乐观更新 + è°ƒæŽ¥å£ï¼›åŽç«¯ 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)
      }
    },
    /** ç§»å‡ºé»‘名单:本端乐观更新 + è°ƒæŽ¥å£ï¼›åŽç«¯ 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 è¯­ä¹‰ï¼šnull/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)
      }
    },
    /** æœ¬åœ°åˆå¹¶ / æ–°å¢žæŸä¸ªå¥½å‹ï¼ˆWebSocket äº‹ä»¶ & æ‰‹åŠ¨åˆ·æ–°éƒ½ç”¨ï¼‰ */
    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)
    },
    /** æœ¬åœ°æ ‡è®°åˆ é™¤ï¼ˆWebSocket FRIEND_DELETE äº‹ä»¶è§¦å‘ï¼›clear=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):收到新申请;payload å·²å¸¦ç”³è¯·æ–¹æ˜µç§° / å¤´åƒï¼ŒæŒ‰ 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 ç®—好传入:becomeFriends å•条入库后双方收到同一份 payload,
     * æœ¬ç«¯çœŸæ­£çš„「对端」是帧上的另一个用户,不是 payload.friendUserId(payload é‡Œå›ºå®šæ˜¯ 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)
    },
    /** æ¸…空好友内存状态,并废弃未返回请求(pending 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))
}