refactor(db): 重构数据库架构使用serverId作为主键

- 将数据库主键从自增id改为直接使用serverId,避免ID冲突
- 简化数据存储和查询逻辑,提高性能
- 添加重复数据检测和去重功能
- 更新相关组件以适配新的数据库接口
- 在应用启动时初始化数据库连接
This commit is contained in:
超级老白兔
2025-08-30 15:00:26 +08:00
parent d4336ed447
commit 5bdd299dad
7 changed files with 152 additions and 95 deletions

View File

@@ -1,10 +1,25 @@
// main.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./styles/global.scss";
// 引入错误处理器来抑制findDOMNode警告
// import VConsole from "vconsole";
// new VConsole();
import { db } from "@/utils/db"; // 引入数据库实例
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
// 数据库初始化
async function initializeApp() {
try {
// 确保数据库已打开
await db.open();
console.log("数据库初始化成功");
} catch (error) {
console.error("数据库初始化失败:", error);
// 可以选择显示错误提示或使用降级方案
}
// 渲染应用
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
}
// 启动应用
initializeApp();

View File

@@ -44,7 +44,7 @@ const Person: React.FC<PersonProps> = ({
const [isEditingRemark, setIsEditingRemark] = useState(false);
const [remarkValue, setRemarkValue] = useState(contract.conRemark || "");
const kfSelectedUser = useCkChatStore(state => state.kfSelectedUser());
const getKfSelectedUser = useCkChatStore(state => state.getKfSelectedUser());
// 当contract变化时更新备注值
useEffect(() => {
@@ -124,7 +124,7 @@ const Person: React.FC<PersonProps> = ({
</Tooltip>
<div className={styles.profileRemark}>
{JSON.stringify(kfSelectedUser)}
{JSON.stringify(getKfSelectedUser)}
{isEditingRemark ? (
<div
style={{

View File

@@ -68,7 +68,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
>({});
const messagesEndRef = useRef<HTMLDivElement>(null);
const kfSelectedUser = useCkChatStore(state => state.kfSelectedUser());
const getKfSelectedUser = useCkChatStore(state => state.getKfSelectedUser());
useEffect(() => {
clearUnreadCount([contract.id]).then(() => {
setLoading(true);

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { Avatar, Badge, Tooltip } from "antd";
import styles from "./VerticalUserList.module.scss";
import { useCkChatStore, asyncKfSelected } from "@/store/module/ckchat/ckchat";
@@ -14,9 +14,24 @@ const VerticalUserList: React.FC = () => {
const handleUserSelect = (userId: number) => {
asyncKfSelected(userId);
};
const kfUserList = useCkChatStore(state => state.kfUserList);
const getkfUserList = useCkChatStore(state => state.getkfUserList);
const kfSelected = useCkChatStore(state => state.kfSelected);
const [kefuList, setKefuList] = useState([]);
// 获取客服列表数据
useEffect(() => {
const fetchKfUserList = async () => {
try {
const data = await getkfUserList();
setKefuList(data || []);
} catch (error) {
console.error("获取客服列表失败:", error);
setKefuList([]);
}
};
fetchKfUserList();
}, [getkfUserList]);
return (
<div className={styles.verticalUserList}>
<div
@@ -27,7 +42,7 @@ const VerticalUserList: React.FC = () => {
<div className={styles.allFriends}></div>
</div>
<div className={styles.userList}>
{kfUserList.map(user => (
{kefuList.map(user => (
<Tooltip key={user.id} title={user.name} placement="right">
<div
className={`${styles.userItem} ${kfSelected === user.id ? styles.active : ""}`}

View File

@@ -31,7 +31,7 @@ export interface CkChatState {
chatSessions: any[];
kfUserList: KfUserListData[];
kfSelected: number;
kfSelectedUser: () => KfUserListData | undefined;
getKfSelectedUser: () => KfUserListData | undefined;
newContractList: { groupName: string; contacts: any[] }[];
asyncKfSelected: (data: number) => void;
getkfUserList: () => KfUserListData[];

View File

@@ -6,7 +6,7 @@ import {
CkAccount,
KfUserListData,
} from "@/pages/pc/ckbox/data";
import { kfUserService } from "@/utils/db";
export const useCkChatStore = createPersistStore<CkChatState>(
set => ({
userInfo: null,
@@ -16,9 +16,18 @@ export const useCkChatStore = createPersistStore<CkChatState>(
kfUserList: [], //客服列表
kfSelected: 0,
newContractList: [], //联系人分组
kfSelectedUser: () => {
const state = useCkChatStore.getState();
return state.kfUserList.find(item => item.id === state.kfSelected);
//客服列表
asyncKfUserList: async data => {
console.log(data);
await kfUserService.createManyWithServerId(data);
// set({ kfUserList: data });
},
// 获取客服列表
getkfUserList: async () => {
return await kfUserService.findAll();
// const state = useCkChatStore.getState();
// return state.kfUserList;
},
asyncKfSelected: (data: number) => {
set({ kfSelected: data });
@@ -39,14 +48,12 @@ export const useCkChatStore = createPersistStore<CkChatState>(
asyncContractList: data => {
set({ contractList: data });
},
// 控制终端用户列表
getkfUserList: () => {
//获取选中的客服信息
getgetKfSelectedUser: () => {
const state = useCkChatStore.getState();
return state.kfUserList;
},
asyncKfUserList: data => {
set({ kfUserList: data });
return state.kfUserList.find(item => item.id === state.kfSelected);
},
// 删除控制终端用户
deleteCtrlUser: (userId: number) => {
set(state => ({

View File

@@ -1,45 +1,53 @@
/**
* 数据库工具类 - 解决服务器ID与本地自增主键冲突问题
* 数据库工具类 - 使用serverId作为主键的优化架构
*
* 问题描述
* 接口返回的数据包含id字段直接存储到数据库会与Dexie的自增主键(++id)产生冲突
* 架构设计
* 1. 使用serverId作为数据库主键直接对应接口返回的id字段
* 2. 保留原始的id字段用于存储接口数据的完整性
* 3. 简化数据处理逻辑避免ID映射的复杂性
*
* 解决方案
* 1. 将服务器返回的id字段映射为serverId字段存储
* 2. 数据库使用自增的id作为主键
* 3. 提供专门的方法处理服务器数据的存储和查询
* 优势
* - 直接使用服务器ID作为主键避免ID冲突
* - 保持数据的一致性和可追溯性
* - 简化查询逻辑,提高性能
* - 支持重复数据检测和去重
*
* 使用方法:
* - 存储接口数据:使用 createWithServerId() 或 createManyWithServerId()
* - 查询服务器数据:使用 findByServerId()
* - 常规操作:使用原有的 create(), findById() 等方法
* - 查询数据:使用 findById(id) 根据原始ID查询或 findByPrimaryKey(serverId) 根据主键查询
* - 批量查询:使用 findByIds([id1, id2, ...]) 根据原始ID批量查询
* - 内部操作serverId作为主键用于数据库内部管理
*
* 示例:
* const serverData = { id: 1001, name: '测试', ... }; // 接口返回的数据
* const localId = await service.createWithServerId(serverData); // 存储,返回本地ID
* const data = await service.findByServerId(1001); // 根据服务器ID查询
* const serverId = await service.createWithServerId(serverData); // 存储,返回serverId
* const data = await service.findById(1001); // 根据原始ID查询(用户友好)
* const dataByPK = await service.findByPrimaryKey(serverId); // 根据主键查询(内部使用)
*/
import Dexie, { Table } from "dexie";
import { KfUserListData, GroupData, ContractData } from "@/pages/pc/ckbox/data";
// 扩展数据类型,添加serverId字段
export interface KfUserWithServerId extends KfUserListData {
serverId?: number | string; // 服务器返回的原始ID
// 数据类型定义,使用serverId作为主键
export interface KfUserWithServerId extends Omit<KfUserListData, "id"> {
serverId: number | string; // 服务器ID作为主键
id?: number; // 接口数据的原始ID字段
}
export interface GroupWithServerId extends GroupData {
serverId?: number | string; // 服务器返回的原始ID
export interface GroupWithServerId extends Omit<GroupData, "id"> {
serverId: number | string; // 服务器ID作为主键
id?: number; // 接口数据的原始ID字段
}
export interface ContractWithServerId extends ContractData {
serverId?: number | string; // 服务器返回的原始ID
export interface ContractWithServerId extends Omit<ContractData, "id"> {
serverId: number | string; // 服务器ID作为主键
id?: number; // 接口数据的原始ID字段
}
// 新联系人列表数据接口
export interface NewContactListData {
id?: number;
serverId?: number | string; // 服务器返回的原始ID
serverId: number | string; // 服务器ID作为主键
id?: number; // 接口数据的原始ID字段
groupName: string;
contacts: ContractData[] | GroupData[];
}
@@ -54,34 +62,16 @@ class CunkebaoDatabase extends Dexie {
constructor() {
super("CunkebaoDatabase");
// 版本1初始版本(不包含serverId字段)
// 版本1使用serverId作为主键的架构
this.version(1).stores({
kfUsers:
"++id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
"serverId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
groups:
"++id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar, groupId, config, unreadCount, notice, selfDisplyName",
"serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar, groupId, config, unreadCount, notice, selfDisplyName",
contracts:
"++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, unreadCount, duplicate",
newContractList: "++id, groupName, contacts",
"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, unreadCount, duplicate",
newContractList: "serverId, id, groupName, contacts",
});
// 版本2添加serverId字段支持
this.version(2)
.stores({
kfUsers:
"++id, serverId, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
groups:
"++id, serverId, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar, groupId, config, unreadCount, notice, selfDisplyName",
contracts:
"++id, serverId, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, unreadCount, duplicate",
newContractList: "++id, serverId, groupName, contacts",
})
.upgrade(tx => {
// 数据库升级逻辑为现有数据添加serverId字段可选
console.log("数据库升级到版本2添加serverId字段支持");
// 注意这里不需要迁移数据因为serverId是可选字段
// 如果需要迁移现有数据,可以在这里添加相应逻辑
});
}
}
@@ -92,64 +82,90 @@ export const db = new CunkebaoDatabase();
export class DatabaseService<T> {
constructor(private table: Table<T>) {}
// 基础 CRUD 操作
async create(data: Omit<T, "id">): Promise<number> {
// 基础 CRUD 操作 - 使用serverId作为主键
async create(data: Omit<T, "serverId">): Promise<string | number> {
return await this.table.add(data as T);
}
// 创建数据(处理服务器ID映射
// 用于存储从接口获取的数据,将服务器的id字段映射为serverId,避免与数据库自增主键冲突
async createWithServerId(data: any): Promise<number> {
const { id, ...restData } = data;
// 创建数据(直接使用接口数据
// 接口数据的id字段直接作为serverId主键原id字段保留
async createWithServerId(data: any): Promise<string | number> {
const dataToInsert = {
...restData,
serverId: id, // 将服务器的id映射为serverId
...data,
serverId: data.id, // 使用接口的id为serverId主键
};
return await this.table.add(dataToInsert as T);
}
async findById(id: number): Promise<T | undefined> {
return await this.table.get(id);
// 根据原始ID查询用户友好的查询方法
async findById(id: string | number): Promise<T | undefined> {
return await this.table.where("id").equals(id).first();
}
// 根据serverId查询内部主键查询
async findByPrimaryKey(serverId: string | number): Promise<T | undefined> {
return await this.table.get(serverId);
}
async findAll(): Promise<T[]> {
return await this.table.toArray();
}
async update(id: number, data: Partial<T>): Promise<number> {
return await this.table.update(id, data as any);
async update(serverId: string | number, data: Partial<T>): Promise<number> {
return await this.table.update(serverId, data as any);
}
async updateMany(
dataList: { id: number; data: Partial<T> }[],
dataList: { serverId: string | number; data: Partial<T> }[],
): Promise<number> {
return await this.table.bulkUpdate(
dataList.map(item => ({
key: item.id,
key: item.serverId,
changes: item.data as any,
})),
);
}
async createMany(dataList: Omit<T, "id">[]): Promise<number[]> {
async createMany(
dataList: Omit<T, "serverId">[],
): Promise<(string | number)[]> {
return await this.table.bulkAdd(dataList as T[], { allKeys: true });
}
// 批量创建数据(处理服务器ID映射
// 用于批量存储从接口获取的数据,将服务器的id字段映射为serverId
async createManyWithServerId(dataList: any[]): Promise<number[]> {
const processedData = dataList.map(item => {
const { id, ...restData } = item;
return {
...restData,
serverId: id, // 将服务器的id映射为serverId
};
});
// 批量创建数据(直接使用接口数据
// 接口数据的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主键
}));
console.log(
`插入 ${processedData.length} 条新数据,跳过 ${dataList.length - newData.length} 条重复数据`,
);
return await this.table.bulkAdd(processedData as T[], { allKeys: true });
}
async delete(id: number): Promise<void> {
await this.table.delete(id);
async delete(serverId: string | number): Promise<void> {
await this.table.delete(serverId);
}
async clear(): Promise<void> {
@@ -164,10 +180,14 @@ export class DatabaseService<T> {
.toArray();
}
// 根据服务器ID查询
// 用于根据原始的服务器ID查找数据
// 根据服务器ID查询(兼容性方法)
async findByServerId(serverId: any): Promise<T | undefined> {
return await this.table.where("serverId").equals(serverId).first();
return await this.table.get(serverId);
}
// 根据原始ID批量查询
async findByIds(ids: (string | number)[]): Promise<T[]> {
return await this.table.where("id").anyOf(ids).toArray();
}
// 多值查询IN 查询)