/** * 数据库工具类 - 使用serverId作为主键的优化架构 * * 架构设计: * 1. 使用serverId作为数据库主键,直接对应接口返回的id字段 * 2. 保留原始的id字段,用于存储接口数据的完整性 * 3. 添加userId字段实现多用户数据隔离 * 4. 统一会话表和联系人表,兼容好友和群聊 * * 优势: * - 直接使用服务器ID作为主键,避免ID冲突 * - 通过userId实现多用户数据隔离 * - 统一的会话和联系人表结构,兼容好友和群聊 * - 支持复合索引,提高查询性能 * - 支持用户登录记录和自动清理 */ import Dexie, { Table } from "dexie"; import { KfUserListData, weChatGroup, ContractData, MessageListData, } from "@/pages/pc/ckbox/data"; // ==================== 用户登录记录 ==================== 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; // 最后更新时间 config: { unreadCount: number; // 未读数量 top: boolean; // 是否置顶 }; sortKey: string; // 预计算排序键 // 好友特有字段(type='friend'时有效) wechatFriendId?: number; // 好友ID wechatId?: string; // 微信号 alias?: string; // 别名 // 群聊特有字段(type='group'时有效) chatroomId?: string; // 群聊ID chatroomOwner?: string; // 群主 selfDisplayName?: string; // 群内昵称 notice?: string; // 群公告 } // ==================== 统一联系人表(兼容好友和群聊) ==================== export interface Contact { serverId: string; // 主键 userId: number; // 用户ID(数据隔离) id: number; // 原始ID type: "friend" | "group"; // 类型:好友或群聊 // 通用字段 wechatAccountId: number; // 所属客服账号 nickname: string; // 显示名称 conRemark?: string; // 备注名 avatar: string; // 头像 lastUpdateTime: string; // 最后更新时间 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; // 全拼 // 群聊特有字段(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; } // ==================== 保留原有数据类型(向后兼容) ==================== export interface KfUserWithServerId extends Omit { serverId: number | string; // 服务器ID作为主键 userId: number; // 用户ID(数据隔离) id?: number; // 接口数据的原始ID字段 } export interface NewContactListData { serverId: number | string; // 服务器ID作为主键 userId: number; // 用户ID(数据隔离) id?: number; // 接口数据的原始ID字段 groupName: string; contacts: Contact[]; weChatGroup: Contact[]; } // 数据库类 class CunkebaoDatabase extends Dexie { // ==================== 保留原有表(向后兼容) ==================== kfUsers!: Table; weChatGroup!: Table; contracts!: Table; newContactList!: Table; messageList!: Table; // ==================== 新增统一表 ==================== chatSessions!: Table; // 统一会话表 contactsUnified!: Table; // 统一联系人表 contactLabelMap!: Table; // 联系人标签映射表 userLoginRecords!: Table; // 用户登录记录表 constructor() { super("CunkebaoDatabase"); // 版本1:使用serverId作为主键的架构(保留原有表) this.version(1).stores({ kfUsers: "serverId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline", weChatGroup: "serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, notice, selfDisplyName", contracts: "serverId, id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, duplicate", newContactList: "serverId, id, groupName, contacts", messageList: "serverId, id, dataType, wechatAccountId, tenantId, accountId, nickname, avatar, groupId, config, labels, wechatId, alias, conRemark, quanPin, gender, region, addFrom, phone, signature, extendFields, city, lastUpdateTime, isPassed, thirdParty, additionalPicture, desc, lastMessageTime, duplicate, chatroomId, chatroomOwner, chatroomAvatar, notice, selfDisplyName", }); // 版本2:添加用户隔离字段到原有表 this.version(2) .stores({ kfUsers: "serverId, userId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline", weChatGroup: "serverId, userId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, notice, selfDisplyName", contracts: "serverId, userId, id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, duplicate", newContactList: "serverId, userId, id, groupName, contacts", messageList: "serverId, userId, id, dataType, wechatAccountId, tenantId, accountId, nickname, avatar, groupId, config, labels, wechatId, alias, conRemark, quanPin, gender, region, addFrom, phone, signature, extendFields, city, lastUpdateTime, isPassed, thirdParty, additionalPicture, desc, lastMessageTime, duplicate, chatroomId, chatroomOwner, chatroomAvatar, notice, selfDisplyName", }) .upgrade(trans => { // 为现有数据添加默认userId=0(需要手动清理) return trans .table("kfUsers") .toCollection() .modify(item => { item.userId = 0; }); }); // 版本3:添加新的统一表和用户登录记录表 this.version(3).stores({ // 保留原有表 kfUsers: "serverId, userId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline", weChatGroup: "serverId, userId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, notice, selfDisplyName", contracts: "serverId, userId, id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, duplicate", newContactList: "serverId, userId, id, groupName, contacts", messageList: "serverId, userId, id, dataType, wechatAccountId, tenantId, accountId, nickname, avatar, groupId, config, labels, wechatId, alias, conRemark, quanPin, gender, region, addFrom, phone, signature, extendFields, city, lastUpdateTime, isPassed, thirdParty, additionalPicture, desc, lastMessageTime, duplicate, chatroomId, chatroomOwner, chatroomAvatar, notice, selfDisplyName", // 新增统一表 // 会话表索引:支持按用户、类型、时间、置顶等查询 chatSessions: "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], sortKey, nickname, conRemark, avatar, content, lastUpdateTime", // 联系人表索引:支持按用户、类型、标签、搜索等查询 contactsUnified: "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime", // 联系人标签映射表索引:支持按用户、标签、联系人、类型查询 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", }); } } // 创建数据库实例 export const db = new CunkebaoDatabase(); // 简单的数据库操作类 export class DatabaseService { constructor(private table: Table) {} // 基础 CRUD 操作 - 使用serverId作为主键 async create(data: Omit): Promise { return await this.table.add(data as T); } // 创建数据(直接使用接口数据) // 接口数据的id字段直接作为serverId主键,原id字段保留 async createWithServerId(data: any): Promise { const dataToInsert = { ...data, serverId: data.id, // 使用接口的id作为serverId主键 }; 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, data as any); } async updateMany( dataList: { serverId: string | number; data: Partial }[], ): Promise { return await this.table.bulkUpdate( dataList.map(item => ({ key: item.serverId, changes: item.data as any, })), ); } async createMany( dataList: Omit[], ): Promise<(string | number)[]> { return await this.table.bulkAdd(dataList 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 => ({ ...item, serverId: item.id, // 使用接口的id作为serverId主键 })); 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(); } } // 创建各表的服务实例 export const kfUserService = new DatabaseService(db.kfUsers); export const weChatGroupService = new DatabaseService(db.weChatGroup); export const contractService = new DatabaseService(db.contracts); export const newContactListService = new DatabaseService(db.newContactList); export const messageListService = new DatabaseService(db.messageList); // 新增统一表服务 export const chatSessionService = new DatabaseService(db.chatSessions); export const contactUnifiedService = new DatabaseService(db.contactsUnified); export const contactLabelMapService = new DatabaseService(db.contactLabelMap); export const userLoginRecordService = new DatabaseService(db.userLoginRecords); // 默认导出数据库实例 export default db;