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