@@ -194,7 +202,7 @@ watch(visible, (open) => {
}
})
-/** 备注 popover 点击保存:先走 store API 同步后端,成功后再关 popover + 提示 */
+/** 备注 popover 点击保存:先走 store API 同步后端,成功后再关 popover + 提示(接口错误由全局拦截器统一 toast,不重复 catch) */
async function handleSaveDisplayName() {
if (!props.friend) {
return
diff --git a/src/views/im/home/store/faceStore.ts b/src/views/im/home/store/faceStore.ts
index 3aa7fede8..3fe789492 100644
--- a/src/views/im/home/store/faceStore.ts
+++ b/src/views/im/home/store/faceStore.ts
@@ -28,6 +28,9 @@ export const useFaceStore = defineStore('imFace', () => {
/** 个人表情包列表(用户长按「添加到表情」/ 上传产生) */
const faceUserItems = ref([])
+ /** reset() 时递增;旧账号那次还没返回的请求 resolve 后比对一致才写 store,防跨账号数据泄漏 */
+ let storeEpoch = 0
+
/**
* 系统表情包拉取 promise;ensureFacePacks 内 cache:
* - null = 还没拉过,下次调用真发请求
@@ -38,13 +41,19 @@ export const useFaceStore = defineStore('imFace', () => {
/** 按需拉取系统表情包(已拉过则直接复用 cached promise) */
async function ensureFacePacks(): Promise {
if (!facePacksPromise) {
+ const requestEpoch = storeEpoch
facePacksPromise = apiGetFacePackList()
.then((data) => {
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
facePacks.value = data || []
})
.catch((e) => {
console.warn('[IM] 拉取表情包失败', e)
- facePacksPromise = null
+ if (requestEpoch === storeEpoch) {
+ facePacksPromise = null
+ }
throw e
})
}
@@ -56,13 +65,19 @@ export const useFaceStore = defineStore('imFace', () => {
/** 按需拉取个人表情(已拉过则直接复用 cached promise) */
async function ensureFaceUserItems(): Promise {
if (!faceUserItemsPromise) {
+ const requestEpoch = storeEpoch
faceUserItemsPromise = apiGetFaceUserItemList()
.then((data) => {
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
faceUserItems.value = data || []
})
.catch((e) => {
console.warn('[IM] 拉取个人表情失败', e)
- faceUserItemsPromise = null
+ if (requestEpoch === storeEpoch) {
+ faceUserItemsPromise = null
+ }
throw e
})
}
@@ -76,11 +91,16 @@ export const useFaceStore = defineStore('imFace', () => {
* 返回 true / false 与 removeFaceUserItem 风格对齐;调用方按 boolean 决定是否提示
*/
async function addFaceUserItem(reqVO: ImFaceUserItemSaveReqVO): Promise {
+ const requestEpoch = storeEpoch
try {
const id = await apiCreateFaceUserItem(reqVO)
if (!id) {
return false
}
+ // reset 已切账号:旧请求拿到的 id 不能再 unshift 进新账号内存
+ if (requestEpoch !== storeEpoch) {
+ return false
+ }
// id 不在缓存里才插入;服务端唯一约束兜底了 race,本地理论上不会拿到重复 id
if (!faceUserItems.value.some((item) => item.id === id)) {
faceUserItems.value.unshift({
@@ -100,8 +120,13 @@ export const useFaceStore = defineStore('imFace', () => {
/** 删除个人表情;本地立即移除 */
async function removeFaceUserItem(id: number): Promise {
+ const requestEpoch = storeEpoch
try {
await apiDeleteFaceUserItem(id)
+ // reset 已切账号:不要再 filter 新账号列表
+ if (requestEpoch !== storeEpoch) {
+ return false
+ }
faceUserItems.value = faceUserItems.value.filter((item) => item.id !== id)
return true
} catch (e) {
@@ -116,6 +141,7 @@ export const useFaceStore = defineStore('imFace', () => {
faceUserItems.value = []
facePacksPromise = null
faceUserItemsPromise = null
+ storeEpoch++
}
return {
diff --git a/src/views/im/home/store/friendStore.ts b/src/views/im/home/store/friendStore.ts
index 85afc5bbd..be2a2f6ad 100644
--- a/src/views/im/home/store/friendStore.ts
+++ b/src/views/im/home/store/friendStore.ts
@@ -77,12 +77,23 @@ export const useFriendStore = defineStore('imFriendStore', {
}),
getters: {
+ /**
+ * friendUserId → Friend 的 O(1) 索引;从 friends 数组派生,Pinia 在 friends 变化时自动重算
+ *
+ * 消息渲染需要按 senderId 反查发送人头像 / 备注,每条消息渲染都会 getFriend;
+ * 直接 find 时 N 条消息 × M 好友 = O(N×M);建索引后单次读 O(1),重建只在写好友(fetchFriends / upsertFriend 等)时发生
+ */
+ friendMap: (state): Map => {
+ const map = new Map()
+ for (const friend of state.friends) {
+ map.set(friend.friendUserId, friend)
+ }
+ return map
+ },
/** 按 friendUserId 找好友(含已软删的 DISABLE 记录,调用方自行判定) */
- getFriend:
- (state) =>
- (friendUserId: number): Friend | undefined => {
- return state.friends.find((friend) => friend.friendUserId === friendUserId)
- },
+ getFriend(): (friendUserId: number) => Friend | undefined {
+ return (friendUserId: number) => this.friendMap.get(friendUserId)
+ },
/** 当前生效的好友列表(过滤掉 DISABLE 软删记录) */
getActiveFriends: (state): Friend[] => {
return state.friends.filter((friend) => friend.status !== CommonStatusEnum.DISABLE)
@@ -195,11 +206,16 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 按 friendUserId 获取详情并合并到本地(保证 nickname / avatar 最新) */
async loadFriendInfo(friendUserId: number) {
+ const requestEpoch = storeEpoch
try {
const data = await apiGetFriend(friendUserId)
if (!data) {
return
}
+ // clear() 已切账号:旧请求的好友详情不能再 upsert 进新账号的 friends
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
this.upsertFriend(convertFriend(data))
} catch (e) {
console.warn('[IM friendStore] loadFriendInfo 失败', e)
@@ -301,10 +317,15 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 按 id 从后端单查并 upsert 到本地(dispatcher 兜底用,避免全量重拉);后端带越权过滤 */
async loadFriendRequest(requestId: number) {
+ const requestEpoch = storeEpoch
const data = await apiGetMyFriendRequest(requestId)
if (!data) {
return
}
+ // clear() 已切账号:旧请求的申请记录不能再写进新账号的 friendRequests
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
const next = convertFriendRequest(data)
const existing = this.findFriendRequest(requestId)
if (existing) {
@@ -329,13 +350,21 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 删除好友(单向软删,本端置 DISABLE);clear=true 时级联清理本地相关数据(如私聊会话),并透传后端给多端同步 */
async deleteFriend(friendUserId: number, clear: boolean = true) {
+ const requestEpoch = storeEpoch
await apiDeleteFriend(friendUserId, clear)
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
this.removeFriend(friendUserId, clear)
},
/** 切换免打扰:同步会话的 silent 字段,避免会话列表 silent 图标等 1210 推到才更新 */
async setSilent(friendUserId: number, silent: boolean) {
+ const requestEpoch = storeEpoch
await apiUpdateFriend({ friendUserId, silent })
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
const friend = this.getFriend(friendUserId)
if (friend) {
friend.silent = silent
@@ -347,7 +376,11 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 切换联系人置顶 */
async setPinned(friendUserId: number, pinned: boolean) {
+ const requestEpoch = storeEpoch
await apiUpdateFriend({ friendUserId, pinned })
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
const friend = this.getFriend(friendUserId)
if (friend) {
friend.pinned = pinned
@@ -357,7 +390,11 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 拉黑好友:本端乐观更新 + 调接口;后端 FRIEND_BLOCK 推到时由 dispatcher 兜底同步多端 */
async blockFriend(friendUserId: number) {
+ const requestEpoch = storeEpoch
await apiBlockFriend(friendUserId)
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
const friend = this.getFriend(friendUserId)
if (friend) {
friend.blocked = true
@@ -367,7 +404,11 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 移出黑名单:本端乐观更新 + 调接口;后端 FRIEND_UNBLOCK 推到时由 dispatcher 兜底同步多端 */
async unblockFriend(friendUserId: number) {
+ const requestEpoch = storeEpoch
await apiUnblockFriend(friendUserId)
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
const friend = this.getFriend(friendUserId)
if (friend) {
friend.blocked = false
@@ -377,9 +418,13 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 修改好友展示备注(仅自己可见) */
async setDisplayName(friendUserId: number, displayName: string) {
+ const requestEpoch = storeEpoch
const value = displayName.trim()
// 后端 displayName 语义:null/undefined = 不改,"" = 清空,所以这里直接传 value(可能是空串)
await apiUpdateFriend({ friendUserId, displayName: value })
+ if (requestEpoch !== storeEpoch) {
+ return
+ }
const friend = this.getFriend(friendUserId)
if (friend) {
friend.displayName = value