🐛 fix(im): 私聊已读消费端卡 maxReadId + 上报 messageId 与后端对齐

handlePrivateReceipt 收到对方 RECEIPT 时丢弃了后端编码在 DTO id 字段
的 maxReadId,applyReadReceipt 把会话里所有 selfSend 未撤回消息一刀切
标 READ;回执在路上时刚发的消息会被误标已读。
- applyReadReceipt 的 markPrivateRead 改为 privateReadMaxId,按
  id <= maxReadId 卡边界,超过 maxReadId 的自发消息保留原状态;
- handlePrivateReceipt 透传 websocketMessage.id 作为 privateReadMaxId;
- apiReadPrivateMessages 增加 messageId 形参,与后端新接口对齐;
- websocketStore 私聊自动已读用刚到的消息 id;useMessageSender.readActive
  把私聊 / 群聊的 maxMessageId 计算合并到调用前。
im
YunaiV 2026-04-26 09:46:09 +08:00
parent a35698fc07
commit 8c1f17f5a6
3 changed files with 28 additions and 9 deletions

View File

@ -38,13 +38,17 @@ export const pullPrivateMessages = (params: { minId: number | string; size: numb
}
// 查询私聊历史消息
// TODO @AI历史消息是不是通过这个接口
export const getPrivateMessageList = (params: ImPrivateMessageListReqVO) => {
return request.get<ImPrivateMessageRespVO[]>({ url: '/im/message/private/list', params })
}
// 标记私聊消息已读
export const readPrivateMessages = (receiverId: number | string) => {
return request.put<boolean>({ url: '/im/message/private/read', params: { receiverId } })
export const readPrivateMessages = (receiverId: number | string, messageId: number | string) => {
return request.put<boolean>({
url: '/im/message/private/read',
params: { receiverId, messageId }
})
}
// 撤回私聊消息

View File

@ -512,8 +512,9 @@ export const useConversationStore = defineStore('imConversationStore', {
applyReadReceipt(options: {
conversationType: number
targetId: number
// 私聊:把和该好友的「自己发送的」消息标为已读
markPrivateRead?: boolean
// 私聊把和该好友的「自己发送的、id <= privateReadMaxId 的」消息标为已读
// 必须卡 maxId 边界:回执在路上时新发的消息不能被误标为已读
privateReadMaxId?: number
// 群聊:针对单条消息的回执刷新
groupMessageId?: number
readCount?: number
@ -523,9 +524,15 @@ export const useConversationStore = defineStore('imConversationStore', {
if (!conversation) {
return
}
if (options.conversationType === ImConversationType.PRIVATE && options.markPrivateRead) {
if (options.conversationType === ImConversationType.PRIVATE && options.privateReadMaxId) {
const maxReadId = options.privateReadMaxId
conversation.messages.forEach((message) => {
if (message.selfSend && message.status !== ImMessageStatus.RECALL) {
if (
message.selfSend &&
message.id &&
message.id <= maxReadId &&
message.status !== ImMessageStatus.RECALL
) {
message.status = ImMessageStatus.READ
}
})

View File

@ -318,8 +318,9 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
conversationStore.activeConversation?.targetId === peerId
if (isActive) {
// 聊天窗口打开 = 实际看到了:本端清未读 + 上报后端,让对方 UI 立刻切到"已读"
// 已读位置直接用刚到的消息 id这条就是当前会话最大 id
conversationStore.markActiveAsRead()
apiReadPrivateMessages(peerId).catch((e) => {
apiReadPrivateMessages(peerId, websocketMessage.id).catch((e) => {
console.warn('[IM WS] 自动已读上报失败', e)
})
} else if (!conversation?.muted) {
@ -342,13 +343,20 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
conversationStore.saveConversations()
},
/** 私聊 RECEIPT 事件:对方读了我的消息,把和对方会话里自己发的消息标为已读 */
/**
* RECEIPT
* maxReadId DTO id ImPrivateMessageDTO.ofReceipt
* "回执在路上时刚发的消息"
*/
handlePrivateReceipt(websocketMessage: ImPrivateMessageDTO) {
if (!websocketMessage.id) {
return
}
const conversationStore = useConversationStore()
conversationStore.applyReadReceipt({
conversationType: ImConversationType.PRIVATE,
targetId: websocketMessage.senderId,
markPrivateRead: true
privateReadMaxId: websocketMessage.id
})
},