From fc812aef26aa3d48bae7e646753d42a6dac89b36 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 20 May 2026 01:00:46 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=A2=91=E9=81=93=E6=B6=88=E6=81=AF=E7=9A=84=E5=B7=B2=E8=AF=BB?= =?UTF-8?q?=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/im/manager/channel/message/index.ts | 1 + src/api/im/message/channel/index.ts | 10 +++++++ .../im/home/composables/useMessagePuller.ts | 3 +- .../im/home/composables/useMessageSender.ts | 22 +++++++++----- src/views/im/home/store/websocketStore.ts | 29 +++++++++++++++++-- 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/api/im/manager/channel/message/index.ts b/src/api/im/manager/channel/message/index.ts index 4b13356a0..257e72042 100644 --- a/src/api/im/manager/channel/message/index.ts +++ b/src/api/im/manager/channel/message/index.ts @@ -6,6 +6,7 @@ export interface ImManagerChannelMessageVO { channelName?: string materialId: number materialTitle?: string + materialCoverUrl?: string type: number content?: string receiverUserIds?: number[] diff --git a/src/api/im/message/channel/index.ts b/src/api/im/message/channel/index.ts index b57242258..41ab605ff 100644 --- a/src/api/im/message/channel/index.ts +++ b/src/api/im/message/channel/index.ts @@ -6,6 +6,8 @@ export interface ImChannelMessageRespVO { materialId: number type: number content: string + /** 当前用户已读态;pull 时按 Redis 游标计算填充,多端同步使用 */ + status?: number sendTime: string } @@ -16,3 +18,11 @@ export const pullChannelMessages = (params: { minId: number; size?: number }) => params }) } + +// 上报频道消息已读位置;切到频道会话或拉到新消息后调 +export const readChannelMessages = (channelId: number, messageId: number) => { + return request.put({ + url: '/im/channel/message/read', + params: { channelId, messageId } + }) +} diff --git a/src/views/im/home/composables/useMessagePuller.ts b/src/views/im/home/composables/useMessagePuller.ts index e5096334d..3f045963a 100644 --- a/src/views/im/home/composables/useMessagePuller.ts +++ b/src/views/im/home/composables/useMessagePuller.ts @@ -19,6 +19,7 @@ import { } from '@/api/im/message/channel' import { ImConversationType, + ImMessageStatus, ImMessageType, isFriendChatTip, isFriendNotification @@ -96,7 +97,7 @@ export const useMessagePuller = () => { clientMessageId: '', type: message.type, content: message.content, - status: 0, // 频道消息无状态机;占位 UNREAD + status: message.status ?? ImMessageStatus.UNREAD, sendTime: new Date(message.sendTime).getTime(), senderId: 0, // 系统下发,无发送人 targetId: message.channelId, // 会话归属到频道编号 diff --git a/src/views/im/home/composables/useMessageSender.ts b/src/views/im/home/composables/useMessageSender.ts index f0576352a..d3b13cff8 100644 --- a/src/views/im/home/composables/useMessageSender.ts +++ b/src/views/im/home/composables/useMessageSender.ts @@ -10,6 +10,7 @@ import { readGroupMessages as apiReadGroupMessages, recallGroupMessage as apiRecallGroupMessage } from '@/api/im/message/group' +import { readChannelMessages as apiReadChannelMessages } from '@/api/im/message/channel' import { generateClientMessageId, serializeMessage, @@ -233,19 +234,24 @@ export const useMessageSender = () => { // 接口调用:按会话类型分发,并按对应已读开关控制;失败仅记录日志,不回退本地已读状态 const isPrivate = conversation.type === ImConversationType.PRIVATE const isGroup = conversation.type === ImConversationType.GROUP - // 频道目前不上报已读 - // TODO @AI:频道已读,应该还是要上报的,同步到别的端。但是不用记录 status 字段。 - if (!isPrivate && !isGroup) { + const isChannel = conversation.type === ImConversationType.CHANNEL + if (!isPrivate && !isGroup && !isChannel) { return } - const readEnabled = isPrivate ? MESSAGE_PRIVATE_READ_ENABLED : MESSAGE_GROUP_READ_ENABLED - if (!readEnabled) { + if (isPrivate && !MESSAGE_PRIVATE_READ_ENABLED) { + return + } + if (isGroup && !MESSAGE_GROUP_READ_ENABLED) { return } try { - await (isPrivate - ? apiReadPrivateMessages(conversation.targetId, maxMessageId) - : apiReadGroupMessages(conversation.targetId, maxMessageId)) + if (isPrivate) { + await apiReadPrivateMessages(conversation.targetId, maxMessageId) + } else if (isGroup) { + await apiReadGroupMessages(conversation.targetId, maxMessageId) + } else { + await apiReadChannelMessages(conversation.targetId, maxMessageId) + } } catch (e) { console.error( '[IM] 标记已读失败', diff --git a/src/views/im/home/store/websocketStore.ts b/src/views/im/home/store/websocketStore.ts index b60cc159d..600b9d368 100644 --- a/src/views/im/home/store/websocketStore.ts +++ b/src/views/im/home/store/websocketStore.ts @@ -5,6 +5,7 @@ import { useUserStore } from '@/store/modules/user' import { ImWebSocketMessageType, + ImMessageStatus, ImMessageType, ImConversationType, ImRtcParticipantStatus, @@ -213,13 +214,37 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', { this.dispatchGroupFrame(content as ImGroupMessageDTO) break case ImWebSocketMessageType.CHANNEL_MESSAGE: - this.handleChannelMessage(content as ImChannelMessageRespVO) + this.dispatchChannelFrame(content as ImChannelMessageRespVO) break default: console.debug('[IM WS] 未识别事件', frame) } }, + /** + * 频道帧分发:按 payload.type 分到 READ(多端已读同步)或普通素材推送 + */ + dispatchChannelFrame(websocketMessage: ImChannelMessageRespVO) { + if (websocketMessage.type === ImMessageType.READ) { + this.handleChannelRead(websocketMessage) + return + } + this.handleChannelMessage(websocketMessage) + }, + + /** 频道 READ:自己其它终端在某频道里标为已读,本端同步清零该频道未读 */ + handleChannelRead(websocketMessage: ImChannelMessageRespVO) { + const conversationStore = useConversationStore() + const conversation = conversationStore.getConversation( + ImConversationType.CHANNEL, + websocketMessage.channelId + ) + if (conversation) { + conversation.unreadCount = 0 + } + conversationStore.saveConversations() + }, + /** * 频道消息实时入会话;频道消息单向 + 无状态机,直接 insertMessage 即可 * pull 与 WS 拿到同一条 id 时,conversationStore.insertMessage 内部按 id 去重,不会重复 @@ -245,7 +270,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', { clientMessageId: '', type: websocketMessage.type, content: websocketMessage.content, - status: 0, + status: ImMessageStatus.UNREAD, sendTime: sendTimeMs, senderId: 0, targetId: websocketMessage.channelId,