重构数据库管理逻辑,简化用户数据库初始化流程。引入新的数据库管理类以支持动态数据库名称和用户状态管理。更新应用启动逻辑以确保在用户登录时正确初始化数据库。增强持久化数据恢复功能,确保用户数据的可靠性和一致性。

This commit is contained in:
超级老白兔
2025-11-13 11:58:12 +08:00
parent eb5dbe5066
commit ae4a165b07
7 changed files with 342 additions and 150 deletions

View File

@@ -7,97 +7,18 @@ import dayjs from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import App from "./App"; import App from "./App";
import "./styles/global.scss"; import "./styles/global.scss";
import { db } from "./utils/db"; // 引入数据库实例 import { initializeDatabaseFromPersistedUser } from "./utils/db";
// 设置dayjs为中文 // 设置dayjs为中文
dayjs.locale("zh-cn"); dayjs.locale("zh-cn");
// 清理旧数据库 async function bootstrap() {
async function cleanupOldDatabase() {
try { try {
// 获取所有数据库 await initializeDatabaseFromPersistedUser();
const databases = await indexedDB.databases();
for (const dbInfo of databases) {
if (dbInfo.name === "CunkebaoDatabase") {
console.log("检测到旧版数据库,开始清理...");
// 打开数据库检查版本
const openRequest = indexedDB.open(dbInfo.name);
await new Promise<void>((resolve, reject) => {
openRequest.onsuccess = async event => {
const database = (event.target as IDBOpenDBRequest).result;
const objectStoreNames = Array.from(database.objectStoreNames);
// 检查是否存在旧表
const hasOldTables = objectStoreNames.some(name =>
[
"kfUsers",
"weChatGroup",
"contracts",
"newContactList",
"messageList",
].includes(name),
);
if (hasOldTables) {
console.log("发现旧表,删除整个数据库:", objectStoreNames);
database.close();
// 删除整个数据库
const deleteRequest = indexedDB.deleteDatabase(dbInfo.name);
deleteRequest.onsuccess = () => {
console.log("旧数据库已删除");
resolve();
};
deleteRequest.onerror = () => {
console.error("删除旧数据库失败");
reject();
};
} else {
console.log("数据库结构正确,无需清理");
database.close();
resolve();
}
};
openRequest.onerror = () => {
console.error("无法打开数据库进行检查");
reject();
};
});
}
}
} catch (error) { } catch (error) {
console.warn("清理旧数据库时出错(可忽略):", error); console.warn("Failed to prepare database before app bootstrap:", error);
}
}
// 数据库初始化
async function initializeApp() {
try {
// 1. 清理旧数据库
await cleanupOldDatabase();
// 2. 打开新数据库
await db.open();
console.log("数据库初始化成功");
// 3. 开发环境清空数据(可选)
if (process.env.NODE_ENV === "development") {
console.log("开发环境:跳过数据清理");
// 如需清空数据,取消下面的注释
// await db.chatSessions.clear();
// await db.contactsUnified.clear();
// await db.contactLabelMap.clear();
// await db.userLoginRecords.clear();
}
} catch (error) {
console.error("数据库初始化失败:", error);
} }
// 渲染应用
const root = createRoot(document.getElementById("root")!); const root = createRoot(document.getElementById("root")!);
root.render( root.render(
<ConfigProvider locale={zhCN}> <ConfigProvider locale={zhCN}>
@@ -106,5 +27,4 @@ async function initializeApp() {
); );
} }
// 启动应用 void bootstrap();
initializeApp();

View File

@@ -33,5 +33,5 @@ export interface GetGroupPushHistoryParams {
[property: string]: any; [property: string]: any;
} }
export const getPushHistory = async (params: GetGroupPushHistoryParams) => { export const getPushHistory = async (params: GetGroupPushHistoryParams) => {
return request("/v1/workbench/group-push-history", { params }); return request("/v1/workbench/group-push-history", params, "GET");
}; };

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useCallback, useMemo, useState } from "react";
import { Layout, Tabs } from "antd"; import { Layout, Tabs } from "antd";
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
import styles from "./Person.module.scss"; import styles from "./Person.module.scss";
@@ -9,6 +9,8 @@ import LayoutFiexd from "@/components/Layout/LayoutFiexd";
const { Sider } = Layout; const { Sider } = Layout;
const noop = () => {};
interface PersonProps { interface PersonProps {
contract: ContractData | weChatGroup; contract: ContractData | weChatGroup;
} }
@@ -16,47 +18,46 @@ interface PersonProps {
const Person: React.FC<PersonProps> = ({ contract }) => { const Person: React.FC<PersonProps> = ({ contract }) => {
const [activeKey, setActiveKey] = useState("profile"); const [activeKey, setActiveKey] = useState("profile");
const isGroup = "chatroomId" in contract; const isGroup = "chatroomId" in contract;
const tabItems = useMemo(() => {
const baseItems = [
{
key: "quickwords",
label: "快捷语录",
},
{
key: "profile",
label: isGroup ? "群资料" : "个人资料",
},
];
if (!isGroup) {
baseItems.push({
key: "moments",
label: "朋友圈",
});
}
return baseItems;
}, [isGroup]);
const handleTabChange = useCallback((key: string) => {
setActiveKey(key);
}, []);
const tabBarStyle = useMemo(() => ({ padding: "0 30px" }), []);
return ( return (
<Sider width={330} className={styles.profileSider}> <Sider width={330} className={styles.profileSider}>
<LayoutFiexd <LayoutFiexd
header={ header={
<Tabs <Tabs
activeKey={activeKey} activeKey={activeKey}
onChange={key => setActiveKey(key)} onChange={handleTabChange}
tabBarStyle={{ tabBarStyle={tabBarStyle}
padding: "0 30px", items={tabItems}
}}
items={[
{
key: "quickwords",
label: "快捷语录",
},
{
key: "profile",
label: isGroup ? "群资料" : "个人资料",
},
...(!isGroup
? [
{
key: "moments",
label: "朋友圈",
},
]
: []),
]}
/> />
} }
> >
{activeKey === "profile" && <ProfileModules contract={contract} />} {activeKey === "profile" && <ProfileModules contract={contract} />}
{activeKey === "quickwords" && ( {activeKey === "quickwords" && <QuickWords onInsert={noop} />}
<QuickWords
words={[]}
onInsert={() => {}}
onAdd={() => {}}
onRemove={() => {}}
/>
)}
{activeKey === "moments" && !isGroup && ( {activeKey === "moments" && !isGroup && (
<FriendsCircle wechatFriendId={contract.id} /> <FriendsCircle wechatFriendId={contract.id} />
)} )}

View File

@@ -1,5 +1,52 @@
import { createPersistStore } from "@/store/createPersistStore"; import { createPersistStore } from "@/store/createPersistStore";
import { Toast } from "antd-mobile"; import { Toast } from "antd-mobile";
import { databaseManager } from "@/utils/db";
const STORE_CACHE_KEYS = [
"user-store",
"app-store",
"settings-store",
"websocket-store",
"ckchat-store",
"wechat-storage",
"contacts-storage",
"message-storage",
"customer-storage",
];
const allStorages = (): Storage[] => {
if (typeof window === "undefined") {
return [];
}
const storages: Storage[] = [];
try {
storages.push(window.localStorage);
} catch (error) {
console.warn("无法访问 localStorage:", error);
}
try {
storages.push(window.sessionStorage);
} catch (error) {
console.warn("无法访问 sessionStorage:", error);
}
return storages;
};
const clearStoreCaches = () => {
const storages = allStorages();
if (!storages.length) {
return;
}
STORE_CACHE_KEYS.forEach(key => {
storages.forEach(storage => {
try {
storage.removeItem(key);
} catch (error) {
console.warn(`清理持久化数据失败: ${key}`, error);
}
});
});
};
export interface User { export interface User {
id: number; id: number;
@@ -28,7 +75,7 @@ interface UserState {
setToken: (token: string) => void; setToken: (token: string) => void;
setToken2: (token2: string) => void; setToken2: (token2: string) => void;
clearUser: () => void; clearUser: () => void;
login: (token: string, userInfo: User) => void; login: (token: string, userInfo: User) => Promise<void>;
login2: (token2: string) => void; login2: (token2: string) => void;
logout: () => void; logout: () => void;
} }
@@ -39,12 +86,27 @@ export const useUserStore = createPersistStore<UserState>(
token: null, token: null,
token2: null, token2: null,
isLoggedIn: false, isLoggedIn: false,
setUser: user => set({ user, isLoggedIn: true }), setUser: user => {
set({ user, isLoggedIn: true });
databaseManager.ensureDatabase(user.id).catch(error => {
console.warn("Failed to initialize database for user:", error);
});
},
setToken: token => set({ token }), setToken: token => set({ token }),
setToken2: token2 => set({ token2 }), setToken2: token2 => set({ token2 }),
clearUser: () => clearUser: () => {
set({ user: null, token: null, token2: null, isLoggedIn: false }), databaseManager.closeCurrentDatabase().catch(error => {
login: (token, userInfo) => { console.warn("Failed to close database on clearUser:", error);
});
clearStoreCaches();
set({ user: null, token: null, token2: null, isLoggedIn: false });
},
login: async (token, userInfo) => {
clearStoreCaches();
// 清除旧的双token缓存
localStorage.removeItem("token2");
// 只将token存储到localStorage // 只将token存储到localStorage
localStorage.setItem("token", token); localStorage.setItem("token", token);
@@ -66,6 +128,11 @@ export const useUserStore = createPersistStore<UserState>(
lastLoginIp: userInfo.lastLoginIp, lastLoginIp: userInfo.lastLoginIp,
lastLoginTime: userInfo.lastLoginTime, lastLoginTime: userInfo.lastLoginTime,
}; };
try {
await databaseManager.ensureDatabase(user.id);
} catch (error) {
console.error("Failed to initialize user database:", error);
}
set({ user, token, isLoggedIn: true }); set({ user, token, isLoggedIn: true });
Toast.show({ content: "登录成功", position: "top" }); Toast.show({ content: "登录成功", position: "top" });
@@ -80,6 +147,10 @@ export const useUserStore = createPersistStore<UserState>(
// 清除localStorage中的token // 清除localStorage中的token
localStorage.removeItem("token"); localStorage.removeItem("token");
localStorage.removeItem("token2"); localStorage.removeItem("token2");
databaseManager.closeCurrentDatabase().catch(error => {
console.warn("Failed to close user database on logout:", error);
});
clearStoreCaches();
set({ user: null, token: null, token2: null, isLoggedIn: false }); set({ user: null, token: null, token2: null, isLoggedIn: false });
}, },
}), }),
@@ -92,7 +163,11 @@ export const useUserStore = createPersistStore<UserState>(
isLoggedIn: state.isLoggedIn, isLoggedIn: state.isLoggedIn,
}), }),
onRehydrateStorage: () => state => { onRehydrateStorage: () => state => {
// console.log("User store hydrated:", state); if (state?.user?.id) {
databaseManager.ensureDatabase(state.user!.id).catch(error => {
console.warn("Failed to restore user database:", error);
});
}
}, },
}, },
); );

View File

@@ -1,7 +1,7 @@
import { createPersistStore } from "@/store/createPersistStore"; import { createPersistStore } from "@/store/createPersistStore";
import { Toast } from "antd-mobile";
import { useUserStore } from "../user"; import { useUserStore } from "../user";
import { useCkChatStore } from "@/store/module/ckchat/ckchat"; import { useCkChatStore } from "@/store/module/ckchat/ckchat";
import { useCustomerStore } from "@/store/module/weChat/customer";
const { getAccountId } = useCkChatStore.getState(); const { getAccountId } = useCkChatStore.getState();
import { msgManageCore } from "./msgManage"; import { msgManageCore } from "./msgManage";
// WebSocket消息类型 // WebSocket消息类型
@@ -52,6 +52,7 @@ interface WebSocketState {
reconnectAttempts: number; reconnectAttempts: number;
reconnectTimer: NodeJS.Timeout | null; reconnectTimer: NodeJS.Timeout | null;
aliveStatusTimer: NodeJS.Timeout | null; // 客服用户状态查询定时器 aliveStatusTimer: NodeJS.Timeout | null; // 客服用户状态查询定时器
aliveStatusUnsubscribe: (() => void) | null;
// 方法 // 方法
connect: (config: Partial<WebSocketConfig>) => void; connect: (config: Partial<WebSocketConfig>) => void;
@@ -97,6 +98,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
reconnectAttempts: 0, reconnectAttempts: 0,
reconnectTimer: null, reconnectTimer: null,
aliveStatusTimer: null, aliveStatusTimer: null,
aliveStatusUnsubscribe: null,
// 连接WebSocket // 连接WebSocket
connect: (config: Partial<WebSocketConfig>) => { connect: (config: Partial<WebSocketConfig>) => {
@@ -405,7 +407,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
}, },
// 内部方法:处理连接关闭 // 内部方法:处理连接关闭
_handleClose: (event: CloseEvent) => { _handleClose: () => {
const currentState = get(); const currentState = get();
// console.log("WebSocket连接关闭:", event.code, event.reason); // console.log("WebSocket连接关闭:", event.code, event.reason);
@@ -431,7 +433,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
}, },
// 内部方法:处理连接错误 // 内部方法:处理连接错误
_handleError: (event: Event) => { _handleError: () => {
// console.error("WebSocket连接错误:", event); // console.error("WebSocket连接错误:", event);
set({ status: WebSocketStatus.ERROR }); set({ status: WebSocketStatus.ERROR });
@@ -477,42 +479,84 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
// 先停止现有定时器 // 先停止现有定时器
currentState._stopAliveStatusTimer(); currentState._stopAliveStatusTimer();
// 获取客服用户列表 const requestAliveStatus = () => {
const { kfUserList } = useCkChatStore.getState(); const state = get();
if (state.status !== WebSocketStatus.CONNECTED) {
return;
}
// 如果没有客服用户,不启动定时器 const { customerList } = useCustomerStore.getState();
if (!kfUserList || kfUserList.length === 0) { const { kfUserList } = useCkChatStore.getState();
return; const targets =
} customerList && customerList.length > 0
? customerList
: kfUserList && kfUserList.length > 0
? kfUserList
: [];
if (targets.length > 0) {
state.sendCommand("CmdRequestWechatAccountsAliveStatus", {
wechatAccountIds: targets.map(v => v.id),
});
}
};
// 尝试立即请求一次,如果客服列表尚未加载,后续定时器会继续检查
requestAliveStatus();
const unsubscribeCustomer = useCustomerStore.subscribe(state => {
if (
get().status === WebSocketStatus.CONNECTED &&
state.customerList &&
state.customerList.length > 0
) {
requestAliveStatus();
}
});
const unsubscribeKf = useCkChatStore.subscribe(state => {
if (
get().status === WebSocketStatus.CONNECTED &&
state.kfUserList &&
state.kfUserList.length > 0
) {
requestAliveStatus();
}
});
// 启动定时器每5秒查询一次 // 启动定时器每5秒查询一次
const timer = setInterval(() => { const timer = setInterval(() => {
const state = get(); const state = get();
// 检查连接状态 // 检查连接状态
if (state.status === WebSocketStatus.CONNECTED) { if (state.status === WebSocketStatus.CONNECTED) {
const { kfUserList: currentKfUserList } = useCkChatStore.getState(); requestAliveStatus();
if (currentKfUserList && currentKfUserList.length > 0) {
state.sendCommand("CmdRequestWechatAccountsAliveStatus", {
wechatAccountIds: currentKfUserList.map(v => v.id),
});
}
} else { } else {
// 如果连接断开,停止定时器 // 如果连接断开,停止定时器
state._stopAliveStatusTimer(); state._stopAliveStatusTimer();
} }
}, 5 * 1000); }, 5 * 1000);
set({ aliveStatusTimer: timer }); set({
aliveStatusTimer: timer,
aliveStatusUnsubscribe: () => {
unsubscribeCustomer();
unsubscribeKf();
},
});
}, },
// 内部方法:停止客服状态查询定时器 // 内部方法:停止客服状态查询定时器
_stopAliveStatusTimer: () => { _stopAliveStatusTimer: () => {
const currentState = get(); const currentState = get();
if (currentState.aliveStatusUnsubscribe) {
currentState.aliveStatusUnsubscribe();
}
if (currentState.aliveStatusTimer) { if (currentState.aliveStatusTimer) {
clearInterval(currentState.aliveStatusTimer); clearInterval(currentState.aliveStatusTimer);
set({ aliveStatusTimer: null });
} }
set({ aliveStatusTimer: null, aliveStatusUnsubscribe: null });
}, },
}), }),
{ {

View File

@@ -5,6 +5,12 @@ export const PERSIST_KEYS = {
USER_STORE: "user-store", USER_STORE: "user-store",
APP_STORE: "app-store", APP_STORE: "app-store",
SETTINGS_STORE: "settings-store", SETTINGS_STORE: "settings-store",
CKCHAT_STORE: "ckchat-store",
WEBSOCKET_STORE: "websocket-store",
WECHAT_STORAGE: "wechat-storage",
CONTACTS_STORAGE: "contacts-storage",
MESSAGE_STORAGE: "message-storage",
CUSTOMER_STORAGE: "customer-storage",
} as const; } as const;
// 存储类型 // 存储类型

View File

@@ -16,6 +16,8 @@
*/ */
import Dexie, { Table } from "dexie"; import Dexie, { Table } from "dexie";
import { getPersistedData, PERSIST_KEYS } from "@/store/persistUtils";
const DB_NAME_PREFIX = "CunkebaoDatabase";
// ==================== 用户登录记录 ==================== // ==================== 用户登录记录 ====================
export interface UserLoginRecord { export interface UserLoginRecord {
@@ -123,8 +125,8 @@ class CunkebaoDatabase extends Dexie {
contactLabelMap!: Table<ContactLabelMap>; // 联系人标签映射表 contactLabelMap!: Table<ContactLabelMap>; // 联系人标签映射表
userLoginRecords!: Table<UserLoginRecord>; // 用户登录记录表 userLoginRecords!: Table<UserLoginRecord>; // 用户登录记录表
constructor() { constructor(dbName: string) {
super("CunkebaoDatabase"); super(dbName);
// 版本1统一表结构 // 版本1统一表结构
this.version(1).stores({ this.version(1).stores({
@@ -188,12 +190,148 @@ class CunkebaoDatabase extends Dexie {
} }
} }
// 创建数据库实例 class DatabaseManager {
export const db = new CunkebaoDatabase(); 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<CunkebaoDatabase | null> | null = null;
async function restoreDatabaseFromPersistedState() {
if (typeof window === "undefined") {
return null;
}
const persistedData = getPersistedData<string | Record<string, any>>(
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<T> { export class DatabaseService<T> {
constructor(private table: Table<T>) {} constructor(private readonly tableAccessor: () => Table<T>) {}
private get table(): Table<T> {
return this.tableAccessor();
}
// 基础 CRUD 操作 - 使用serverId作为主键 // 基础 CRUD 操作 - 使用serverId作为主键
async create(data: Omit<T, "serverId">): Promise<string | number> { async create(data: Omit<T, "serverId">): Promise<string | number> {
@@ -446,10 +584,18 @@ export class DatabaseService<T> {
} }
// 创建统一表的服务实例 // 创建统一表的服务实例
export const chatSessionService = new DatabaseService(db.chatSessions); export const chatSessionService = new DatabaseService<ChatSession>(
export const contactUnifiedService = new DatabaseService(db.contactsUnified); () => databaseManager.getCurrentDatabase().chatSessions,
export const contactLabelMapService = new DatabaseService(db.contactLabelMap); );
export const userLoginRecordService = new DatabaseService(db.userLoginRecords); export const contactUnifiedService = new DatabaseService<Contact>(
() => databaseManager.getCurrentDatabase().contactsUnified,
);
export const contactLabelMapService = new DatabaseService<ContactLabelMap>(
() => databaseManager.getCurrentDatabase().contactLabelMap,
);
export const userLoginRecordService = new DatabaseService<UserLoginRecord>(
() => databaseManager.getCurrentDatabase().userLoginRecords,
);
// 默认导出数据库实例 // 默认导出数据库实例
export default db; export default db;