From 8ba76813aec66b97fa0cacf98119ddd926ba1113 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 17 Jun 2026 09:20:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(im):=20=E6=94=B6=E6=95=9B=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E6=8B=89=E5=8F=96=E7=9A=84=E5=AE=9E=E6=97=B6=E5=89=AF=E4=BD=9C?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 离线 pull 只还原历史好友、群聊事件气泡,不再重放实时通知副作用 - 好友详情请求增加 in-flight 去重,有效好友已存在时跳过重复拉取 - 修复软删好友重新添加时被本地缓存误跳过的问题 - 群创建通知只拉群详情,群成员改为进入会话后懒加载 - 避免群基础信息缺失或退群时兜底拉取整群成员 --- .../im/home/composables/useMessagePuller.ts | 5 +- src/views/im/home/index.vue | 8 +-- src/views/im/home/store/friendStore.ts | 51 ++++++++++++++----- src/views/im/home/store/groupStore.ts | 12 ++--- src/views/im/home/store/messageStore.ts | 22 ++------ 5 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/views/im/home/composables/useMessagePuller.ts b/src/views/im/home/composables/useMessagePuller.ts index a3d148462..d45044daf 100644 --- a/src/views/im/home/composables/useMessagePuller.ts +++ b/src/views/im/home/composables/useMessagePuller.ts @@ -217,10 +217,8 @@ export const useMessagePuller = () => { }) continue } - // 特殊:离线 pull 期间入库的 FRIEND_* 帧(目前仅 FRIEND_ADD persistent=true)也要走好友数据分发, - // 否则断线期间的好友列表更新会丢失;与 WebSocket 路径 dispatchPrivateFrame 保持对称 + // 特殊:历史好友事件只还原聊天气泡;好友主数据由好友增量补偿同步 if (isFriendNotification(message.type)) { - wsStore.handleFriendNotification(message) // 仅 FRIEND_ADD / FRIEND_DELETE 才作为会话气泡入消息列表 if (!isFriendChatTip(message.type)) { continue @@ -244,7 +242,6 @@ export const useMessagePuller = () => { }) continue } - // 其它消息正常入会话消息列表 pulledMessages.push({ kind: 'insert', conversationInfo: convertGroupConversation(message), diff --git a/src/views/im/home/index.vue b/src/views/im/home/index.vue index a84e8c7a7..7695b1fc1 100644 --- a/src/views/im/home/index.vue +++ b/src/views/im/home/index.vue @@ -79,7 +79,7 @@ onMounted(async () => { // 1.2 打开当前用户 IM DB await initDb() // 1.3 多个 store 并发从 IDB 读取本地缓存 - const [, , hasCachedFriends, hasCachedGroups, hasCachedChannels] = await Promise.all([ + const [, , hasFriendRows, hasGroupRows, hasChannelRows] = await Promise.all([ conversationStore.loadConversationList(), messageStore.loadMessageCursorList(), friendStore.loadFriendData(), @@ -99,18 +99,18 @@ onMounted(async () => { // 2.2 无缓存(首登 / 切账号回切):必须 await + 失败抛出中断本轮 onMounted, // 否则 pullOnce 会用 senderId 数字给会话起名落到 IDB 后续基本无法自愈;无缓存分支并发 Promise.all 省一个 RTT const requiredFetches: Promise[] = [] - if (hasCachedFriends) { + if (hasFriendRows) { void friendStore.pullFriends().catch((e) => console.warn('[IM] 后台增量拉好友失败', e)) } else { requiredFetches.push(friendStore.pullFriends()) } - if (hasCachedGroups) { + if (hasGroupRows) { void groupStore.fetchGroupList(true).catch((e) => console.warn('[IM] 后台刷新群列表失败', e)) } else { requiredFetches.push(groupStore.fetchGroupList(true)) } // 2.3 频道无增量 pull 接口,继续走 list - if (hasCachedChannels) { + if (hasChannelRows) { void channelStore.fetchChannelList().catch((e) => console.warn('[IM] 后台刷频道列表失败', e)) } else { requiredFetches.push(channelStore.fetchChannelList()) diff --git a/src/views/im/home/store/friendStore.ts b/src/views/im/home/store/friendStore.ts index 820bd0653..7c80ea633 100644 --- a/src/views/im/home/store/friendStore.ts +++ b/src/views/im/home/store/friendStore.ts @@ -39,10 +39,17 @@ let pendingFetchFriends: PendingRequest | null = null let pendingFetchRequests: PendingRequest | null = null /** 当前正在进行的「加载更多申请」请求 */ let pendingLoadMoreRequests: PendingRequest | null = null +/** 当前正在进行的好友详情请求 */ +const pendingFetchFriendInfos = new Map>() /** clear() 时递增;旧账号那次还没返回的请求 resolve 后比对一致才写 store,防跨账号数据泄漏 */ let storeEpoch = 0 +/** 构建好友详情请求去重 key */ +function getPendingFriendInfoKey(userId: number, friendUserId: number): string { + return `${userId}:${friendUserId}` +} + /** 好友通知 payload(对齐后端 BaseFriendNotification + 子类裁减后的字段) */ export interface FriendNotificationPayload { operatorUserId: number @@ -303,19 +310,35 @@ export const useFriendStore = defineStore('imFriendStore', { async fetchFriendInfo(friendUserId: number) { const requestEpoch = storeEpoch const requestUserId = getCurrentUserId() - try { - const data = await apiGetFriend(friendUserId) - if (!data) { - return - } - // clear() 已切账号:旧请求的好友详情不能再 upsert 进新账号的 friends - if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { - return - } - this.upsertFriend(convertFriend(data)) - } catch (e) { - console.warn('[IM friendStore] fetchFriendInfo 失败', e) + 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 (e) { + console.warn('[IM friendStore] fetchFriendInfo 失败', e) + } + })().finally(() => { + if (pendingFetchFriendInfos.get(key) === promise) { + pendingFetchFriendInfos.delete(key) + } + }) + pendingFetchFriendInfos.set(key, promise) + return promise }, // ==================== 申请-审批 ==================== @@ -707,6 +730,9 @@ export const useFriendStore = defineStore('imFriendStore', { * 本端真正的「对端」是帧上的另一个用户,不是 payload.friendUserId(payload 里固定是 toUserId)。 */ applyFriendAddNotification(_payload: FriendNotificationPayload, peerUserId: number) { + if (this.isActiveFriend(peerUserId)) { + return + } void this.fetchFriendInfo(peerUserId) }, @@ -773,6 +799,7 @@ export const useFriendStore = defineStore('imFriendStore', { pendingFetchFriends = null pendingFetchRequests = null pendingLoadMoreRequests = null + pendingFetchFriendInfos.clear() storeEpoch++ } } diff --git a/src/views/im/home/store/groupStore.ts b/src/views/im/home/store/groupStore.ts index b85904094..4b131f60c 100644 --- a/src/views/im/home/store/groupStore.ts +++ b/src/views/im/home/store/groupStore.ts @@ -226,7 +226,7 @@ export const useGroupStore = defineStore('imGroupStore', { if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { return } - const fresh = (list || []).map(convertGroup) + const fresh = (list || []).map((group) => convertGroup(group)) // 合并而非全量替换:silent / groupRemark / 成员缓存这些字段不在 ImGroupRespVO 里,得从旧 group 保留 const groupMap = new Map(this.groups.map((group) => [group.id, group])) this.groups = fresh.map((group) => { @@ -584,14 +584,14 @@ export const useGroupStore = defineStore('imGroupStore', { /** * 接收 GROUP_* 群广播事件,按 type 分发到对应私有 action * - * WebSocket 实时收 + useMessagePuller 离线 pull 都走 messageStore.insertMessage 旁路调用 + * WebSocket 实时收走 messageStore.insertMessage 旁路调用 * store 里没缓存的群静默忽略,等 fetchGroupList 兜底 */ applyGroupNotification(groupId: number, type: number, content?: string) { if (!groupId) { return } - let payload: GroupNotificationPayload = {} + let payload: GroupNotificationPayload try { payload = content ? JSON.parse(content) : {} } catch (error) { @@ -683,7 +683,7 @@ export const useGroupStore = defineStore('imGroupStore', { } }, - /** 创建群广播:创建者多端同步 + 初始成员首次拉取;payload.memberUserIds 含自己 → 拉群详情 / 成员;本端发起者已经 upsert 过本群,跳过避免双拉 */ + /** 创建群广播:群未就位时拉群详情 */ async applyGroupCreateNotification(groupId: number, payload: GroupNotificationPayload) { if (!isSelfInPayloadMembers(payload)) { return @@ -693,9 +693,7 @@ export const useGroupStore = defineStore('imGroupStore', { if (selfIsOperator && this.getGroup(groupId)) { return } - // 先 await fetchGroupInfo 把群 upsert 进 state.groups;否则 fetchGroupMemberList 的「不是我加入的群」guard 会兜空 await this.fetchGroupInfo(groupId) - this.fetchGroupMemberList(groupId, true).catch(() => undefined) }, /** 群名变更:按 newName 局部更新本地群名 */ @@ -890,7 +888,7 @@ function convertGroup(group: ImGroupRespVO): Group { avatar: group.avatar, notice: group.notice, ownerUserId: group.ownerUserId, - pinnedMessages: group.pinnedMessages?.map(convertGroupMessageVO), + pinnedMessages: group.pinnedMessages?.map((message) => convertGroupMessageVO(message)), mutedAll: group.mutedAll, banned: group.banned, joinApproval: group.joinApproval, diff --git a/src/views/im/home/store/messageStore.ts b/src/views/im/home/store/messageStore.ts index 2c474334f..69ac1bfa8 100644 --- a/src/views/im/home/store/messageStore.ts +++ b/src/views/im/home/store/messageStore.ts @@ -137,7 +137,7 @@ function deriveLastSenderDisplayName( if (conversation.type === ImConversationType.GROUP) { const groupStore = useGroupStore() const group = groupStore.getGroup(conversation.targetId) - if (isGroupQuit(group)) { + if (!group || isGroupQuit(group)) { return conversation.lastSenderId === senderId ? conversation.lastSenderDisplayName : undefined } const fetchPromise = @@ -501,24 +501,12 @@ export const useMessageStore = defineStore('imMessageStore', { const { conversationInfo } = pulledMessage const hasServerClientMessageId = !!pulledMessage.message.clientMessageId const message = ensureClientMessageId(pulledMessage.message) - // 1.2 群通知先同步群资料 - if ( - conversationInfo.type === ImConversationType.GROUP && - isGroupNotification(message.type) - ) { - useGroupStore().applyGroupNotification( - conversationInfo.targetId, - message.type, - message.content - ) - } - - // 1.3 确保会话和消息缓存存在 + // 1.2 确保会话和消息缓存存在 const conversation = conversationStore.ensureConversation(conversationInfo) const messages = this.getMessageList(conversationInfo.type, conversationInfo.targetId) const existingIndex = messages.findIndex((existing) => isSameMessage(existing, message)) if (existingIndex >= 0) { - // 1.4 已存在消息合并服务端状态 + // 1.3 已存在消息合并服务端状态 applyServerMessageUpdate(messages[existingIndex], message) if (existingIndex === messages.length - 1) { recomputeConversationLast(conversation, messages) @@ -530,7 +518,7 @@ export const useMessageStore = defineStore('imMessageStore', { continue } - // 1.5 新消息更新会话摘要和未读状态 + // 1.4 新消息更新会话摘要和未读状态 applyConversationSummary(conversation, message) syncConversationAtFlags(conversation, message) const isActive = @@ -545,7 +533,7 @@ export const useMessageStore = defineStore('imMessageStore', { conversation.unreadCount++ } - // 1.6 新消息按服务端 id 插入内存列表 + // 1.5 新消息按服务端 id 插入内存列表 let insertIndex = messages.length if (message.id) { for (let index = 0; index < messages.length; index++) {