From 27cd042df9aca0383a49f3514bc21958dd890912 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 15:42:23 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加IM即时通讯模块

---
 src/views/im/home/store/messageStore.ts |  953 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 953 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/store/messageStore.ts b/src/views/im/home/store/messageStore.ts
new file mode 100644
index 0000000..fa17b67
--- /dev/null
+++ b/src/views/im/home/store/messageStore.ts
@@ -0,0 +1,953 @@
+import type { Conversation, Message, MessageDO } from '../types'
+
+import { acceptHMRUpdate, defineStore } from 'pinia'
+
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import {
+  IM_AT_ALL_USER_ID,
+  ImContentType,
+  ImConversationType,
+  ImMessageReceiptStatus,
+  ImMessageStatus,
+  isGroupNotification,
+  isNormalMessage
+} from '../../utils/constants'
+import { resolveConversationLastContent } from '../../utils/conversation'
+import {
+  type DbTransaction,
+  getClientConversationId,
+  getClientMessageKey,
+  getDb,
+  getServerMessageKey,
+  parseClientConversationId,
+  setMessageMaxId,
+  StorageKeys
+} from '../../utils/db'
+import {
+  generateClientMessageId,
+  parseRecallMessageId,
+  revokeBlobUrlsInContent
+} from '../../utils/message'
+import { isGroupQuit, tryGetSenderDisplayName } from '../../utils/user'
+import { useConversationStore } from './conversationStore'
+import { useGroupStore } from './groupStore'
+
+const MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT = 5
+const MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT = MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT + 1
+const ackMergingPromises = new Map<string, Promise<void>>()
+
+interface MessageConversationInfo {
+  type: number
+  targetId: number
+  name: string
+  avatar: string
+  silent?: boolean
+}
+
+interface PersistMessageRecordOptions {
+  mergeClientRecord?: boolean
+}
+
+/** 鎷夊彇娑堟伅鎵归噺澶勭悊椤� */
+export type PulledMessage =
+  | {
+      conversationInfo: MessageConversationInfo
+      kind: 'insert'
+      message: Message
+    }
+  | {
+      conversationType: number
+      kind: 'recall'
+      recallSignalContent: string
+      targetId: number
+    }
+
+/** 鑾峰彇浼氳瘽鐨勬秷鎭紦瀛� key */
+function getMessageCacheKey(type: number, targetId: number): string {
+  return getClientConversationId(type, targetId)
+}
+
+/** 鐢熸垚娑堟伅鏈湴涓婚敭 */
+function getMessageKey(
+  message: Pick<Message, 'clientMessageId' | 'id'>,
+  conversationType: number
+): string {
+  return message.id
+    ? getServerMessageKey(conversationType, message.id)
+    : getClientMessageKey(message.clientMessageId)
+}
+
+/** 琛ラ綈瀹㈡埛绔秷鎭紪鍙� */
+function ensureClientMessageId(message: Message): Message {
+  if (!message.clientMessageId) {
+    message.clientMessageId = generateClientMessageId()
+  }
+  if (!message.id) {
+    message.id = undefined
+  }
+  return message
+}
+
+/** 杞崲涓� IndexedDB 娑堟伅璁板綍 */
+function buildMessageDO(message: Message, conversationType: number): MessageDO {
+  return {
+    id: message.id,
+    clientMessageId: message.clientMessageId,
+    type: message.type,
+    content: message.content,
+    status: message.status,
+    sendTime: message.sendTime,
+    senderId: message.senderId,
+    atUserIds: message.atUserIds ? [...message.atUserIds] : undefined,
+    receiverUserIds: message.receiverUserIds ? [...message.receiverUserIds] : undefined,
+    receiptStatus: message.receiptStatus,
+    readCount: message.readCount,
+    materialId: message.materialId,
+    targetId: message.targetId,
+    selfSend: message.selfSend,
+    messageKey: getMessageKey(message, conversationType),
+    conversationType,
+    clientConversationId: getClientConversationId(conversationType, message.targetId)
+  }
+}
+
+/** IndexedDB 娑堟伅璁板綍杞墠绔秷鎭� */
+function buildMessageFromDO(message: MessageDO): Message {
+  const {
+    messageKey: _messageKey,
+    conversationType: _conversationType,
+    clientConversationId: _clientConversationId,
+    ...rest
+  } = message
+  return rest
+}
+
+/** 绠楀嚭鏈潯娑堟伅鐨勫彂閫佷汉蹇収 */
+function deriveLastSenderDisplayName(
+  conversation: Conversation,
+  senderId: number
+): string | undefined {
+  // 1. 浼樺厛浣跨敤褰撳墠鍐呭瓨涓殑濂藉弸 / 缇ゆ垚鍛樹俊鎭�
+  const liveSenderName = tryGetSenderDisplayName(senderId, conversation.type, conversation.targetId)
+  if (liveSenderName) {
+    return liveSenderName
+  }
+  // 2. 缇ゆ垚鍛樼紦瀛樼己澶辨椂寮傛琛ラ綈
+  if (conversation.type === ImConversationType.GROUP) {
+    const groupStore = useGroupStore()
+    const group = groupStore.getGroup(conversation.targetId)
+    if (!group || isGroupQuit(group)) {
+      return conversation.lastSenderId === senderId ? conversation.lastSenderDisplayName : undefined
+    }
+    const fetchPromise =
+      group?.membersLoaded && !group.membersExpired
+        ? groupStore.fetchGroupMember(conversation.targetId, senderId)
+        : groupStore.fetchGroupMemberList(conversation.targetId)
+    fetchPromise.catch((error) =>
+      console.warn(
+        '[IM messageStore] 鍏滃簳鎷夌兢鎴愬憳澶辫触',
+        { groupId: conversation.targetId, senderId, fullFetch: !group?.membersLoaded },
+        error
+      )
+    )
+  }
+  return conversation.lastSenderId === senderId ? conversation.lastSenderDisplayName : undefined
+}
+
+/** 鎸夋秷鎭洿鏂颁細璇濇憳瑕� */
+function applyConversationSummary(conversation: Conversation, message: Message): void {
+  const senderDisplayName = deriveLastSenderDisplayName(conversation, message.senderId)
+  conversation.lastContent = resolveConversationLastContent(
+    message,
+    conversation.type,
+    conversation.targetId,
+    senderDisplayName
+  )
+  conversation.lastSendTime = message.sendTime || Date.now()
+  conversation.lastSenderId = message.senderId
+  conversation.lastMessageType = message.type
+  conversation.lastMessageId = message.id
+  conversation.lastClientMessageId = message.clientMessageId
+  conversation.lastMessageStatus = message.status
+  conversation.lastReceiptStatus = message.receiptStatus
+  conversation.lastSelfSend = message.selfSend
+  conversation.lastSenderDisplayName = senderDisplayName
+}
+
+/** 鎸夋湯鏉℃秷鎭噸绠椾細璇濇憳瑕� */
+function recomputeConversationLast(conversation: Conversation, messages: Message[]): void {
+  const last = messages[messages.length - 1]
+  if (last) {
+    applyConversationSummary(conversation, last)
+    return
+  }
+  conversation.lastContent = ''
+  conversation.lastSendTime = 0
+  conversation.lastSenderId = undefined
+  conversation.lastMessageType = undefined
+  conversation.lastMessageId = undefined
+  conversation.lastClientMessageId = undefined
+  conversation.lastMessageStatus = undefined
+  conversation.lastReceiptStatus = undefined
+  conversation.lastSelfSend = undefined
+  conversation.lastSenderDisplayName = undefined
+}
+
+/** 鍚屾缇� @ 鐘舵�� */
+function syncConversationAtFlags(conversation: Conversation, message: Message): void {
+  if (
+    message.selfSend ||
+    conversation.type !== ImConversationType.GROUP ||
+    !message.atUserIds ||
+    message.atUserIds.length === 0
+  ) {
+    return
+  }
+  const currentUserId = getCurrentUserId()
+  if (currentUserId && message.atUserIds.includes(currentUserId)) {
+    conversation.atMe = true
+  }
+  if (message.atUserIds.includes(IM_AT_ALL_USER_ID)) {
+    conversation.atAll = true
+  }
+}
+
+/** 搴旂敤鏈嶅姟绔秷鎭洿鏂� */
+function applyServerMessageUpdate(message: Message, updates: Partial<Message>): void {
+  if (updates.content && updates.content !== message.content) {
+    revokeBlobUrlsInContent(message.content)
+  }
+  Object.assign(message, updates)
+  if (updates.id === 0) {
+    message.id = undefined
+  }
+  if (updates.status !== undefined && updates.status !== ImMessageStatus.SENDING) {
+    message.uploadProgress = undefined
+    if (updates.status !== ImMessageStatus.FAILED) {
+      message._localFile = undefined
+    }
+  }
+}
+
+/** 鍒ゆ柇鏄惁涓哄悓涓�鏉℃秷鎭� */
+function isSameMessage(left: Message, right: Message): boolean {
+  if (left.id && right.id && left.id === right.id) {
+    return true
+  }
+  return !!left.clientMessageId && left.clientMessageId === right.clientMessageId
+}
+
+export const useMessageStore = defineStore('imMessageStore', {
+  state: () => ({
+    messagesByConversation: {} as Record<string, Message[]>,
+    loadedConversationKeys: [] as string[],
+    privateReadMaxIds: {} as Partial<Record<number, number>>,
+    privateMessageMaxId: 0,
+    groupMessageMaxId: 0,
+    channelMessageMaxId: 0
+  }),
+
+  getters: {
+    /** 鑾峰彇浼氳瘽宸插姞杞芥秷鎭� */
+    getMessages:
+      (state) =>
+      (clientConversationId: string): Message[] =>
+        state.messagesByConversation[clientConversationId] || []
+  },
+
+  actions: {
+    /** 娓呯┖娑堟伅鍐呭瓨 */
+    clear() {
+      Object.values(this.messagesByConversation).forEach((messages) => {
+        messages.forEach((message) => {
+          revokeBlobUrlsInContent(message.content)
+          message._localFile = undefined
+        })
+      })
+      this.messagesByConversation = {}
+      this.loadedConversationKeys = []
+      this.privateReadMaxIds = {}
+      this.privateMessageMaxId = 0
+      this.groupMessageMaxId = 0
+      this.channelMessageMaxId = 0
+      ackMergingPromises.clear()
+    },
+
+    /** 浠� settings 鍔犺浇娑堟伅娓告爣 */
+    async loadMessageCursorList() {
+      const db = getDb()
+      const [privateMaxId, groupMaxId, channelMaxId] = await Promise.all([
+        db.getSetting<number>(StorageKeys.settings.privateMessageMaxId),
+        db.getSetting<number>(StorageKeys.settings.groupMessageMaxId),
+        db.getSetting<number>(StorageKeys.settings.channelMessageMaxId)
+      ])
+      this.privateMessageMaxId = privateMaxId || 0
+      this.groupMessageMaxId = groupMaxId || 0
+      this.channelMessageMaxId = channelMaxId || 0
+    },
+
+    /** 鏇存柊鍐呭瓨娓告爣 */
+    updateMessageCursor(conversationType: number, messageId?: number) {
+      if (!messageId) {
+        return
+      }
+      if (conversationType === ImConversationType.PRIVATE && messageId > this.privateMessageMaxId) {
+        this.privateMessageMaxId = messageId
+      } else if (
+        conversationType === ImConversationType.GROUP &&
+        messageId > this.groupMessageMaxId
+      ) {
+        this.groupMessageMaxId = messageId
+      } else if (
+        conversationType === ImConversationType.CHANNEL &&
+        messageId > this.channelMessageMaxId
+      ) {
+        this.channelMessageMaxId = messageId
+      }
+    },
+
+    /** 鑾峰彇绉佽亰瀵规柟宸茶浣嶇疆缂撳瓨 */
+    getPrivateReadMaxId(peerId: number): number | undefined {
+      return this.privateReadMaxIds[peerId]
+    },
+
+    /** 鏇存柊绉佽亰瀵规柟宸茶浣嶇疆缂撳瓨 */
+    updatePrivateReadMaxId(peerId: number, maxReadId: null | number = 0): number {
+      if (!peerId) {
+        return 0
+      }
+      const nextMaxReadId = maxReadId || 0
+      const current = this.getPrivateReadMaxId(peerId)
+      if (current !== undefined && nextMaxReadId <= current) {
+        return current
+      }
+      this.privateReadMaxIds = { ...this.privateReadMaxIds, [peerId]: nextMaxReadId }
+      return nextMaxReadId
+    },
+
+    /** 娓呯┖绉佽亰瀵规柟宸茶浣嶇疆缂撳瓨 */
+    clearPrivateReadMaxIdCache(): void {
+      this.privateReadMaxIds = {}
+    },
+
+    /** 鏍囪浼氳瘽杩戞湡浣跨敤 */
+    touchConversationMessageCache(clientConversationId: string) {
+      this.loadedConversationKeys = [
+        clientConversationId,
+        ...this.loadedConversationKeys.filter((key) => key !== clientConversationId)
+      ]
+      // 淇濈暀褰撳墠娲昏穬浼氳瘽 + 鏈�杩戞墦寮�杩囩殑浼氳瘽
+      const retained = this.loadedConversationKeys.slice(0, MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT)
+      const removed = this.loadedConversationKeys.slice(MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT)
+      this.loadedConversationKeys = retained
+      removed.forEach((key) => {
+        Reflect.deleteProperty(this.messagesByConversation, key)
+      })
+    },
+
+    /** 鍔犺浇褰撳墠浼氳瘽鏈�杩戞秷鎭� */
+    async loadMoreMessageList(
+      clientConversationId: string,
+      beforeSendTime?: number,
+      limit = 50
+    ): Promise<Message[]> {
+      // 1. 浠� IndexedDB 鍊掑簭璇诲彇涓�椤碉紝杩斿洖鍓嶅凡鎸夋椂闂村崌搴忔帓鍒�
+      const list = await getDb().getMessageListByConversation(clientConversationId, {
+        beforeSendTime,
+        limit
+      })
+      // 2. 鍚堝苟鍒板唴瀛樼紦瀛橈紝杩囨护宸插瓨鍦ㄧ殑娑堟伅
+      const parsed = parseClientConversationId(clientConversationId)
+      if (!parsed) {
+        return []
+      }
+      const messages = list.map((message) => buildMessageFromDO(message))
+      const existing = this.messagesByConversation[clientConversationId] || []
+      const existingKeys = new Set(existing.map((message) => getMessageKey(message, parsed.type)))
+      const fresh = messages.filter(
+        (message) => !existingKeys.has(getMessageKey(message, parsed.type))
+      )
+      this.messagesByConversation[clientConversationId] = [...fresh, ...existing].toSorted(
+        (messageA, messageB) => (messageA.sendTime || 0) - (messageB.sendTime || 0)
+      )
+      this.touchConversationMessageCache(clientConversationId)
+      return fresh
+    },
+
+    /** 纭繚浼氳瘽娑堟伅宸插姞杞� */
+    async ensureConversationMessageListLoaded(conversation: Conversation) {
+      const key = getMessageCacheKey(conversation.type, conversation.targetId)
+      if (this.messagesByConversation[key]) {
+        this.touchConversationMessageCache(key)
+        return
+      }
+      await this.loadMoreMessageList(key)
+    },
+
+    /** 鑾峰彇鍐呭瓨娑堟伅鏁扮粍 */
+    getMessageList(conversationType: number, targetId: number): Message[] {
+      const key = getMessageCacheKey(conversationType, targetId)
+      if (!this.messagesByConversation[key]) {
+        this.messagesByConversation[key] = []
+      }
+      this.touchConversationMessageCache(key)
+      return this.messagesByConversation[key]
+    },
+
+    /** 鎸佷箙鍖栨秷鎭褰� */
+    async saveMessageRecord(
+      message: Message,
+      conversationType: number,
+      tx?: DbTransaction,
+      options?: PersistMessageRecordOptions
+    ) {
+      const db = getDb()
+      const next = buildMessageDO(message, conversationType)
+      // 鏈嶅姟绔� key 鏇挎崲 client key
+      if (options?.mergeClientRecord && message.id && message.clientMessageId) {
+        const existing = await db.getByIndex<MessageDO>(
+          'messages',
+          'clientMessageId',
+          message.clientMessageId,
+          tx
+        )
+        if (existing && existing.messageKey !== next.messageKey) {
+          await db.delete('messages', existing.messageKey, tx)
+        }
+      }
+      await db.put('messages', next, tx)
+    },
+
+    /** 淇濆瓨娑堟伅娓告爣 */
+    async saveMessageCursor(conversationType: number, messageId?: number, tx?: DbTransaction) {
+      await setMessageMaxId(conversationType, messageId, tx)
+      this.updateMessageCursor(conversationType, messageId)
+    },
+
+    /** 搴旂敤鎾ゅ洖鍒板唴瀛� */
+    applyRecallMessageInMemory(
+      conversationType: number,
+      targetId: number,
+      recallSignalContent: string
+    ) {
+      // 1. 瀹氫綅琚挙鍥炵殑鍘熸秷鎭�
+      const messageId = parseRecallMessageId(recallSignalContent)
+      if (!messageId) {
+        return null
+      }
+      const conversationStore = useConversationStore()
+      const conversation = conversationStore.getConversation(conversationType, targetId)
+      if (!conversation) {
+        return null
+      }
+      const messages = this.getMessageList(conversationType, targetId)
+      const message = messages.find((item) => item.id === messageId)
+      if (!message) {
+        return null
+      }
+      // 2. 鏇存柊娑堟伅鍜屼細璇濇憳瑕�
+      message.type = ImContentType.RECALL
+      message.status = ImMessageStatus.RECALL
+      message.content = ''
+      if (messages[messages.length - 1]?.id === messageId) {
+        recomputeConversationLast(conversation, messages)
+      }
+      return { conversation, message }
+    },
+
+    /** 鎵归噺鍐欏叆鎷夊彇娑堟伅 */
+    async applyPulledMessageList(
+      pulledMessages: PulledMessage[],
+      conversationType: number,
+      maxMessageId?: number
+    ) {
+      if (pulledMessages.length === 0) {
+        // 1. 绌烘壒娆″彧鎺ㄨ繘娓告爣
+        await this.saveMessageCursor(conversationType, maxMessageId)
+        return
+      }
+      const conversationStore = useConversationStore()
+      const persistedMessages = new Map<
+        string,
+        { conversationType: number; mergeClientRecord?: boolean; message: Message; }
+      >()
+      const changedConversations = new Map<string, Conversation>()
+
+      const addChanged = (
+        conversation: Conversation,
+        message: Message,
+        options?: PersistMessageRecordOptions
+      ) => {
+        const clientConversationId = getClientConversationId(
+          conversation.type,
+          conversation.targetId
+        )
+        changedConversations.set(clientConversationId, conversation)
+        persistedMessages.set(getMessageKey(message, conversation.type), {
+          message,
+          conversationType: conversation.type,
+          mergeClientRecord: options?.mergeClientRecord
+        })
+      }
+
+      // 1. 鍏堟洿鏂板唴瀛橈紝鏀堕泦闇�瑕佹寔涔呭寲鐨勬秷鎭拰浼氳瘽
+      for (const pulledMessage of pulledMessages) {
+        if (pulledMessage.kind === 'recall') {
+          // 1.1 鎾ゅ洖淇″彿鏇存柊鍘熸秷鎭�
+          const changed = this.applyRecallMessageInMemory(
+            pulledMessage.conversationType,
+            pulledMessage.targetId,
+            pulledMessage.recallSignalContent
+          )
+          if (changed) {
+            addChanged(changed.conversation, changed.message)
+          }
+          continue
+        }
+
+        const { conversationInfo } = pulledMessage
+        const hasServerClientMessageId = !!pulledMessage.message.clientMessageId
+        const message = ensureClientMessageId(pulledMessage.message)
+        // 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 !== -1) {
+          const existing = messages[existingIndex]
+          if (!existing) {
+            continue
+          }
+          // 1.3 宸插瓨鍦ㄦ秷鎭悎骞舵湇鍔$鐘舵��
+          applyServerMessageUpdate(existing, message)
+          if (existingIndex === messages.length - 1) {
+            recomputeConversationLast(conversation, messages)
+            syncConversationAtFlags(conversation, message)
+          }
+          addChanged(conversation, existing, {
+            mergeClientRecord: hasServerClientMessageId
+          })
+          continue
+        }
+
+        // 1.4 鏂版秷鎭洿鏂颁細璇濇憳瑕佸拰鏈鐘舵��
+        applyConversationSummary(conversation, message)
+        syncConversationAtFlags(conversation, message)
+        const isActive =
+          conversationStore.activeConversation?.type === conversationInfo.type &&
+          conversationStore.activeConversation?.targetId === conversationInfo.targetId
+        if (
+          !message.selfSend &&
+          !isActive &&
+          !conversationStore.isMessageCoveredByReadPosition(conversation, message) &&
+          isNormalMessage(message.type) &&
+          message.status !== ImMessageStatus.RECALL
+        ) {
+          conversation.unreadCount++
+        }
+
+        // 1.5 鏂版秷鎭寜鏈嶅姟绔� id 鎻掑叆鍐呭瓨鍒楄〃
+        let insertIndex = messages.length
+        if (message.id) {
+          for (const [index, existing] of messages.entries()) {
+            if (existing.id && message.id < existing.id) {
+              insertIndex = index
+              break
+            }
+          }
+        }
+        messages.splice(insertIndex, 0, message)
+        addChanged(conversation, message, {
+          mergeClientRecord: hasServerClientMessageId && !!message.id
+        })
+      }
+
+      // 2. 鍗曚簨鍔″啓鍏ユ秷鎭�佷細璇濇憳瑕佸拰娓告爣
+      await getDb().transaction(
+        ['messages', 'conversations', 'settings'],
+        'readwrite',
+        async (tx) => {
+          // 2.1 鍐欏叆鏈壒鍙樻洿娑堟伅
+          for (const item of persistedMessages.values()) {
+            await this.saveMessageRecord(item.message, item.conversationType, tx, {
+              mergeClientRecord: item.mergeClientRecord
+            })
+          }
+          // 2.2 鍐欏叆鏈壒鍙樻洿浼氳瘽
+          await conversationStore.saveConversationRecord([...changedConversations.values()], tx)
+          // 2.3 鍐欏叆鏈壒娓告爣
+          await setMessageMaxId(conversationType, maxMessageId, tx)
+        }
+      )
+      // 3. 鎸佷箙鍖栨垚鍔熷悗鎺ㄨ繘鍐呭瓨娓告爣
+      this.updateMessageCursor(conversationType, maxMessageId)
+      for (const item of persistedMessages.values()) {
+        this.updateMessageCursor(item.conversationType, item.message.id)
+      }
+    },
+
+    /** 鎻掑叆娑堟伅 */
+    insertMessage(
+      conversationInfo: MessageConversationInfo,
+      messageInfo: Message,
+      options?: { saveMaxId?: boolean }
+    ): Promise<void> {
+      const conversationStore = useConversationStore()
+      const hasIncomingClientMessageId = !!messageInfo.clientMessageId
+      const message = ensureClientMessageId(messageInfo)
+      // 1. 鍏堝鐞嗘秷鎭甫鏉ョ殑缇よ祫鏂欏彉鏇�
+      if (conversationInfo.type === ImConversationType.GROUP && isGroupNotification(message.type)) {
+        useGroupStore().applyGroupNotification(
+          conversationInfo.targetId,
+          message.type,
+          message.content
+        )
+      }
+
+      // 2. 纭繚浼氳瘽鍜屾秷鎭紦瀛樺瓨鍦�
+      const conversation = conversationStore.ensureConversation(conversationInfo)
+      const messages = this.getMessageList(conversationInfo.type, conversationInfo.targetId)
+      const existingIndex = messages.findIndex((item) => isSameMessage(item, message))
+      // 3. 宸插瓨鍦ㄦ秷鎭蛋瑕嗙洊鏇存柊
+      if (existingIndex !== -1) {
+        const existing = messages[existingIndex]
+        if (!existing) {
+          return Promise.resolve()
+        }
+        applyServerMessageUpdate(existing, message)
+        if (existingIndex === messages.length - 1) {
+          recomputeConversationLast(conversation, messages)
+          syncConversationAtFlags(conversation, message)
+        }
+        return getDb()
+          .transaction(['messages', 'conversations', 'settings'], 'readwrite', async (tx) => {
+            await this.saveMessageRecord(existing, conversationInfo.type, tx, {
+              mergeClientRecord: hasIncomingClientMessageId
+            })
+            await conversationStore.saveConversationRecord(conversation, tx)
+            if (options?.saveMaxId !== false) {
+              await setMessageMaxId(conversationInfo.type, message.id, tx)
+            }
+          })
+          .catch((error) => {
+            console.error('[IM messageStore] 娑堟伅鍐欏叆澶辫触', error)
+            throw error
+          })
+          .then(() => {
+            this.updateMessageCursor(conversationInfo.type, message.id)
+          })
+      }
+
+      // 4. 鏂版秷鎭洿鏂颁細璇濇憳瑕佸拰鏈鐘舵��
+      applyConversationSummary(conversation, message)
+      syncConversationAtFlags(conversation, message)
+
+      const isActive =
+        conversationStore.activeConversation?.type === conversationInfo.type &&
+        conversationStore.activeConversation?.targetId === conversationInfo.targetId
+      if (
+        !message.selfSend &&
+        !isActive &&
+        !conversationStore.isMessageCoveredByReadPosition(conversation, message) &&
+        isNormalMessage(message.type) &&
+        message.status !== ImMessageStatus.RECALL
+      ) {
+        conversation.unreadCount++
+      }
+
+      // 5. 鏂版秷鎭寜 id 鎻掑叆鍒板唴瀛樻暟缁�
+      let insertIndex = messages.length
+      if (message.id) {
+        for (const [index, existing] of messages.entries()) {
+          if (existing.id && message.id < existing.id) {
+            insertIndex = index
+            break
+          }
+        }
+      }
+      messages.splice(insertIndex, 0, message)
+      // 6. 鍗曚簨鍔″啓鍏ユ秷鎭�佷細璇濇憳瑕佸拰娓告爣
+      return getDb()
+        .transaction(['messages', 'conversations', 'settings'], 'readwrite', async (tx) => {
+          await this.saveMessageRecord(message, conversationInfo.type, tx, {
+            mergeClientRecord: hasIncomingClientMessageId && !!message.id
+          })
+          await conversationStore.saveConversationRecord(conversation, tx)
+          if (options?.saveMaxId !== false) {
+            await setMessageMaxId(conversationInfo.type, message.id, tx)
+          }
+        })
+        .catch((error) => {
+          console.error('[IM messageStore] 娑堟伅鍐欏叆澶辫触', error)
+          throw error
+        })
+        .then(() => {
+          this.updateMessageCursor(conversationInfo.type, message.id)
+        })
+    },
+
+    /** ack 鍚堝苟 */
+    ackMessage(
+      conversationType: number,
+      targetId: number,
+      clientMessageId: string,
+      updates: Partial<Message>
+    ) {
+      const mergeKey = `${conversationType}:${targetId}:${clientMessageId}`
+      const existingPromise = ackMergingPromises.get(mergeKey)
+      if (existingPromise) {
+        return existingPromise
+      }
+      const promise = this.doAckMessage(
+        conversationType,
+        targetId,
+        clientMessageId,
+        updates
+      ).finally(() => {
+        ackMergingPromises.delete(mergeKey)
+      })
+      ackMergingPromises.set(mergeKey, promise)
+      return promise
+    },
+
+    /** 鎵ц ack 鍚堝苟 */
+    async doAckMessage(
+      conversationType: number,
+      targetId: number,
+      clientMessageId: string,
+      updates: Partial<Message>
+    ) {
+      // 1. 瀹氫綅寰呭悎骞舵秷鎭�
+      const conversationStore = useConversationStore()
+      const conversation = conversationStore.getConversation(conversationType, targetId)
+      if (!conversation) {
+        return
+      }
+      const messages = this.getMessageList(conversationType, targetId)
+      const message = messages.find((item) => item.clientMessageId === clientMessageId)
+      if (!message) {
+        return
+      }
+      message._ackMerging = true
+      try {
+        // 2. 鍚堝苟鏈嶅姟绔� ack 鍒板唴瀛�
+        applyServerMessageUpdate(message, updates)
+        if (messages[messages.length - 1] === message) {
+          recomputeConversationLast(conversation, messages)
+        }
+        // 3. 鍗曚簨鍔″啓鍏ユ秷鎭�佷細璇濇憳瑕佸拰娓告爣
+        await getDb()
+          .transaction(['messages', 'conversations', 'settings'], 'readwrite', async (tx) => {
+            await this.saveMessageRecord(message, conversationType, tx, {
+              mergeClientRecord: true
+            })
+            await conversationStore.saveConversationRecord(conversation, tx)
+            await setMessageMaxId(conversationType, message.id, tx)
+          })
+          .catch((error) => {
+            console.error('[IM messageStore] ack 鍐欏叆澶辫触', error)
+            throw error
+          })
+        this.updateMessageCursor(conversationType, message.id)
+      } finally {
+        // 4. 娓呯悊鍚堝苟鏍囪
+        message._ackMerging = false
+      }
+    },
+
+    /** 灞�閮ㄦ洿鏂版秷鎭� */
+    patchMessage(
+      conversationType: number,
+      targetId: number,
+      clientMessageId: string,
+      patch: Partial<Message>
+    ) {
+      const message = this.getMessageList(conversationType, targetId).find(
+        (item) => item.clientMessageId === clientMessageId
+      )
+      if (!message) {
+        return
+      }
+      let changed = false
+      for (const key in patch) {
+        if (
+          Object.prototype.hasOwnProperty.call(patch, key) &&
+          (patch as Record<string, unknown>)[key] !==
+            (message as unknown as Record<string, unknown>)[key]
+        ) {
+          changed = true
+          break
+        }
+      }
+      if (changed) {
+        applyServerMessageUpdate(message, patch)
+      }
+    },
+
+    /** 鎾ゅ洖娑堟伅 */
+    async recallMessage(
+      conversationType: number,
+      targetId: number,
+      recallSignalContent: string
+    ): Promise<void> {
+      const conversationStore = useConversationStore()
+      const changed = this.applyRecallMessageInMemory(
+        conversationType,
+        targetId,
+        recallSignalContent
+      )
+      if (!changed) {
+        return
+      }
+      await getDb()
+        .transaction(['messages', 'conversations'], 'readwrite', async (tx) => {
+          await this.saveMessageRecord(changed.message, conversationType, tx)
+          await conversationStore.saveConversationRecord(changed.conversation, tx)
+        })
+        .catch((error) => {
+          console.error('[IM messageStore] 鎾ゅ洖娑堟伅鍐欏叆澶辫触', error)
+          throw error
+        })
+    },
+
+    /** 搴旂敤宸茶鍥炴墽 */
+    applyMessageReadReceipt(options: {
+      conversationType: number
+      groupMessageId?: number
+      privateReadMaxId?: number
+      readCount?: number
+      receiptStatus?: number
+      targetId: number
+    }) {
+      const messages = this.getMessageList(options.conversationType, options.targetId)
+      const changed: Message[] = []
+      // 1. 绉佽亰鍥炴墽鎵归噺鏇存柊鑷繁鍙戦�佺殑娑堟伅
+      if (options.conversationType === ImConversationType.PRIVATE && options.privateReadMaxId) {
+        this.updatePrivateReadMaxId(options.targetId, options.privateReadMaxId)
+        const privateReadMaxId = options.privateReadMaxId
+        messages.forEach((message) => {
+          if (
+            message.selfSend &&
+            message.id &&
+            message.id <= privateReadMaxId &&
+            message.receiptStatus === ImMessageReceiptStatus.PENDING
+          ) {
+            message.receiptStatus = ImMessageReceiptStatus.DONE
+            changed.push(message)
+          }
+        })
+      } else if (options.conversationType === ImConversationType.GROUP && options.groupMessageId) {
+        // 2. 缇よ亰鍥炴墽鏇存柊鍗曟潯娑堟伅
+        const message = messages.find((item) => item.id === options.groupMessageId)
+        if (message) {
+          if (options.readCount !== undefined) {
+            message.readCount = options.readCount
+          }
+          if (options.receiptStatus !== undefined) {
+            message.receiptStatus = options.receiptStatus
+          }
+          changed.push(message)
+        }
+      }
+      if (changed.length === 0) {
+        return
+      }
+      // 3. 鍗曚簨鍔″啓鍏ュ彉鏇存秷鎭�
+      void getDb()
+        .transaction(['messages'], 'readwrite', async (tx) => {
+          for (const message of changed) {
+            await this.saveMessageRecord(message, options.conversationType, tx)
+          }
+        })
+        .catch((error) => console.warn('[IM messageStore] 鍥炴墽鍐欏叆澶辫触', error))
+    },
+
+    /** 鍓嶇疆鍘嗗彶娑堟伅 */
+    prependMessageList(conversationType: number, targetId: number, earlierMessages: Message[]) {
+      if (earlierMessages.length === 0) {
+        return
+      }
+      const messages = this.getMessageList(conversationType, targetId)
+      const existingIds = new Set(messages.map((message) => message.id).filter(Boolean))
+      const fresh = earlierMessages
+        .map((message) => ensureClientMessageId(message))
+        .filter((message) => message.id && !existingIds.has(message.id))
+        .toSorted((messageA, messageB) => (messageA.id || 0) - (messageB.id || 0))
+      if (fresh.length === 0) {
+        return
+      }
+      const key = getMessageCacheKey(conversationType, targetId)
+      this.messagesByConversation[key] = [...fresh, ...messages]
+      void getDb()
+        .transaction(['messages'], 'readwrite', async (tx) => {
+          for (const message of fresh) {
+            await this.saveMessageRecord(message, conversationType, tx)
+          }
+        })
+        .catch((error) => console.warn('[IM messageStore] 鍘嗗彶娑堟伅鍐欏叆澶辫触', error))
+    },
+
+    /** 鍒犻櫎鍗曟潯娑堟伅 */
+    removeMessage(
+      conversationType: number,
+      targetId: number,
+      key: { clientMessageId?: string; id?: number; }
+    ) {
+      // 1. 瀹氫綅浼氳瘽鍜屾秷鎭�
+      const conversationStore = useConversationStore()
+      const conversation = conversationStore.getConversation(conversationType, targetId)
+      if (!conversation) {
+        return
+      }
+      const messages = this.getMessageList(conversationType, targetId)
+      const index = messages.findIndex((message) => {
+        if (key.id && message.id && message.id === key.id) {
+          return true
+        }
+        return !!key.clientMessageId && message.clientMessageId === key.clientMessageId
+      })
+      if (index === -1) {
+        return
+      }
+      // 2. 浠庡唴瀛樼Щ闄ゆ秷鎭�
+      const [removed] = messages.splice(index, 1)
+      if (!removed) {
+        return
+      }
+      revokeBlobUrlsInContent(removed.content)
+      if (index === messages.length) {
+        recomputeConversationLast(conversation, messages)
+      }
+      // 3. 鍒犻櫎鏈湴璁板綍骞朵繚瀛樹細璇濇憳瑕�
+      getDb()
+        .delete('messages', getMessageKey(removed, conversationType))
+        .catch((error) => console.warn('[IM messageStore] 娑堟伅鍒犻櫎澶辫触', error))
+      conversationStore.saveConversation(conversation)
+    },
+
+    /** 鍒犻櫎浼氳瘽鍏ㄩ儴娑堟伅 */
+    deleteConversationMessageList(conversationType: number, targetId: number) {
+      // 1. 娓呯悊鍐呭瓨娑堟伅鍜屽獟浣撹祫婧�
+      const clientConversationId = getClientConversationId(conversationType, targetId)
+      const messages = this.messagesByConversation[clientConversationId] || []
+      messages.forEach((message) => {
+        revokeBlobUrlsInContent(message.content)
+        message._localFile = undefined
+      })
+      Reflect.deleteProperty(this.messagesByConversation, clientConversationId)
+      this.loadedConversationKeys = this.loadedConversationKeys.filter(
+        (key) => key !== clientConversationId
+      )
+      // 2. 鍒犻櫎 IndexedDB 娑堟伅
+      getDb()
+        .deleteByIndex('messages', 'clientConversationId', clientConversationId)
+        .catch((error) => console.warn('[IM messageStore] 浼氳瘽娑堟伅鍒犻櫎澶辫触', error))
+    }
+  }
+})
+
+export const useMessageStoreWithOut = () => useMessageStore()
+
+if (import.meta.hot) {
+  import.meta.hot.accept(acceptHMRUpdate(useMessageStore, import.meta.hot))
+}

--
Gitblit v1.9.3