/** * 数据库工具类 - 使用serverId作为主键的优化架构 * * 架构设计: * 1. 使用serverId作为数据库主键,直接对应接口返回的id字段 * 2. 保留原始的id字段,用于存储接口数据的完整性 * 3. 添加userId字段实现多用户数据隔离 * 4. 统一会话表和联系人表,兼容好友和群聊 * * 优势: * - 直接使用服务器ID作为主键,避免ID冲突 * - 通过userId实现多用户数据隔离 * - 统一的会话和联系人表结构,兼容好友和群聊 * - 支持复合索引,提高查询性能 * - 支持用户登录记录和自动清理 */ import Dexie, { Table } from "dexie"; import { getPersistedData, PERSIST_KEYS } from "@/store/persistUtils"; const DB_NAME_PREFIX = "CunkebaoDatabase"; // ==================== 用户登录记录 ==================== export interface UserLoginRecord { serverId: string; // 主键: user_${userId} userId: number; // 用户ID lastLoginTime: string; // 最后登录时间 loginCount: number; // 登录次数 createTime: string; // 首次登录时间 lastActiveTime: string; // 最后活跃时间 } // ==================== 统一会话表(兼容好友和群聊) ==================== export interface ChatSession { serverId: string; // 主键 userId: number; // 用户ID(数据隔离) id: number; // 原始ID type: "friend" | "group"; // 类型:好友或群聊 // 通用字段 wechatAccountId: number; // 所属客服账号 nickname: string; // 显示名称 conRemark?: string; // 备注名 avatar: string; // 头像 content: string; // 最新消息内容 lastUpdateTime: string; // 最后更新时间 aiType?: number; // AI类型(0=普通,1=AI辅助) config: { unreadCount: number; // 未读数量 top: number; // 是否置顶(1=置顶,0=非置顶) }; sortKey: string; // 预计算排序键 // 好友特有字段(type='friend'时有效) wechatFriendId?: number; // 好友ID wechatId?: string; // 微信号 alias?: string; // 别名 // 群聊特有字段(type='group'时有效) chatroomId?: string; // 群聊ID chatroomOwner?: string; // 群主 selfDisplayName?: string; // 群内昵称 notice?: string; // 群公告 phone?: string; // 联系人电话 region?: string; // 联系人地区 extendFields?: string; // 扩展字段(JSON 字符串) } // ==================== 统一联系人表(兼容好友和群聊) ==================== export interface Contact { serverId: string; // 主键 userId: number; // 用户ID(数据隔离) id: number; // 原始ID type: "friend" | "group"; // 类型:好友或群聊 // 通用字段 wechatAccountId: number; // 所属客服账号 nickname: string; // 显示名称 conRemark?: string; // 备注名 avatar: string; // 头像 lastUpdateTime: string; // 最后更新时间 aiType?: number; // AI类型(0=普通,1=AI辅助) config?: any; // 配置信息 sortKey: string; // 预计算排序键 searchKey: string; // 预计算搜索键 // 好友特有字段(type='friend'时有效) wechatFriendId?: number; // 好友ID wechatId?: string; // 微信号 alias?: string; // 别名 gender?: number; // 性别 groupId?: number; // 标签ID(分组) region?: string; // 地区 signature?: string; // 个性签名 phone?: string; // 手机号 quanPin?: string; // 全拼 extendFields?: string; // 扩展字段(JSON 字符串) // 群聊特有字段(type='group'时有效) chatroomId?: string; // 群聊ID chatroomOwner?: string; // 群主 selfDisplayName?: string; // 群内昵称 notice?: string; // 群公告 memberCount?: number; // 群成员数量 } // ==================== 联系人标签映射表 ==================== export interface ContactLabelMap { serverId: string; // 主键: ${contactId}_${labelId} userId: number; // 用户ID(数据隔离) labelId: number; // 标签ID contactId: number; // 联系人ID contactType: "friend" | "group"; // 联系人类型 sortKey: string; // 预计算排序键 searchKey: string; // 预计算搜索键 // 列表展示必需字段(轻量) avatar: string; nickname: string; conRemark?: string; unreadCount: number; lastUpdateTime: string; } // 数据库类 class CunkebaoDatabase extends Dexie { // ==================== 统一表结构 ==================== chatSessions!: Table; // 统一会话表 contactsUnified!: Table; // 统一联系人表 contactLabelMap!: Table; // 联系人标签映射表 userLoginRecords!: Table; // 用户登录记录表 constructor(dbName: string) { super(dbName); this.version(1).stores({ // 会话表索引:支持按用户、类型、时间、置顶等查询 chatSessions: "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], [userId+aiType], sortKey, nickname, conRemark, avatar, content, lastUpdateTime, aiType, phone, region", // 联系人表索引:支持按用户、类型、标签、搜索等查询 contactsUnified: "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+aiType], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId, aiType, phone, region", // 联系人标签映射表索引:支持按用户、标签、联系人、类型查询 contactLabelMap: "serverId, userId, labelId, contactId, contactType, [userId+labelId], [userId+contactId], [userId+labelId+sortKey], sortKey, searchKey, avatar, nickname, conRemark, unreadCount, lastUpdateTime", // 用户登录记录表索引:支持按用户ID、登录时间查询 userLoginRecords: "serverId, userId, lastLoginTime, loginCount, createTime, lastActiveTime", }); this.version(2) .stores({ chatSessions: "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], [userId+aiType], sortKey, nickname, conRemark, avatar, content, lastUpdateTime, aiType, phone, region, extendFields", contactsUnified: "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+aiType], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId, aiType, phone, region, extendFields", contactLabelMap: "serverId, userId, labelId, contactId, contactType, [userId+labelId], [userId+contactId], [userId+labelId+sortKey], sortKey, searchKey, avatar, nickname, conRemark, unreadCount, lastUpdateTime", userLoginRecords: "serverId, userId, lastLoginTime, loginCount, createTime, lastActiveTime", }) .upgrade(async tx => { await tx .table("chatSessions") .toCollection() .modify(session => { if (!("extendFields" in session) || session.extendFields == null) { session.extendFields = "{}"; } else if (typeof session.extendFields !== "string") { session.extendFields = JSON.stringify(session.extendFields); } }); await tx .table("contactsUnified") .toCollection() .modify(contact => { if (!("extendFields" in contact) || contact.extendFields == null) { contact.extendFields = "{}"; } else if (typeof contact.extendFields !== "string") { contact.extendFields = JSON.stringify(contact.extendFields); } }); }); } } class DatabaseManager { private currentDb: CunkebaoDatabase | null = null; private currentUserId: number | null = null; private getDatabaseName(userId: number) { return `${DB_NAME_PREFIX}_${userId}`; } private async openDatabase(dbName: string) { const instance = new CunkebaoDatabase(dbName); await instance.open(); return instance; } async ensureDatabase(userId: number) { if (userId === undefined || userId === null) { throw new Error("Invalid userId provided for database initialization"); } if ( this.currentDb && this.currentUserId === userId && this.currentDb.isOpen() ) { return this.currentDb; } await this.closeCurrentDatabase(); const dbName = this.getDatabaseName(userId); this.currentDb = await this.openDatabase(dbName); this.currentUserId = userId; return this.currentDb; } getCurrentDatabase(): CunkebaoDatabase { if (!this.currentDb) { throw new Error("Database has not been initialized for the current user"); } return this.currentDb; } getCurrentUserId() { return this.currentUserId; } isInitialized(): boolean { return !!this.currentDb && this.currentDb.isOpen(); } async closeCurrentDatabase() { if (this.currentDb) { try { this.currentDb.close(); } catch (error) { console.warn("Failed to close current database:", error); } this.currentDb = null; } this.currentUserId = null; } } export const databaseManager = new DatabaseManager(); let pendingDatabaseRestore: Promise | null = null; async function restoreDatabaseFromPersistedState() { if (typeof window === "undefined") { return null; } const persistedData = getPersistedData>( PERSIST_KEYS.USER_STORE, "localStorage", ); if (!persistedData) { return null; } let parsed: any = persistedData; if (typeof persistedData === "string") { try { parsed = JSON.parse(persistedData); } catch (error) { console.warn("Failed to parse persisted user-store value:", error); return null; } } const state = parsed?.state ?? parsed; const userId = state?.user?.id; if (!userId) { return null; } try { return await databaseManager.ensureDatabase(userId); } catch (error) { console.warn("Failed to initialize database from persisted user:", error); return null; } } export async function initializeDatabaseFromPersistedUser() { if (databaseManager.isInitialized()) { return databaseManager.getCurrentDatabase(); } if (!pendingDatabaseRestore) { pendingDatabaseRestore = restoreDatabaseFromPersistedState().finally(() => { pendingDatabaseRestore = null; }); } return pendingDatabaseRestore; } const dbProxy = new Proxy({} as CunkebaoDatabase, { get(_target, prop: string | symbol) { const currentDb = databaseManager.getCurrentDatabase(); const value = (currentDb as any)[prop]; if (typeof value === "function") { return value.bind(currentDb); } return value; }, }); export const db = dbProxy; // 简单的数据库操作类 export class DatabaseService { constructor(private readonly tableAccessor: () => Table) {} private get table(): Table { return this.tableAccessor(); } // 基础 CRUD 操作 - 使用serverId作为主键 async create(data: Omit): Promise { return await this.table.add(this.prepareDataForWrite(data) as T); } // 创建数据(直接使用接口数据) // 接口数据的id字段直接作为serverId主键,原id字段保留 async createWithServerId(data: any): Promise { const dataToInsert = this.prepareDataForWrite({ ...data, serverId: data.id, // 使用接口的id作为serverId主键 phone: data.phone ?? "", region: data.region ?? "", }); return await this.table.add(dataToInsert as T); } // 根据原始ID查询(用户友好的查询方法) async findById(id: string | number): Promise { return await this.table.where("id").equals(id).first(); } // 根据serverId查询(内部主键查询) async findByPrimaryKey(serverId: string | number): Promise { return await this.table.get(serverId); } async findAll(): Promise { return await this.table.toArray(); } async update(serverId: string | number, data: Partial): Promise { return await this.table.update( serverId, this.prepareDataForWrite(data) as any, ); } async updateMany( dataList: { serverId: string | number; data: Partial }[], ): Promise { return await this.table.bulkUpdate( dataList.map(item => ({ key: item.serverId, changes: this.prepareDataForWrite(item.data) as any, })), ); } async createMany( dataList: Omit[], ): Promise<(string | number)[]> { const processed = dataList.map(item => this.prepareDataForWrite(item)); return await this.table.bulkAdd(processed as T[], { allKeys: true }); } // 批量创建数据(直接使用接口数据) // 接口数据的id字段直接作为serverId主键 async createManyWithServerId(dataList: any[]): Promise<(string | number)[]> { // 检查是否存在重复的serverId const serverIds = dataList.map(item => item.id); const existingData = await this.table .where("serverId") .anyOf(serverIds) .toArray(); const existingServerIds = new Set( existingData.map((item: any) => item.serverId), ); // 过滤掉已存在的数据 const newData = dataList.filter(item => !existingServerIds.has(item.id)); if (newData.length === 0) { // console.log("所有数据都已存在,跳过插入"); return []; } const processedData = newData.map(item => this.prepareDataForWrite({ ...item, serverId: item.id, // 使用接口的id作为serverId主键 phone: item.phone ?? "", region: item.region ?? "", }), ); return await this.table.bulkAdd(processedData as T[], { allKeys: true }); } async delete(serverId: string | number): Promise { await this.table.delete(serverId); } async clear(): Promise { await this.table.clear(); } // 条件查询 async findWhere(field: keyof T, value: any): Promise { return await this.table .where(field as string) .equals(value) .toArray(); } // 根据服务器ID查询(兼容性方法) async findByServerId(serverId: any): Promise { return await this.table.get(serverId); } // 根据原始ID批量查询 async findByIds(ids: (string | number)[]): Promise { return await this.table.where("id").anyOf(ids).toArray(); } // 多值查询(IN 查询) async findWhereIn(field: keyof T, values: any[]): Promise { return await this.table .where(field as string) .anyOf(values) .toArray(); } // 范围查询 async findWhereBetween(field: keyof T, min: any, max: any): Promise { return await this.table .where(field as string) .between(min, max) .toArray(); } // 模糊查询(以指定字符串开头) async findWhereStartsWith(field: keyof T, prefix: string): Promise { return await this.table .where(field as string) .startsWith(prefix) .toArray(); } // 不等于查询 async findWhereNot(field: keyof T, value: any): Promise { return await this.table .where(field as string) .notEqual(value) .toArray(); } // 大于查询 async findWhereGreaterThan(field: keyof T, value: any): Promise { return await this.table .where(field as string) .above(value) .toArray(); } // 小于查询 async findWhereLessThan(field: keyof T, value: any): Promise { return await this.table .where(field as string) .below(value) .toArray(); } // 复合条件查询 async findWhereMultiple( conditions: { field: keyof T; operator: | "equals" | "notEqual" | "above" | "below" | "aboveOrEqual" | "belowOrEqual" | "startsWith" | "anyOf" | "notIn" | "between" | "contains"; value: any; value2?: any; // 用于 between 操作符 }[], ): Promise { let collection = this.table.toCollection(); for (const condition of conditions) { const { field, operator, value, value2 } = condition; collection = collection.and(item => { const fieldValue = (item as any)[field]; switch (operator) { case "equals": return fieldValue === value; case "notEqual": return fieldValue !== value; case "above": return fieldValue > value; case "below": return fieldValue < value; case "aboveOrEqual": return fieldValue >= value; case "belowOrEqual": return fieldValue <= value; case "startsWith": return ( typeof fieldValue === "string" && fieldValue.startsWith(value) ); case "contains": return typeof fieldValue === "string" && fieldValue.includes(value); case "anyOf": return Array.isArray(value) && value.includes(fieldValue); case "notIn": return Array.isArray(value) && !value.includes(fieldValue); case "between": return fieldValue >= value && fieldValue <= (value2 ?? value); default: return true; } }); } return await collection.toArray(); } // 分页查询 async findWithPagination( page: number = 1, limit: number = 10, ): Promise<{ data: T[]; total: number; page: number; limit: number }> { const offset = (page - 1) * limit; const total = await this.table.count(); const data = await this.table.offset(offset).limit(limit).toArray(); return { data, total, page, limit }; } // 排序查询 async findAllSorted( field: keyof T, direction: "asc" | "desc" = "asc", ): Promise { const collection = this.table.orderBy(field as string); return direction === "desc" ? await collection.reverse().toArray() : await collection.toArray(); } // 统计 async count(): Promise { return await this.table.count(); } // 条件统计 async countWhere(field: keyof T, value: any): Promise { return await this.table .where(field as string) .equals(value) .count(); } private prepareDataForWrite(data: any) { if (!data || typeof data !== "object") { return data; } const prepared = { ...data }; if ("extendFields" in prepared) { const value = prepared.extendFields; if (typeof value === "string" && value.trim() !== "") { prepared.extendFields = value; } else if (value && typeof value === "object") { prepared.extendFields = JSON.stringify(value); } else { prepared.extendFields = "{}"; } } return prepared; } } // 创建统一表的服务实例 export const chatSessionService = new DatabaseService( () => databaseManager.getCurrentDatabase().chatSessions, ); export const contactUnifiedService = new DatabaseService( () => databaseManager.getCurrentDatabase().contactsUnified, ); export const contactLabelMapService = new DatabaseService( () => databaseManager.getCurrentDatabase().contactLabelMap, ); export const userLoginRecordService = new DatabaseService( () => databaseManager.getCurrentDatabase().userLoginRecords, ); // 默认导出数据库实例 export default db;