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/utils/db.ts |  541 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 541 insertions(+), 0 deletions(-)

diff --git a/src/views/im/utils/db.ts b/src/views/im/utils/db.ts
new file mode 100644
index 0000000..aabf4e9
--- /dev/null
+++ b/src/views/im/utils/db.ts
@@ -0,0 +1,541 @@
+import type { MessageDO, SettingDO } from '../home/types'
+
+import { toRaw } from 'vue'
+
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import { ImConversationType } from './constants'
+
+export const DB_SCHEMA_VERSION = 2
+
+export type DbStoreName =
+  | 'channels'
+  | 'conversationReads'
+  | 'conversations'
+  | 'friendRequests'
+  | 'friends'
+  | 'groupMembers'
+  | 'groupRequests'
+  | 'groups'
+  | 'messages'
+  | 'settings'
+
+export type DbTransaction = IDBTransaction
+
+/** IM 鏈湴瀛樺偍 key */
+export const StorageKeys = {
+  localStorage: {
+    /** 渚ц竟鏍忓搴︼紝涓変釜 Tab 鍏辩敤涓�浠借蹇� */
+    asideWidth: 'im:aside',
+    /** 浼氳瘽鍒楄〃缃《鎶樺彔灞曞紑鎬� */
+    conversationPinnedExpanded: 'im:conversation:pinnedExpanded'
+  },
+  settings: {
+    /** 绉佽亰娑堟伅鎷夊彇娓告爣 */
+    privateMessageMaxId: 'privateMessageMaxId',
+    /** 缇よ亰娑堟伅鎷夊彇娓告爣 */
+    groupMessageMaxId: 'groupMessageMaxId',
+    /** 棰戦亾娑堟伅鎷夊彇娓告爣 */
+    channelMessageMaxId: 'channelMessageMaxId',
+    /** 鏈�杩戣浆鍙戜細璇� key 鍒楄〃 */
+    recentForwardConversationKeys: 'recentForwardConversationKeys',
+    // 鐘舵�佷簨浠惰ˉ鍋垮閲忔媺鍙栨父鏍囷細涓庝笂闈㈡秷鎭� maxId 娓告爣鍏辩敤鍚屼竴 settings keyspace锛岀粺涓�鐧昏鍦ㄦ閬垮厤鎾� key锛�
+    // 璧� update_time + id 澶嶅悎娓告爣锛堥潪鍗曟潯 maxId锛夛紝鏁呯敤 PullCursor 鍚庣紑鍖哄垎璇箟
+    /** 濂藉弸鍏崇郴澧為噺鎷夊彇娓告爣 */
+    friendPullCursor: 'friendPullCursor',
+    /** 濂藉弸鐢宠澧為噺鎷夊彇娓告爣 */
+    friendRequestPullCursor: 'friendRequestPullCursor',
+    /** 鍔犵兢鐢宠澧為噺鎷夊彇娓告爣 */
+    groupRequestPullCursor: 'groupRequestPullCursor',
+    /** 浼氳瘽璇讳綅缃閲忔媺鍙栨父鏍� */
+    conversationReadPullCursor: 'conversationReadPullCursor'
+  }
+} as const
+
+let currentDb: IDBDatabase | null = null
+let currentUserId: null | number = null
+let currentSession = 0
+
+/** 鏍¢獙褰撳墠 IM IndexedDB session 浠嶆湁鏁� */
+export function isCurrentDbSession(session: number): boolean {
+  return session === currentSession
+}
+
+/** 鑾峰彇褰撳墠 IM IndexedDB session */
+export function getDbSession(): number {
+  return currentSession
+}
+
+/** 鎷兼帴褰撳墠鐢ㄦ埛 IM DB 鍚嶇О */
+function getDbName(userId: number): string {
+  return `im:${userId}`
+}
+
+/** 鍖呰 IndexedDB request */
+function requestToPromise<T = unknown>(request: IDBRequest<T>): Promise<T> {
+  return new Promise((resolve, reject) => {
+    request.addEventListener('success', () => resolve(request.result))
+    request.addEventListener('error', () => reject(request.error))
+  })
+}
+
+/** 绛夊緟浜嬪姟瀹屾垚 */
+function transactionDone(transaction: DbTransaction): Promise<void> {
+  return new Promise((resolve, reject) => {
+    transaction.addEventListener('complete', () => resolve())
+    transaction.addEventListener('error', () => reject(transaction.error))
+    transaction.addEventListener('abort', () => reject(transaction.error))
+  })
+}
+
+/** 鍒涘缓绱㈠紩 */
+function createIndex(
+  store: IDBObjectStore,
+  name: string,
+  keyPath: string | string[],
+  options?: IDBIndexParameters
+) {
+  if (!store.indexNames.contains(name)) {
+    store.createIndex(name, keyPath, options)
+  }
+}
+
+/** 鍒濆鍖� schema */
+function upgradeSchema(db: IDBDatabase) {
+  if (!db.objectStoreNames.contains('conversations')) {
+    const store = db.createObjectStore('conversations', { keyPath: 'clientConversationId' })
+    createIndex(store, 'lastSendTime', 'lastSendTime')
+  }
+  if (!db.objectStoreNames.contains('conversationReads')) {
+    const store = db.createObjectStore('conversationReads', { keyPath: 'clientConversationId' })
+    createIndex(store, 'conversationType+targetId', ['conversationType', 'targetId'], {
+      unique: true
+    })
+  }
+  if (!db.objectStoreNames.contains('messages')) {
+    const store = db.createObjectStore('messages', { keyPath: 'messageKey' })
+    createIndex(store, 'clientConversationId', 'clientConversationId')
+    createIndex(store, 'clientConversationId+sendTime', ['clientConversationId', 'sendTime'])
+    createIndex(store, 'clientMessageId', 'clientMessageId', { unique: true })
+  }
+  if (!db.objectStoreNames.contains('friends')) {
+    const store = db.createObjectStore('friends', { keyPath: 'id' })
+    createIndex(store, 'friendUserId', 'friendUserId', { unique: true })
+    createIndex(store, 'status', 'status')
+  }
+  if (!db.objectStoreNames.contains('friendRequests')) {
+    const store = db.createObjectStore('friendRequests', { keyPath: 'id' })
+    createIndex(store, 'status', 'status')
+    createIndex(store, 'createTime', 'createTime')
+  }
+  if (!db.objectStoreNames.contains('groups')) {
+    const store = db.createObjectStore('groups', { keyPath: 'id' })
+    createIndex(store, 'name', 'name')
+    createIndex(store, 'status', 'status')
+  }
+  if (!db.objectStoreNames.contains('groupMembers')) {
+    const store = db.createObjectStore('groupMembers', { keyPath: 'id' })
+    createIndex(store, 'groupId', 'groupId')
+    createIndex(store, 'groupId+userId', ['groupId', 'userId'], { unique: true })
+  }
+  if (!db.objectStoreNames.contains('groupRequests')) {
+    const store = db.createObjectStore('groupRequests', { keyPath: 'id' })
+    createIndex(store, 'status', 'status')
+    createIndex(store, 'createTime', 'createTime')
+  }
+  if (!db.objectStoreNames.contains('channels')) {
+    const store = db.createObjectStore('channels', { keyPath: 'id' })
+    createIndex(store, 'status', 'status')
+    createIndex(store, 'sort', 'sort')
+  }
+  if (!db.objectStoreNames.contains('settings')) {
+    db.createObjectStore('settings', { keyPath: 'key' })
+  }
+}
+
+/** 鎵撳紑 IM IndexedDB */
+function openDb(name: string): Promise<IDBDatabase> {
+  return new Promise((resolve, reject) => {
+    const request = indexedDB.open(name, DB_SCHEMA_VERSION)
+    // 鍒涘缓鎴栧崌绾у璞′粨搴�
+    request.addEventListener('upgradeneeded', () => upgradeSchema(request.result))
+    // 杩斿洖鍙鐢ㄨ繛鎺�
+    request.addEventListener('success', () => resolve(request.result))
+    request.addEventListener('error', () => reject(request.error))
+  })
+}
+
+/** 鍒濆鍖栧綋鍓嶇敤鎴� IM DB */
+export async function initDb(): Promise<void> {
+  const userId = getCurrentUserId()
+  if (!Number.isFinite(userId) || userId <= 0) {
+    throw new Error('褰撳墠鐢ㄦ埛涓嶅瓨鍦紝鏃犳硶鍒濆鍖� IM DB')
+  }
+  if (currentDb && currentUserId === userId) {
+    return
+  }
+  currentDb?.close()
+  currentSession++
+  currentUserId = userId
+  currentDb = await openDb(getDbName(userId))
+}
+
+/** 鍏抽棴褰撳墠 IM DB 杩炴帴 */
+function closeDbConnection() {
+  currentDb?.close()
+  currentDb = null
+  currentUserId = null
+}
+
+/** 鑾峰彇褰撳墠 IM DB */
+function getRawDb(): IDBDatabase {
+  if (!currentDb) {
+    throw new Error('IM DB 鏈垵濮嬪寲')
+  }
+  return currentDb
+}
+
+/** 鏍¢獙鍗曟鍐欏叆 session */
+function guardSession(session: number) {
+  if (!isCurrentDbSession(session)) {
+    throw new Error('IM DB session 宸插け鏁�')
+  }
+}
+
+/** 鍏嬮殕鍙叆搴撳璞� */
+function toDbValue<T>(value: T): T {
+  return cloneDbValue(value) as T
+}
+
+/** 杞崲涓� IndexedDB 鍙厠闅嗗璞� */
+function cloneDbValue(value: unknown): unknown {
+  const raw = toRaw(value)
+  if (Array.isArray(raw)) {
+    return raw.map((item) => cloneDbValue(item))
+  }
+  if (!raw || typeof raw !== 'object') {
+    return raw
+  }
+  const prototype = Object.getPrototypeOf(raw)
+  if (prototype !== Object.prototype && prototype !== null) {
+    return raw
+  }
+  return Object.fromEntries(
+    Object.entries(raw as Record<string, unknown>).map(([key, item]) => [key, cloneDbValue(item)])
+  )
+}
+
+class DbClient {
+  /** 娓呯┖ store 璁板綍 */
+  async clearStore(storeName: DbStoreName, tx?: DbTransaction): Promise<void> {
+    if (tx) {
+      await requestToPromise(tx.objectStore(storeName).clear())
+      return
+    }
+    await this.transaction([storeName], 'readwrite', (tx) => this.clearStore(storeName, tx))
+  }
+
+  /** 鍒犻櫎璁板綍 */
+  async delete(storeName: DbStoreName, key: IDBValidKey, tx?: DbTransaction): Promise<void> {
+    if (tx) {
+      await requestToPromise(tx.objectStore(storeName).delete(key))
+      return
+    }
+    await this.transaction([storeName], 'readwrite', (tx) => this.delete(storeName, key, tx))
+  }
+
+  /** 鎸夌储寮曞垹闄よ褰� */
+  async deleteByIndex(
+    storeName: DbStoreName,
+    indexName: string,
+    query: IDBKeyRange | IDBValidKey,
+    tx?: DbTransaction
+  ): Promise<void> {
+    if (!tx) {
+      await this.transaction([storeName], 'readwrite', (tx) =>
+        this.deleteByIndex(storeName, indexName, query, tx)
+      )
+      return
+    }
+    const index = tx.objectStore(storeName).index(indexName)
+    await new Promise<void>((resolve, reject) => {
+      const request = index.openCursor(query)
+      request.addEventListener('error', () => reject(request.error))
+      request.addEventListener('success', () => {
+        const cursor = request.result
+        if (!cursor) {
+          resolve()
+          return
+        }
+        cursor.delete()
+        cursor.continue()
+      })
+    })
+  }
+
+  /** 鑾峰彇鍗曟潯璁板綍 */
+  async get<T>(
+    storeName: DbStoreName,
+    key: IDBValidKey,
+    tx?: DbTransaction
+  ): Promise<T | undefined> {
+    if (tx) {
+      return requestToPromise<T | undefined>(tx.objectStore(storeName).get(key))
+    }
+    return this.transaction<T | undefined>([storeName], 'readonly', (tx) =>
+      this.get<T>(storeName, key, tx)
+    )
+  }
+
+  /** 鑾峰彇 store 鍏ㄩ噺璁板綍 */
+  async getAll<T>(storeName: DbStoreName, tx?: DbTransaction): Promise<T[]> {
+    if (tx) {
+      return requestToPromise<T[]>(tx.objectStore(storeName).getAll())
+    }
+    return this.transaction<T[]>([storeName], 'readonly', (tx) => this.getAll<T>(storeName, tx))
+  }
+
+  /** 鎸夌储寮曡幏鍙栬褰曞垪琛� */
+  async getAllByIndex<T>(
+    storeName: DbStoreName,
+    indexName: string,
+    query?: IDBKeyRange | IDBValidKey,
+    tx?: DbTransaction
+  ): Promise<T[]> {
+    if (tx) {
+      return requestToPromise<T[]>(tx.objectStore(storeName).index(indexName).getAll(query))
+    }
+    return this.transaction<T[]>([storeName], 'readonly', (tx) =>
+      this.getAllByIndex<T>(storeName, indexName, query, tx)
+    )
+  }
+
+  /** 鎸夊敮涓�绱㈠紩鑾峰彇鍗曟潯璁板綍 */
+  async getByIndex<T>(
+    storeName: DbStoreName,
+    indexName: string,
+    query: IDBKeyRange | IDBValidKey,
+    tx?: DbTransaction
+  ): Promise<T | undefined> {
+    if (tx) {
+      return requestToPromise<T | undefined>(tx.objectStore(storeName).index(indexName).get(query))
+    }
+    return this.transaction<T | undefined>([storeName], 'readonly', (tx) =>
+      this.getByIndex<T>(storeName, indexName, query, tx)
+    )
+  }
+
+  /** 鎸変細璇濆垎椤佃幏鍙栨秷鎭� */
+  async getMessageListByConversation(
+    clientConversationId: string,
+    options?: { beforeSendTime?: number; limit?: number },
+    tx?: DbTransaction
+  ): Promise<MessageDO[]> {
+    const limit = options?.limit ?? 50
+    const upper = options?.beforeSendTime ?? Number.MAX_SAFE_INTEGER
+    const range = IDBKeyRange.bound(
+      [clientConversationId, 0],
+      [clientConversationId, upper],
+      false,
+      true
+    )
+    const read = async (tx: DbTransaction): Promise<MessageDO[]> => {
+      const index = tx.objectStore('messages').index('clientConversationId+sendTime')
+      const out: MessageDO[] = []
+      await new Promise<void>((resolve, reject) => {
+        // 浠庢柊鍒版棫璇诲彇涓�椤�
+        const request = index.openCursor(range, 'prev')
+        request.addEventListener('error', () => reject(request.error))
+        request.addEventListener('success', () => {
+          const cursor = request.result
+          if (!cursor || out.length >= limit) {
+            resolve()
+            return
+          }
+          out.push(cursor.value as MessageDO)
+          cursor.continue()
+        })
+      })
+      // 姘旀场娓叉煋闇�瑕佹寜鏃堕棿鍗囧簭
+      return out.toReversed()
+    }
+    if (tx) {
+      return read(tx)
+    }
+    return this.transaction<MessageDO[]>(['messages'], 'readonly', read)
+  }
+
+  /** 璇诲彇璁剧疆 */
+  async getSetting<T>(key: string, tx?: DbTransaction): Promise<T | undefined> {
+    const item = await this.get<SettingDO<T>>('settings', key, tx)
+    return item?.value
+  }
+
+  /** 鍐欏叆璁板綍 */
+  async put<T>(storeName: DbStoreName, value: T, tx?: DbTransaction): Promise<void> {
+    if (tx) {
+      await requestToPromise(tx.objectStore(storeName).put(toDbValue(value)))
+      return
+    }
+    await this.transaction([storeName], 'readwrite', (tx) => this.put(storeName, value, tx))
+  }
+
+  /** 鍐欏叆璁剧疆 */
+  async setSetting<T>(key: string, value: T, tx?: DbTransaction): Promise<void> {
+    await this.put<SettingDO<T>>('settings', { key, value, updateTime: Date.now() }, tx)
+  }
+
+  /** 鎵ц浜嬪姟 */
+  async transaction<T>(
+    storeNames: DbStoreName[],
+    mode: IDBTransactionMode,
+    runner: (tx: DbTransaction) => Promise<T>
+  ): Promise<T> {
+    // 寮�鍚簨鍔″墠鏍¢獙 session
+    const session = getDbSession()
+    guardSession(session)
+    const tx = getRawDb().transaction(storeNames, mode)
+    const done = transactionDone(tx)
+    let result: T
+    try {
+      // 浜嬪姟鍐呭彧鎵ц IndexedDB request 閾�
+      result = await runner(tx)
+    } catch (error) {
+      try {
+        tx.abort()
+      } catch {}
+      await done.catch(() => undefined)
+      throw error
+    }
+    // commit 鍚庡啀娆℃牎楠� session
+    await done
+    guardSession(session)
+    return result
+  }
+}
+
+const dbClient = new DbClient()
+
+/** 鑾峰彇褰撳墠 IM DB client */
+export function getDb(): DbClient {
+  return dbClient
+}
+
+/** 褰撳墠鐢ㄦ埛浼氳瘽涓婚敭 */
+export function getClientConversationId(type: number, targetId: number): string {
+  return `${type}:${targetId}`
+}
+
+/** 瑙f瀽褰撳墠鐢ㄦ埛浼氳瘽涓婚敭 */
+export function parseClientConversationId(
+  clientConversationId: string
+): null | { targetId: number; type: number; } {
+  const [typeText, targetIdText] = clientConversationId.split(':')
+  const type = Number(typeText)
+  const targetId = Number(targetIdText)
+  if (!Number.isFinite(type) || !Number.isFinite(targetId) || targetId <= 0) {
+    return null
+  }
+  return { type, targetId }
+}
+
+/** 鏈嶅姟绔秷鎭富閿� */
+export function getServerMessageKey(conversationType: number, id: number): string {
+  return `${conversationType}:${id}`
+}
+
+/** 瀹㈡埛绔复鏃舵秷鎭富閿� */
+export function getClientMessageKey(clientMessageId: string): string {
+  return `client:${clientMessageId}`
+}
+
+/** 瑙f瀽鏈湴娑堟伅涓婚敭 */
+export function parseMessageKey(
+  messageKey: string
+):
+  | null
+  | { clientMessageId: string; kind: 'client'; }
+  | { conversationType: number; id: number; kind: 'server'; } {
+  if (!messageKey) {
+    return null
+  }
+  if (messageKey.startsWith('client:')) {
+    const clientMessageId = messageKey.slice('client:'.length)
+    return clientMessageId ? { kind: 'client', clientMessageId } : null
+  }
+  const [conversationTypeText, idText] = messageKey.split(':')
+  const conversationType = Number(conversationTypeText)
+  const id = Number(idText)
+  if (!Number.isFinite(conversationType) || !Number.isFinite(id) || id <= 0) {
+    return null
+  }
+  return { kind: 'server', conversationType, id }
+}
+
+/** 鏇存柊娑堟伅鎷夊彇娓告爣 */
+export async function setMessageMaxId(
+  conversationType: number,
+  maxId: number | undefined,
+  tx?: DbTransaction
+): Promise<void> {
+  if (!maxId) {
+    return
+  }
+  let key: string
+  switch (conversationType) {
+    case ImConversationType.CHANNEL: {
+      key = StorageKeys.settings.channelMessageMaxId
+      break
+    }
+    case ImConversationType.GROUP: {
+      key = StorageKeys.settings.groupMessageMaxId
+      break
+    }
+    case ImConversationType.PRIVATE: {
+      key = StorageKeys.settings.privateMessageMaxId
+      break
+    }
+    default: {
+      throw new Error(`鏈煡 IM 浼氳瘽绫诲瀷锛�${conversationType}`)
+    }
+  }
+  const db = getDb()
+  const current = (await db.getSetting<number>(key, tx)) || 0
+  if (maxId > current) {
+    await db.setSetting(key, maxId, tx)
+  }
+}
+
+/** 鍋滄褰撳墠 IM DB session */
+export async function stopRequests(): Promise<void> {
+  currentSession++
+  const [
+    { useMessageStoreWithOut },
+    { useConversationStoreWithOut },
+    { useFriendStoreWithOut },
+    { useGroupStoreWithOut },
+    { useChannelStoreWithOut },
+    { useGroupRequestStoreWithOut },
+    { useFaceStoreWithOut },
+    { useRtcStore }
+  ] = await Promise.all([
+    import('../home/store/messageStore'),
+    import('../home/store/conversationStore'),
+    import('../home/store/friendStore'),
+    import('../home/store/groupStore'),
+    import('../home/store/channelStore'),
+    import('../home/store/groupRequestStore'),
+    import('../home/store/faceStore'),
+    import('../home/store/rtcStore')
+  ])
+  useMessageStoreWithOut().clear()
+  useConversationStoreWithOut().clear()
+  useFriendStoreWithOut().clear()
+  useGroupStoreWithOut().clear()
+  useChannelStoreWithOut().clear()
+  useGroupRequestStoreWithOut().clear()
+  useFaceStoreWithOut().clear()
+  useRtcStore().reset()
+  useRtcStore().clearGroupCallCache()
+  closeDbConnection()
+}

--
Gitblit v1.9.3