diff --git a/Touchkebao/src/main.tsx b/Touchkebao/src/main.tsx index a3392df2..dde8d3ad 100644 --- a/Touchkebao/src/main.tsx +++ b/Touchkebao/src/main.tsx @@ -7,97 +7,18 @@ import dayjs from "dayjs"; import "dayjs/locale/zh-cn"; import App from "./App"; import "./styles/global.scss"; -import { db } from "./utils/db"; // 引入数据库实例 +import { initializeDatabaseFromPersistedUser } from "./utils/db"; // 设置dayjs为中文 dayjs.locale("zh-cn"); -// 清理旧数据库 -async function cleanupOldDatabase() { +async function bootstrap() { try { - // 获取所有数据库 - 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((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(); - }; - }); - } - } + await initializeDatabaseFromPersistedUser(); } catch (error) { - console.warn("清理旧数据库时出错(可忽略):", 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); + console.warn("Failed to prepare database before app bootstrap:", error); } - // 渲染应用 const root = createRoot(document.getElementById("root")!); root.render( @@ -106,5 +27,4 @@ async function initializeApp() { ); } -// 启动应用 -initializeApp(); +void bootstrap(); diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts index a6402538..a3627033 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts @@ -33,5 +33,5 @@ export interface GetGroupPushHistoryParams { [property: string]: any; } export const getPushHistory = async (params: GetGroupPushHistoryParams) => { - return request("/v1/workbench/group-push-history", { params }); + return request("/v1/workbench/group-push-history", params, "GET"); }; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx index d738db4b..831a60a2 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { Layout, Tabs } from "antd"; import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import styles from "./Person.module.scss"; @@ -9,6 +9,8 @@ import LayoutFiexd from "@/components/Layout/LayoutFiexd"; const { Sider } = Layout; +const noop = () => {}; + interface PersonProps { contract: ContractData | weChatGroup; } @@ -16,47 +18,46 @@ interface PersonProps { const Person: React.FC = ({ contract }) => { const [activeKey, setActiveKey] = useState("profile"); 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 ( setActiveKey(key)} - tabBarStyle={{ - padding: "0 30px", - }} - items={[ - { - key: "quickwords", - label: "快捷语录", - }, - { - key: "profile", - label: isGroup ? "群资料" : "个人资料", - }, - - ...(!isGroup - ? [ - { - key: "moments", - label: "朋友圈", - }, - ] - : []), - ]} + onChange={handleTabChange} + tabBarStyle={tabBarStyle} + items={tabItems} /> } > {activeKey === "profile" && } - {activeKey === "quickwords" && ( - {}} - onAdd={() => {}} - onRemove={() => {}} - /> - )} + {activeKey === "quickwords" && } {activeKey === "moments" && !isGroup && ( )} diff --git a/Touchkebao/src/store/module/user.ts b/Touchkebao/src/store/module/user.ts index 745af11c..7d1dea2f 100644 --- a/Touchkebao/src/store/module/user.ts +++ b/Touchkebao/src/store/module/user.ts @@ -1,5 +1,52 @@ import { createPersistStore } from "@/store/createPersistStore"; 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 { id: number; @@ -28,7 +75,7 @@ interface UserState { setToken: (token: string) => void; setToken2: (token2: string) => void; clearUser: () => void; - login: (token: string, userInfo: User) => void; + login: (token: string, userInfo: User) => Promise; login2: (token2: string) => void; logout: () => void; } @@ -39,12 +86,27 @@ export const useUserStore = createPersistStore( token: null, token2: null, 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 }), setToken2: token2 => set({ token2 }), - clearUser: () => - set({ user: null, token: null, token2: null, isLoggedIn: false }), - login: (token, userInfo) => { + clearUser: () => { + databaseManager.closeCurrentDatabase().catch(error => { + 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 localStorage.setItem("token", token); @@ -66,6 +128,11 @@ export const useUserStore = createPersistStore( lastLoginIp: userInfo.lastLoginIp, lastLoginTime: userInfo.lastLoginTime, }; + try { + await databaseManager.ensureDatabase(user.id); + } catch (error) { + console.error("Failed to initialize user database:", error); + } set({ user, token, isLoggedIn: true }); Toast.show({ content: "登录成功", position: "top" }); @@ -80,6 +147,10 @@ export const useUserStore = createPersistStore( // 清除localStorage中的token localStorage.removeItem("token"); 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 }); }, }), @@ -92,7 +163,11 @@ export const useUserStore = createPersistStore( isLoggedIn: state.isLoggedIn, }), 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); + }); + } }, }, ); diff --git a/Touchkebao/src/store/module/websocket/websocket.ts b/Touchkebao/src/store/module/websocket/websocket.ts index 68a17c45..359fcf4a 100644 --- a/Touchkebao/src/store/module/websocket/websocket.ts +++ b/Touchkebao/src/store/module/websocket/websocket.ts @@ -1,7 +1,7 @@ import { createPersistStore } from "@/store/createPersistStore"; -import { Toast } from "antd-mobile"; import { useUserStore } from "../user"; import { useCkChatStore } from "@/store/module/ckchat/ckchat"; +import { useCustomerStore } from "@/store/module/weChat/customer"; const { getAccountId } = useCkChatStore.getState(); import { msgManageCore } from "./msgManage"; // WebSocket消息类型 @@ -52,6 +52,7 @@ interface WebSocketState { reconnectAttempts: number; reconnectTimer: NodeJS.Timeout | null; aliveStatusTimer: NodeJS.Timeout | null; // 客服用户状态查询定时器 + aliveStatusUnsubscribe: (() => void) | null; // 方法 connect: (config: Partial) => void; @@ -97,6 +98,7 @@ export const useWebSocketStore = createPersistStore( reconnectAttempts: 0, reconnectTimer: null, aliveStatusTimer: null, + aliveStatusUnsubscribe: null, // 连接WebSocket connect: (config: Partial) => { @@ -405,7 +407,7 @@ export const useWebSocketStore = createPersistStore( }, // 内部方法:处理连接关闭 - _handleClose: (event: CloseEvent) => { + _handleClose: () => { const currentState = get(); // console.log("WebSocket连接关闭:", event.code, event.reason); @@ -431,7 +433,7 @@ export const useWebSocketStore = createPersistStore( }, // 内部方法:处理连接错误 - _handleError: (event: Event) => { + _handleError: () => { // console.error("WebSocket连接错误:", event); set({ status: WebSocketStatus.ERROR }); @@ -477,42 +479,84 @@ export const useWebSocketStore = createPersistStore( // 先停止现有定时器 currentState._stopAliveStatusTimer(); - // 获取客服用户列表 - const { kfUserList } = useCkChatStore.getState(); + const requestAliveStatus = () => { + const state = get(); + if (state.status !== WebSocketStatus.CONNECTED) { + return; + } - // 如果没有客服用户,不启动定时器 - if (!kfUserList || kfUserList.length === 0) { - return; - } + const { customerList } = useCustomerStore.getState(); + const { kfUserList } = useCkChatStore.getState(); + 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秒查询一次 const timer = setInterval(() => { const state = get(); // 检查连接状态 if (state.status === WebSocketStatus.CONNECTED) { - const { kfUserList: currentKfUserList } = useCkChatStore.getState(); - if (currentKfUserList && currentKfUserList.length > 0) { - state.sendCommand("CmdRequestWechatAccountsAliveStatus", { - wechatAccountIds: currentKfUserList.map(v => v.id), - }); - } + requestAliveStatus(); } else { // 如果连接断开,停止定时器 state._stopAliveStatusTimer(); } }, 5 * 1000); - set({ aliveStatusTimer: timer }); + set({ + aliveStatusTimer: timer, + aliveStatusUnsubscribe: () => { + unsubscribeCustomer(); + unsubscribeKf(); + }, + }); }, // 内部方法:停止客服状态查询定时器 _stopAliveStatusTimer: () => { const currentState = get(); + if (currentState.aliveStatusUnsubscribe) { + currentState.aliveStatusUnsubscribe(); + } + if (currentState.aliveStatusTimer) { clearInterval(currentState.aliveStatusTimer); - set({ aliveStatusTimer: null }); } + set({ aliveStatusTimer: null, aliveStatusUnsubscribe: null }); }, }), { diff --git a/Touchkebao/src/store/persistUtils.ts b/Touchkebao/src/store/persistUtils.ts index abfb1e8c..bcbc93bb 100644 --- a/Touchkebao/src/store/persistUtils.ts +++ b/Touchkebao/src/store/persistUtils.ts @@ -5,6 +5,12 @@ export const PERSIST_KEYS = { USER_STORE: "user-store", APP_STORE: "app-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; // 存储类型 diff --git a/Touchkebao/src/utils/db.ts b/Touchkebao/src/utils/db.ts index abd22d02..61bffeb8 100644 --- a/Touchkebao/src/utils/db.ts +++ b/Touchkebao/src/utils/db.ts @@ -16,6 +16,8 @@ */ import Dexie, { Table } from "dexie"; +import { getPersistedData, PERSIST_KEYS } from "@/store/persistUtils"; +const DB_NAME_PREFIX = "CunkebaoDatabase"; // ==================== 用户登录记录 ==================== export interface UserLoginRecord { @@ -123,8 +125,8 @@ class CunkebaoDatabase extends Dexie { contactLabelMap!: Table; // 联系人标签映射表 userLoginRecords!: Table; // 用户登录记录表 - constructor() { - super("CunkebaoDatabase"); + constructor(dbName: string) { + super(dbName); // 版本1:统一表结构 this.version(1).stores({ @@ -188,12 +190,148 @@ class CunkebaoDatabase extends Dexie { } } -// 创建数据库实例 -export const db = new CunkebaoDatabase(); +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 table: Table) {} + constructor(private readonly tableAccessor: () => Table) {} + + private get table(): Table { + return this.tableAccessor(); + } // 基础 CRUD 操作 - 使用serverId作为主键 async create(data: Omit): Promise { @@ -446,10 +584,18 @@ export class DatabaseService { } // 创建统一表的服务实例 -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 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;