From e573462cb77299673e625f5eca84542e64472ee2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 26 Apr 2026 10:38:14 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E5=A2=9E=E5=8A=A0=20us?= =?UTF-8?q?eMessagePuller=20=E7=94=A8=E4=BA=8E=E9=A6=96=E6=AC=A1=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=9A=84=E6=8B=89=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/im/message/private/index.ts | 8 ++ .../im/home/composables/useMessagePuller.ts | 96 +++++++++++++------ 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/api/im/message/private/index.ts b/src/api/im/message/private/index.ts index 698e55d43..df21e2576 100644 --- a/src/api/im/message/private/index.ts +++ b/src/api/im/message/private/index.ts @@ -51,6 +51,14 @@ export const readPrivateMessages = (receiverId: number | string, messageId: numb }) } +// 查询对方已读到我发的最大消息 id(多端 / 离线后用于补齐已读状态) +export const getPrivateMaxReadMessageId = (peerId: number | string) => { + return request.get({ + url: '/im/message/private/max-read-message-id', + params: { peerId } + }) +} + // 撤回私聊消息 export const recallPrivateMessage = (id: number | string) => { return request.delete({ diff --git a/src/views/im/home/composables/useMessagePuller.ts b/src/views/im/home/composables/useMessagePuller.ts index 13372f7f5..114e7d08b 100644 --- a/src/views/im/home/composables/useMessagePuller.ts +++ b/src/views/im/home/composables/useMessagePuller.ts @@ -1,8 +1,11 @@ import { watch } from 'vue' import { useConversationStore } from '../store/conversationStore' import { useImWebSocketStore } from '../store/websocketStore' +import { useFriendStore } from '../store/friendStore' +import { useGroupStore } from '../store/groupStore' import { pullPrivateMessages as apiPullPrivateMessages, + getPrivateMaxReadMessageId as apiGetPrivateMaxReadMessageId, type ImPrivateMessageRespVO } from '@/api/im/message/private' import { @@ -33,10 +36,24 @@ export const useMessagePuller = () => { const conversationStore = useConversationStore() const wsStore = useImWebSocketStore() const userStore = useUserStore() + const friendStore = useFriendStore() + const groupStore = useGroupStore() const currentUserId = Number(userStore.getUser?.id) || 0 + /** 私聊会话归属:自己发的算"发给 receiverId 的会话",否则算"发送方的会话" */ + const getPrivatePeerId = (message: ImPrivateMessageRespVO) => + message.senderId === currentUserId ? message.receiverId : message.senderId + + /** 群消息发送者在群内的展示名(群备注 > 用户昵称) */ + const getGroupSenderNickName = (message: ImGroupMessageRespVO): string => { + const group = groupStore.getGroup(message.groupId) + const member = group?.members?.find((m) => m.userId === message.senderId) + return member?.displayUserName || member?.nickname || '' + } + /** 服务端私聊消息 -> 本地 Message */ const convertPrivateMessage = (message: ImPrivateMessageRespVO): Message => { + const friend = friendStore.getFriend(getPrivatePeerId(message)) return { id: message.id, clientMessageId: message.clientMessageId || '', @@ -45,7 +62,7 @@ export const useMessagePuller = () => { status: message.status, sendTime: new Date(message.sendTime).getTime(), senderId: message.senderId, - senderNickName: '', + senderNickName: friend?.nickname || '', targetId: message.receiverId, selfSend: message.senderId === currentUserId } @@ -61,7 +78,7 @@ export const useMessagePuller = () => { status: message.status, sendTime: new Date(message.sendTime).getTime(), senderId: message.senderId, - senderNickName: '', + senderNickName: getGroupSenderNickName(message), targetId: message.groupId, selfSend: message.senderId === currentUserId, atUserIds: message.atUserIds || [], @@ -73,22 +90,24 @@ export const useMessagePuller = () => { /** 私聊:会话归属到对端 userId */ const convertPrivateConversation = (message: ImPrivateMessageRespVO) => { - const targetId = message.senderId === currentUserId ? message.receiverId : message.senderId + const targetId = getPrivatePeerId(message) + const friend = friendStore.getFriend(targetId) return { type: ImConversationType.PRIVATE, targetId, - name: String(targetId), - avatar: '' + name: friend?.nickname || String(targetId), + avatar: friend?.avatar || '' } } /** 群聊:会话归属到 groupId */ const convertGroupConversation = (message: ImGroupMessageRespVO) => { + const group = groupStore.getGroup(message.groupId) return { type: ImConversationType.GROUP, targetId: message.groupId, - name: String(message.groupId), - avatar: '' + name: group?.name || String(message.groupId), + avatar: group?.avatar || '' } } @@ -112,11 +131,12 @@ export const useMessagePuller = () => { if (isPrivate) { const message = raw as ImPrivateMessageRespVO if (message.type === ImMessageType.RECALL) { + const peerId = getPrivatePeerId(message) conversationStore.recallMessage( ImConversationType.PRIVATE, - message.senderId === currentUserId ? message.receiverId : message.senderId, + peerId, message.content, - '', + friendStore.getFriend(peerId)?.nickname || '', message.senderId === currentUserId ) continue @@ -132,7 +152,7 @@ export const useMessagePuller = () => { ImConversationType.GROUP, message.groupId, message.content, - '', + getGroupSenderNickName(message), message.senderId === currentUserId ) continue @@ -164,28 +184,50 @@ export const useMessagePuller = () => { return pullPromise } pullPromise = (async () => { - conversationStore.loading = true try { - // 并发拉取私聊 + 群聊,降低初始加载耗时 - await Promise.all([ - pullByType(ImConversationType.PRIVATE, conversationStore.privateMessageMaxId), - pullByType(ImConversationType.GROUP, conversationStore.groupMessageMaxId) - ]) + conversationStore.loading = true + try { + // 并发拉取私聊 + 群聊,降低初始加载耗时 + await Promise.all([ + pullByType(ImConversationType.PRIVATE, conversationStore.privateMessageMaxId), + pullByType(ImConversationType.GROUP, conversationStore.groupMessageMaxId) + ]) - // 回放 WebSocket 在 loading 期间收到的缓冲消息 - const buffered = wsStore.flushBuffer() - for (const item of buffered) { - if (item.conversationType === ImConversationType.PRIVATE) { - wsStore.handlePrivateMessage(item.payload) - } else { - wsStore.handleGroupMessage(item.payload) + // 回放 WebSocket 在 loading 期间收到的缓冲消息 + const buffered = wsStore.flushBuffer() + for (const item of buffered) { + if (item.conversationType === ImConversationType.PRIVATE) { + wsStore.handlePrivateMessage(item.payload) + } else { + wsStore.handleGroupMessage(item.payload) + } + } + } catch (e) { + console.error('[IM] 拉取离线消息失败:', e) + } finally { + conversationStore.loading = false + conversationStore.sortConversations() + } + + // 重连 / 冷启动后补齐当前激活私聊会话的「对方已读位置」 + // 离线期间错过的 RECEIPT 推送会被这里补回;其他私聊会话等用户点开时由 Index.vue 的 watch 触发 + const active = conversationStore.activeConversation + if (active && active.type === ImConversationType.PRIVATE) { + try { + const maxReadId = await apiGetPrivateMaxReadMessageId(active.targetId) + if (maxReadId) { + conversationStore.applyReadReceipt({ + conversationType: ImConversationType.PRIVATE, + targetId: active.targetId, + privateReadMaxId: maxReadId + }) + } + } catch (e) { + console.warn('[IM] 拉取对方已读位置失败', e) } } - } catch (e) { - console.error('[IM] 拉取离线消息失败:', e) } finally { - conversationStore.loading = false - conversationStore.sortConversations() + // 整个 IIFE 全部完成(含已读位置补齐)后才允许下一次 pullOnce 重入 pullPromise = null } })()