diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/MessageList.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/MessageList.module.scss index b924c483..0bed38fc 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/MessageList.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/MessageList.module.scss @@ -170,4 +170,25 @@ } } } + + // 同步状态提示 + .refreshIndicator { + padding: 8px; + text-align: center; + font-size: 12px; + color: #999; + background: #f5f5f5; + border-bottom: 1px solid #e8e8e8; + + :global(.ant-spin) { + margin-right: 6px; + } + } + + // 加载状态 + .loading { + padding: 16px; + text-align: center; + color: #999; + } } diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api.ts b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api.ts index 186cc3db..81af8aa7 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api.ts @@ -1,4 +1,8 @@ import request from "@/api/request"; +//群、好友聊天记录列表 +export function getMessageList(params: { page: number; limit: number }) { + return request("/v1/kefu/message/list", params, "GET"); +} // 获取联系人列表 export const getContactList = (params: { prevId: string; count: number }) => { diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx index aac77a5f..cb2083bf 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useRef } from "react"; -import { List, Avatar, Badge, Modal, Input, message } from "antd"; +import { List, Avatar, Badge, Modal, Input, message, Spin } from "antd"; import { UserOutlined, TeamOutlined, @@ -7,39 +7,42 @@ import { DeleteOutlined, EditOutlined, } from "@ant-design/icons"; -import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import { useWeChatStore } from "@/store/module/weChat/weChat"; -import { - useCkChatStore, - toggleChatSessionPin, - deleteChatSession, -} from "@/store/module/ckchat/ckchat"; +import { useCkChatStore } from "@/store/module/ckchat/ckchat"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; import { updateConfig } from "@/pages/pc/ckbox/api"; +import { getMessageList } from "./api"; import { dataProcessing } from "./api"; import styles from "./MessageList.module.scss"; import { formatWechatTime } from "@/utils/common"; +import { MessageManager } from "@/utils/dbAction/message"; +import { ChatSession } from "@/utils/db"; +import { useMessageStore } from "@/store/module/weChat/message"; +import { useUserStore } from "@/store/module/user"; interface MessageListProps {} const MessageList: React.FC = () => { const { setCurrentContact, currentContract } = useWeChatStore(); - const getChatSessions = useCkChatStore(state => state.chatSessions); const kfSelected = useCkChatStore(state => state.kfSelected); - const { sendCommand } = useWebSocketStore(); - const onContactClick = (session: ContractData | weChatGroup) => { - setCurrentContact(session, true); - }; - const [chatSessions, setChatSessions] = useState< - (ContractData | weChatGroup)[] - >([]); const searchKeyword = useCkChatStore(state => state.searchKeyword); + const { sendCommand } = useWebSocketStore(); + const { user } = useUserStore(); + const currentUserId = user?.id || 0; + + // Store状态 + const { loading, refreshing, refreshTrigger, setLoading, setRefreshing } = + useMessageStore(); + + // 组件内部状态:会话列表数据 + const [sessions, setSessions] = useState([]); + const [filteredSessions, setFilteredSessions] = useState([]); // 右键菜单相关状态 const [contextMenu, setContextMenu] = useState<{ visible: boolean; x: number; y: number; - session: ContractData | weChatGroup | null; + session: ChatSession | null; }>({ visible: false, x: 0, @@ -50,7 +53,7 @@ const MessageList: React.FC = () => { // 修改备注相关状态 const [editRemarkModal, setEditRemarkModal] = useState<{ visible: boolean; - session: ContractData | weChatGroup | null; + session: ChatSession | null; remark: string; }>({ visible: false, @@ -61,10 +64,7 @@ const MessageList: React.FC = () => { const contextMenuRef = useRef(null); // 右键菜单事件处理 - const handleContextMenu = ( - e: React.MouseEvent, - session: ContractData | weChatGroup, - ) => { + const handleContextMenu = (e: React.MouseEvent, session: ChatSession) => { e.preventDefault(); e.stopPropagation(); @@ -87,53 +87,94 @@ const MessageList: React.FC = () => { }; // 置顶/取消置顶 - const handleTogglePin = (session: ContractData | weChatGroup) => { - const currentPinned = (session.config as any)?.top || false; - const newPinned = !currentPinned; // 切换置顶状态 + const handleTogglePin = async (session: ChatSession) => { + const currentPinned = session.config?.top || false; + const newPinned = !currentPinned; - updateConfig({ - id: session.id, - config: { top: newPinned, chat: true }, - }) - .then(() => { - // API调用成功,更新本地状态 - toggleChatSessionPin(session.id!, newPinned); - message.success(`${newPinned ? "置顶" : "取消置顶"}成功`); - }) - .catch(() => { - message.error(`${newPinned ? "置顶" : "取消置顶"}失败`); + try { + // 1. 立即更新UI(乐观更新) + setSessions(prev => + prev.map(s => + s.id === session.id + ? { + ...s, + config: { ...s.config, top: newPinned }, + sortKey: "", // 会重新计算 + } + : s, + ), + ); + + // 2. 后台调用API + await updateConfig({ + id: session.id, + config: { top: newPinned, chat: true }, }); + + // 3. 后台更新数据库 + await MessageManager.togglePin( + currentUserId, + session.id, + session.type, + newPinned, + ); + + message.success(`${newPinned ? "置顶" : "取消置顶"}成功`); + } catch (error) { + // 4. 失败时回滚UI + setSessions(prev => + prev.map(s => + s.id === session.id + ? { ...s, config: { ...s.config, top: currentPinned } } + : s, + ), + ); + message.error(`${newPinned ? "置顶" : "取消置顶"}失败`); + } + hideContextMenu(); }; // 删除会话 - const handleDelete = (session: ContractData | weChatGroup) => { + const handleDelete = (session: ChatSession) => { Modal.confirm({ title: "确认删除", content: `确定要删除与 ${session.conRemark || session.nickname} 的会话吗?`, - onOk: () => { - updateConfig({ - id: session.id, - config: { chat: false }, - }) - .then(() => { - message.success(`删除成功`); - //根据id删除会话里Item - deleteChatSession(session.id); - }) - .catch(() => { - message.error(`删除失败`); + onOk: async () => { + try { + // 1. 立即从UI移除 + setSessions(prev => prev.filter(s => s.id !== session.id)); + + // 2. 后台调用API + await updateConfig({ + id: session.id, + config: { chat: false }, }); + + // 3. 后台从数据库删除 + await MessageManager.deleteSession( + currentUserId, + session.id, + session.type, + ); + + message.success("删除成功"); + } catch (error) { + // 4. 失败时恢复UI + setSessions(prev => [...prev, session]); + message.error("删除失败"); + } + hideContextMenu(); }, }); }; // 修改备注 - const handleEditRemark = (session: ContractData | weChatGroup) => { + const handleEditRemark = (session: ChatSession) => { setEditRemarkModal({ visible: true, - session, + session: session as any, remark: session.conRemark || "", }); hideContextMenu(); @@ -145,13 +186,25 @@ const MessageList: React.FC = () => { const session = editRemarkModal.session; const isGroup = "chatroomId" in session; + const sessionData = sessions.find(s => s.id === session.id); + if (!sessionData) return; + + const oldRemark = session.conRemark; try { + // 1. 立即更新UI + setSessions(prev => + prev.map(s => + s.id === session.id ? { ...s, conRemark: editRemarkModal.remark } : s, + ), + ); + + // 2. 后台调用API if (isGroup) { // 群聊备注修改 sendCommand("CmdModifyGroupRemark", { wechatAccountId: session.wechatAccountId, - chatroomId: session.chatroomId, + chatroomId: (session as any).chatroomId, newRemark: editRemarkModal.remark, }); await dataProcessing({ @@ -175,18 +228,22 @@ const MessageList: React.FC = () => { }); } - // 修改备注后会更新当前的session的conRemark,然后使用updateChatSession方法更新会话列表 - const updatedSession = { - ...session, - conRemark: editRemarkModal.remark, - }; - - // 更新会话列表中的备注 - const { updateChatSession } = useCkChatStore.getState(); - updateChatSession(updatedSession); + // 3. 后台更新数据库 + await MessageManager.updateRemark( + currentUserId, + session.id, + sessionData.type, + editRemarkModal.remark, + ); message.success("备注更新成功"); } catch (error) { + // 4. 失败时回滚UI + setSessions(prev => + prev.map(s => + s.id === session.id ? { ...s, conRemark: oldRemark } : s, + ), + ); console.error("更新备注失败:", error); message.error("备注更新失败,请重试"); } @@ -218,32 +275,254 @@ const MessageList: React.FC = () => { }; }, [contextMenu.visible]); + // ==================== 数据加载 ==================== + + // 与服务器同步数据 + const syncWithServer = async () => { + if (!currentUserId) return; + + try { + // 获取会话列表数据(分页获取所有数据) + let allMessages: any[] = []; + let page = 1; + const limit = 500; + let hasMore = true; + + // 分页获取会话列表 + while (hasMore) { + const result: any = await getMessageList({ + page, + limit, + }); + + if (!result || !Array.isArray(result) || result.length === 0) { + hasMore = false; + break; + } + + allMessages = [...allMessages, ...result]; + + if (result.length < limit) { + hasMore = false; + } else { + page++; + } + } + + // 分离好友和群聊数据 + const friends = allMessages.filter( + (msg: any) => msg.dataType === "friend" || !msg.chatroomId, + ); + const groups = allMessages.filter( + (msg: any) => msg.dataType === "group" || msg.chatroomId, + ); + + // 执行增量同步 + const syncResult = await MessageManager.syncSessions(currentUserId, { + friends, + groups, + }); + + console.log( + `会话同步完成: 新增${syncResult.added}, 更新${syncResult.updated}, 删除${syncResult.deleted}`, + ); + } catch (error) { + console.error("同步服务器数据失败:", error); + } + }; + + // 初始化加载会话列表 useEffect(() => { - let filteredSessions = getChatSessions; + const initializeSessions = async () => { + if (!currentUserId) return; + + setLoading(true); + + try { + // 1. 优先从本地数据库加载 + const cachedSessions = + await MessageManager.getUserSessions(currentUserId); + + if (cachedSessions.length > 0) { + // 有缓存数据,立即显示 + setSessions(cachedSessions); + setLoading(false); + + // 2. 后台静默同步 + setRefreshing(true); + await syncWithServer(); + } else { + // 无缓存,直接API加载 + await syncWithServer(); + const newSessions = + await MessageManager.getUserSessions(currentUserId); + setSessions(newSessions); + setLoading(false); + } + } catch (error) { + console.error("初始化会话列表失败:", error); + setLoading(false); + } finally { + setRefreshing(false); + } + }; + + initializeSessions(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentUserId]); + + // 监听refreshTrigger,重新查询数据库 + useEffect(() => { + const refreshSessions = async () => { + if (!currentUserId || refreshTrigger === 0) return; + + try { + const updatedSessions = + await MessageManager.getUserSessions(currentUserId); + setSessions(updatedSessions); + } catch (error) { + console.error("刷新会话列表失败:", error); + } + }; + + refreshSessions(); + }, [refreshTrigger, currentUserId]); + + // 根据客服和搜索关键词筛选会话 + useEffect(() => { + let filtered = [...sessions]; // 根据客服筛选 if (kfSelected !== 0) { - filteredSessions = filteredSessions.filter( - v => v.wechatAccountId === kfSelected, - ); + filtered = filtered.filter(v => v.wechatAccountId === kfSelected); } // 根据搜索关键词进行模糊匹配 if (searchKeyword.trim()) { const keyword = searchKeyword.toLowerCase(); - filteredSessions = filteredSessions.filter(v => { + filtered = filtered.filter(v => { const nickname = (v.nickname || "").toLowerCase(); const conRemark = (v.conRemark || "").toLowerCase(); return nickname.includes(keyword) || conRemark.includes(keyword); }); } - setChatSessions(filteredSessions); - }, [getChatSessions, kfSelected, searchKeyword]); + setFilteredSessions(filtered); + }, [sessions, kfSelected, searchKeyword]); + + // ==================== WebSocket消息处理 ==================== + + // 监听WebSocket消息更新 + useEffect(() => { + const handleNewMessage = (event: CustomEvent) => { + const { message: msgData, sessionId, type } = event.detail; + + // 立即更新组件state + setSessions(prev => { + const existingIndex = prev.findIndex(s => s.id === sessionId); + + if (existingIndex >= 0) { + // 更新现有会话 + const updatedSession = { + ...prev[existingIndex], + config: { + ...prev[existingIndex].config, + unreadCount: prev[existingIndex].config.unreadCount + 1, + }, + content: msgData.content, + lastUpdateTime: new Date().toISOString(), + }; + + // 重新计算sortKey(会触发重新排序) + const newSessions = [...prev]; + newSessions[existingIndex] = updatedSession; + + // 按sortKey降序重新排序(最新的在前面) + return newSessions.sort((a, b) => { + const aKey = MessageManager["generateSortKey"](a); + const bKey = MessageManager["generateSortKey"](b); + return bKey.localeCompare(aKey); // 降序:b在前,a在后 + }); + } else { + // 新会话,插入到列表中 + const newSession: ChatSession = { + serverId: `${type}_${sessionId}`, + userId: currentUserId, + id: sessionId, + type, + wechatAccountId: msgData.wechatAccountId, + nickname: msgData.nickname || "", + conRemark: "", + avatar: msgData.avatar || "", + content: msgData.content, + lastUpdateTime: new Date().toISOString(), + config: { + unreadCount: 1, + top: false, + }, + sortKey: "", + }; + + return [newSession, ...prev]; + } + }); + + // 后台更新数据库 + MessageManager.updateSessionOnNewMessage( + currentUserId, + sessionId, + type, + msgData, + ); + }; + + window.addEventListener( + "chatMessageReceived", + handleNewMessage as EventListener, + ); + + return () => { + window.removeEventListener( + "chatMessageReceived", + handleNewMessage as EventListener, + ); + }; + }, [currentUserId]); + + // ==================== 会话操作 ==================== + + // 点击会话 + const onContactClick = async (session: ChatSession) => { + // 设置当前会话 + setCurrentContact(session as any, true); + + // 标记为已读 + if (session.config.unreadCount > 0) { + // 立即更新UI + setSessions(prev => + prev.map(s => + s.id === session.id + ? { ...s, config: { ...s.config, unreadCount: 0 } } + : s, + ), + ); + + // 后台更新数据库 + MessageManager.markAsRead(currentUserId, session.id, session.type); + } + }; + return (
+ {loading &&
加载中...
} + {refreshing && !loading && ( +
+ 正在同步... +
+ )} + ( void; + //设置同步状态 + setRefreshing: (refreshing: boolean) => void; + //触发刷新(通知组件重新查询) + triggerRefresh: () => void; + + // ==================== 保留原有接口(向后兼容) ==================== + //消息列表(废弃,保留兼容) messageList: Message[]; - //当前选中的消息 + //当前选中的消息(废弃,保留兼容) currentMessage: Message | null; - //更新消息列表 + //更新消息列表(废弃,保留兼容) updateMessageList: (messageList: Message[]) => void; - //更新消息状态 + //更新消息状态(废弃,保留兼容) updateMessageStatus: (messageId: number, status: string) => void; - //更新当前选中的消息 + //更新当前选中的消息(废弃,保留兼容) updateCurrentMessage: (message: Message) => void; } diff --git a/Touchkebao/src/store/module/weChat/message.ts b/Touchkebao/src/store/module/weChat/message.ts index 60e6e74e..52533cee 100644 --- a/Touchkebao/src/store/module/weChat/message.ts +++ b/Touchkebao/src/store/module/weChat/message.ts @@ -1,24 +1,48 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { Message, MessageState } from "./message.data"; + +/** + * 会话列表状态管理Store + * 职责:只管理状态,不存储会话列表数据 + * 数据存储在:组件state + IndexedDB + */ export const useMessageStore = create()( persist( (set, get) => ({ - messageList: [], //消息列表 - currentMessage: null, //当前选中的消息 - updateMessageList: (messageList: Message[]) => set({ messageList }), //更新消息列表 + // ==================== 新增状态管理 ==================== + loading: false, + refreshing: false, + refreshTrigger: 0, + lastRefreshTime: null, + + setLoading: (loading: boolean) => set({ loading }), + setRefreshing: (refreshing: boolean) => set({ refreshing }), + triggerRefresh: () => + set({ + refreshTrigger: get().refreshTrigger + 1, + lastRefreshTime: new Date().toISOString(), + }), + + // ==================== 保留原有接口(向后兼容) ==================== + messageList: [], + currentMessage: null, + updateMessageList: (messageList: Message[]) => set({ messageList }), updateCurrentMessage: (message: Message) => - set({ currentMessage: message }), //更新当前选中的消息 + set({ currentMessage: message }), updateMessageStatus: (messageId: number, status: string) => set({ messageList: get().messageList.map(message => message.id === messageId ? { ...message, status } : message, ), - }), //更新消息状态 + }), }), { name: "message-storage", partialize: state => ({ + // 只持久化必要的状态,不持久化数据 + lastRefreshTime: state.lastRefreshTime, + // 保留原有持久化字段(向后兼容) messageList: [], currentMessage: null, }), @@ -54,3 +78,24 @@ export const getCurrentMessage = () => */ export const updateMessageStatus = (messageId: number, status: string) => useMessageStore.getState().updateMessageStatus(messageId, status); + +// ==================== 新增导出函数 ==================== + +/** + * 设置加载状态 + * @param loading 加载状态 + */ +export const setLoading = (loading: boolean) => + useMessageStore.getState().setLoading(loading); + +/** + * 设置同步状态 + * @param refreshing 同步状态 + */ +export const setRefreshing = (refreshing: boolean) => + useMessageStore.getState().setRefreshing(refreshing); + +/** + * 触发刷新(通知组件重新查询数据库) + */ +export const triggerRefresh = () => useMessageStore.getState().triggerRefresh(); diff --git a/Touchkebao/src/store/module/websocket/msgManage.ts b/Touchkebao/src/store/module/websocket/msgManage.ts index 2a9166b6..64559e53 100644 --- a/Touchkebao/src/store/module/websocket/msgManage.ts +++ b/Touchkebao/src/store/module/websocket/msgManage.ts @@ -57,8 +57,28 @@ const messageHandlers: Record = { }, //收到消息 CmdNewMessage: (message: Messages) => { - // 在这里添加具体的处理逻辑 + // 处理消息本身 receivedMsg(message.friendMessage || message.chatroomMessage); + + // 触发会话列表更新事件 + const msgData = message.friendMessage || message.chatroomMessage; + if (msgData) { + const sessionId = message.friendMessage + ? message.friendMessage.wechatFriendId + : message.chatroomMessage?.wechatChatroomId; + const type = message.friendMessage ? "friend" : "group"; + + // 发送自定义事件通知MessageList组件 + window.dispatchEvent( + new CustomEvent("chatMessageReceived", { + detail: { + message: msgData, + sessionId, + type, + }, + }), + ); + } }, CmdFriendInfoChanged: () => { // console.log("好友信息变更", message); diff --git a/Touchkebao/src/utils/db.ts b/Touchkebao/src/utils/db.ts index df673a94..9a8559e4 100644 --- a/Touchkebao/src/utils/db.ts +++ b/Touchkebao/src/utils/db.ts @@ -4,25 +4,15 @@ * 架构设计: * 1. 使用serverId作为数据库主键,直接对应接口返回的id字段 * 2. 保留原始的id字段,用于存储接口数据的完整性 - * 3. 简化数据处理逻辑,避免ID映射的复杂性 + * 3. 添加userId字段实现多用户数据隔离 + * 4. 统一会话表和联系人表,兼容好友和群聊 * * 优势: * - 直接使用服务器ID作为主键,避免ID冲突 - * - 保持数据的一致性和可追溯性 - * - 简化查询逻辑,提高性能 - * - 支持重复数据检测和去重 - * - * 使用方法: - * - 存储接口数据:使用 createWithServerId() 或 createManyWithServerId() - * - 查询数据:使用 findById(id) 根据原始ID查询,或 findByPrimaryKey(serverId) 根据主键查询 - * - 批量查询:使用 findByIds([id1, id2, ...]) 根据原始ID批量查询 - * - 内部操作:serverId作为主键用于数据库内部管理 - * - * 示例: - * const serverData = { id: 1001, name: '测试', ... }; // 接口返回的数据 - * const serverId = await service.createWithServerId(serverData); // 存储,返回serverId - * const data = await service.findById(1001); // 根据原始ID查询(用户友好) - * const dataByPK = await service.findByPrimaryKey(serverId); // 根据主键查询(内部使用) + * - 通过userId实现多用户数据隔离 + * - 统一的会话和联系人表结构,兼容好友和群聊 + * - 支持复合索引,提高查询性能 + * - 支持用户登录记录和自动清理 */ import Dexie, { Table } from "dexie"; @@ -33,33 +23,136 @@ import { MessageListData, } from "@/pages/pc/ckbox/data"; -// 数据类型定义,使用serverId作为主键 +// ==================== 用户登录记录 ==================== +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; // 性别 + 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: ContractData[]; - weChatGroup: weChatGroup[]; + contacts: Contact[]; + weChatGroup: Contact[]; } // 数据库类 class CunkebaoDatabase extends Dexie { + // ==================== 保留原有表(向后兼容) ==================== kfUsers!: Table; weChatGroup!: Table; contracts!: Table; - newContractList!: Table; + newContactList!: Table; messageList!: Table; + // ==================== 新增统一表 ==================== + chatSessions!: Table; // 统一会话表 + contactsUnified!: Table; // 统一联系人表 + contactLabelMap!: Table; // 联系人标签映射表 + userLoginRecords!: Table; // 用户登录记录表 + constructor() { super("CunkebaoDatabase"); - // 版本1:使用serverId作为主键的架构 + // 版本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", @@ -67,10 +160,64 @@ class CunkebaoDatabase extends Dexie { "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", - newContractList: "serverId, id, groupName, contacts", + 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", + }); } } @@ -335,8 +482,14 @@ export class DatabaseService { 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.newContractList); +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; diff --git a/Touchkebao/src/utils/dbAction/index.ts b/Touchkebao/src/utils/dbAction/index.ts new file mode 100644 index 00000000..3152e5db --- /dev/null +++ b/Touchkebao/src/utils/dbAction/index.ts @@ -0,0 +1,7 @@ +/** + * 数据库操作层统一导出 + */ + +export { MessageManager } from "./message"; +export { ContactManager } from "./contact"; +export { LoginManager } from "./loginManager"; diff --git a/Touchkebao/src/utils/dbAction/message.ts b/Touchkebao/src/utils/dbAction/message.ts index e69de29b..d741c75a 100644 --- a/Touchkebao/src/utils/dbAction/message.ts +++ b/Touchkebao/src/utils/dbAction/message.ts @@ -0,0 +1,693 @@ +/** + * 会话列表数据库操作管理器 + * 职责: + * 1. 会话数据的增删改查 + * 2. 增量同步逻辑(对比本地和服务器数据) + * 3. 好友/群聊数据转换为统一格式 + * 4. 提供回调机制通知组件更新 + */ + +import { db, chatSessionService, ChatSession } from "../db"; +import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; + +export class MessageManager { + private static updateCallbacks = new Set<(sessions: ChatSession[]) => void>(); + + // ==================== 回调管理 ==================== + + /** + * 注册会话更新回调 + * @param callback 回调函数 + * @returns 取消注册的函数 + */ + static onSessionsUpdate(callback: (sessions: ChatSession[]) => void) { + this.updateCallbacks.add(callback); + return () => this.updateCallbacks.delete(callback); + } + + /** + * 触发所有注册的回调 + * @param userId 用户ID + */ + private static async triggerCallbacks(userId: number) { + try { + const sessions = await this.getUserSessions(userId); + this.updateCallbacks.forEach(callback => { + try { + callback(sessions); + } catch (error) { + console.error("会话更新回调执行失败:", error); + } + }); + } catch (error) { + console.error("触发回调失败:", error); + } + } + + // ==================== 数据转换 ==================== + + /** + * 生成会话排序键(微信排序方式) + * @param session 会话数据 + * @returns 排序键 + */ + private static generateSortKey(session: any): string { + const isTop = session.config?.top ? 0 : 1; + // 时间戳转换:使用9e15减去时间戳,这样最新的时间sortKey最小 + // 9e15 是一个足够大的数字,确保结果为正数 + const timestamp = new Date(session.lastUpdateTime || new Date()).getTime(); + const time = 9e15 - timestamp; + const displayName = ( + session.conRemark || + session.nickname || + "" + ).toLowerCase(); + + // 格式:置顶标识|时间反转值(补齐15位)|显示名称 + // sortKey越小,排序越靠前 + // 最新的消息time值最小,所以排在最前面 + const timeStr = String(Math.floor(time)).padStart(16, "0"); + return `${isTop}|${timeStr}|${displayName}`; + } + + /** + * 转换好友会话为统一格式 + * @param friend 好友数据 + * @param userId 用户ID + * @returns 统一会话格式 + */ + private static convertFriendToChatSession( + friend: ContractData, + userId: number, + ): ChatSession { + return { + serverId: `friend_${friend.id}`, + userId, + id: friend.id!, + type: "friend", + wechatAccountId: friend.wechatAccountId, + nickname: friend.nickname, + conRemark: friend.conRemark, + avatar: friend.avatar || "", + content: (friend as any).content || "", + lastUpdateTime: friend.lastUpdateTime || new Date().toISOString(), + config: { + unreadCount: friend.config?.unreadCount || 0, + top: (friend.config as any)?.top || false, + }, + sortKey: this.generateSortKey(friend), + wechatFriendId: friend.id, + wechatId: friend.wechatId, + alias: friend.alias, + }; + } + + /** + * 转换群聊会话为统一格式 + * @param group 群聊数据 + * @param userId 用户ID + * @returns 统一会话格式 + */ + private static convertGroupToChatSession( + group: weChatGroup, + userId: number, + ): ChatSession { + return { + serverId: `group_${group.id}`, + userId, + id: group.id!, + type: "group", + wechatAccountId: group.wechatAccountId, + nickname: group.nickname, + conRemark: group.conRemark, + avatar: group.chatroomAvatar || "", + content: (group as any).content || "", + lastUpdateTime: (group as any).lastUpdateTime || new Date().toISOString(), + config: { + unreadCount: (group.config as any)?.unreadCount || 0, + top: (group.config as any)?.top || false, + }, + sortKey: this.generateSortKey(group), + chatroomId: group.chatroomId, + chatroomOwner: group.chatroomOwner, + selfDisplayName: group.selfDisplyName, + notice: group.notice, + }; + } + + // ==================== 查询操作 ==================== + + /** + * 获取用户的所有会话(已排序) + * @param userId 用户ID + * @returns 会话列表 + */ + static async getUserSessions(userId: number): Promise { + try { + // 按sortKey降序排序查询(最新的在前面) + const sessions = await db.chatSessions + .where("userId") + .equals(userId) + .reverse() + .sortBy("sortKey"); + + return sessions; + } catch (error) { + console.error("获取用户会话失败:", error); + return []; + } + } + + /** + * 根据ID查找会话 + * @param userId 用户ID + * @param sessionId 会话ID + * @returns 会话数据 + */ + static async getSessionById( + userId: number, + sessionId: number, + ): Promise { + try { + return await db.chatSessions + .where(["userId", "id"]) + .equals([userId, sessionId]) + .first(); + } catch (error) { + console.error("查找会话失败:", error); + return undefined; + } + } + + // ==================== 同步操作 ==================== + + /** + * 判断会话是否需要更新 + * @param local 本地会话 + * @param server 服务器会话 + * @returns 是否需要更新 + */ + private static needsUpdate(local: ChatSession, server: ChatSession): boolean { + const fieldsToCompare = [ + "content", + "lastUpdateTime", + "nickname", + "conRemark", + "avatar", + ]; + + for (const field of fieldsToCompare) { + if ( + JSON.stringify((local as any)[field]) !== + JSON.stringify((server as any)[field]) + ) { + return true; + } + } + + // 检查config对象 + if ( + local.config.unreadCount !== server.config.unreadCount || + local.config.top !== server.config.top + ) { + return true; + } + + return false; + } + + /** + * 增量同步会话数据 + * @param userId 用户ID + * @param serverData 服务器数据 + * @returns 同步结果统计 + */ + static async syncSessions( + userId: number, + serverData: { + friends?: ContractData[]; + groups?: weChatGroup[]; + }, + ): Promise<{ + added: number; + updated: number; + deleted: number; + }> { + return await db.transaction("rw", [db.chatSessions], async () => { + // 1. 获取本地现有会话 + const localSessions = (await chatSessionService.findWhere( + "userId", + userId, + )) as ChatSession[]; + const localSessionMap = new Map(localSessions.map(s => [s.id, s])); + + // 2. 转换服务器数据为统一格式 + const serverSessions: ChatSession[] = []; + + // 处理好友会话 + if (serverData.friends) { + const friends = serverData.friends + .filter(f => (f.config as any)?.chat === true) // 只要开启会话的 + .map(friend => this.convertFriendToChatSession(friend, userId)); + serverSessions.push(...friends); + } + + // 处理群聊会话 + if (serverData.groups) { + const groups = serverData.groups + .filter(g => (g.config as any)?.chat === true) // 只要开启会话的 + .map(group => this.convertGroupToChatSession(group, userId)); + serverSessions.push(...groups); + } + + const serverSessionMap = new Map(serverSessions.map(s => [s.id, s])); + + // 3. 计算差异 + const toAdd: ChatSession[] = []; + const toUpdate: ChatSession[] = []; + const toDelete: number[] = []; + + // 检查新增和更新 + for (const serverSession of serverSessions) { + const localSession = localSessionMap.get(serverSession.id); + + if (!localSession) { + toAdd.push(serverSession); + } else { + if (this.needsUpdate(localSession, serverSession)) { + toUpdate.push(serverSession); + } + } + } + + // 检查删除 + for (const localSession of localSessions) { + if (!serverSessionMap.has(localSession.id)) { + toDelete.push(localSession.id); + } + } + + // 4. 执行同步操作 + let added = 0, + updated = 0, + deleted = 0; + + if (toAdd.length > 0) { + await this.batchAddSessions(toAdd); + added = toAdd.length; + } + + if (toUpdate.length > 0) { + await this.batchUpdateSessions(toUpdate); + updated = toUpdate.length; + } + + if (toDelete.length > 0) { + await this.batchDeleteSessions(userId, toDelete); + deleted = toDelete.length; + } + + console.log(`会话同步完成: 新增${added}, 更新${updated}, 删除${deleted}`); + + // 5. 触发回调通知组件 + await this.triggerCallbacks(userId); + + return { added, updated, deleted }; + }); + } + + // ==================== 增删改操作 ==================== + + /** + * 批量新增会话 + * @param sessions 会话列表 + */ + private static async batchAddSessions(sessions: ChatSession[]) { + if (sessions.length === 0) return; + + const dataToInsert = sessions.map(session => ({ + ...session, + serverId: `${session.type}_${session.id}`, + })); + + await db.chatSessions.bulkAdd(dataToInsert); + } + + /** + * 批量更新会话 + * @param sessions 会话列表 + */ + private static async batchUpdateSessions(sessions: ChatSession[]) { + if (sessions.length === 0) return; + + for (const session of sessions) { + const serverId = `${session.type}_${session.id}`; + await chatSessionService.update(serverId, session); + } + } + + /** + * 批量删除会话 + * @param userId 用户ID + * @param sessionIds 会话ID列表 + */ + private static async batchDeleteSessions( + userId: number, + sessionIds: number[], + ) { + if (sessionIds.length === 0) return; + + await db.chatSessions + .where("userId") + .equals(userId) + .and(session => sessionIds.includes(session.id)) + .delete(); + } + + /** + * 新增单个会话 + * @param session 会话数据 + */ + static async addSession(session: ChatSession): Promise { + try { + const dataToInsert = { + ...session, + serverId: `${session.type}_${session.id}`, + sortKey: this.generateSortKey(session), + }; + + await db.chatSessions.add(dataToInsert); + await this.triggerCallbacks(session.userId); + } catch (error) { + console.error("新增会话失败:", error); + throw error; + } + } + + /** + * 更新单个会话 + * @param session 会话数据 + */ + static async updateSession( + session: Partial & { + userId: number; + id: number; + type: "friend" | "group"; + }, + ): Promise { + try { + const serverId = `${session.type}_${session.id}`; + const updateData = { + ...session, + sortKey: this.generateSortKey(session), + }; + + await chatSessionService.update(serverId, updateData); + await this.triggerCallbacks(session.userId); + } catch (error) { + console.error("更新会话失败:", error); + throw error; + } + } + + /** + * 删除单个会话 + * @param userId 用户ID + * @param sessionId 会话ID + * @param type 会话类型 + */ + static async deleteSession( + userId: number, + sessionId: number, + type: "friend" | "group", + ): Promise { + try { + const serverId = `${type}_${sessionId}`; + await chatSessionService.delete(serverId); + await this.triggerCallbacks(userId); + } catch (error) { + console.error("删除会话失败:", error); + throw error; + } + } + + // ==================== 特殊操作 ==================== + + /** + * 从联系人数据构建会话(发起新会话时使用) + * @param contact 联系人数据(好友或群聊) + * @param userId 用户ID + * @returns 会话数据 + */ + static buildSessionFromContact( + contact: ContractData | weChatGroup, + userId: number, + ): ChatSession { + const isGroup = "chatroomId" in contact; + + if (isGroup) { + // 群聊 + return this.convertGroupToChatSession(contact as weChatGroup, userId); + } else { + // 好友 + return this.convertFriendToChatSession(contact as ContractData, userId); + } + } + + /** + * 更新会话的最新消息(WebSocket消息到达时使用) + * @param userId 用户ID + * @param sessionId 会话ID + * @param type 会话类型 + * @param message 消息内容 + */ + static async updateSessionOnNewMessage( + userId: number, + sessionId: number, + type: "friend" | "group", + message: { + content: string; + }, + ): Promise { + try { + const serverId = `${type}_${sessionId}`; + const session = (await chatSessionService.findByPrimaryKey( + serverId, + )) as ChatSession; + + if (session) { + const updatedSession = { + ...session, + config: { + ...session.config, + unreadCount: (session.config?.unreadCount || 0) + 1, + }, + content: message.content, + lastUpdateTime: new Date().toISOString(), + }; + + updatedSession.sortKey = this.generateSortKey(updatedSession); + + await chatSessionService.update(serverId, updatedSession); + await this.triggerCallbacks(userId); + } + } catch (error) { + console.error("更新会话消息失败:", error); + } + } + + /** + * 标记会话为已读 + * @param userId 用户ID + * @param sessionId 会话ID + * @param type 会话类型 + */ + static async markAsRead( + userId: number, + sessionId: number, + type: "friend" | "group", + ): Promise { + try { + const serverId = `${type}_${sessionId}`; + const session = (await chatSessionService.findByPrimaryKey( + serverId, + )) as ChatSession; + + if (session && session.config.unreadCount > 0) { + await chatSessionService.update(serverId, { + config: { + ...session.config, + unreadCount: 0, + }, + }); + // 不触发回调,因为只是已读状态变化,不需要重新排序 + } + } catch (error) { + console.error("标记已读失败:", error); + } + } + + /** + * 置顶/取消置顶会话 + * @param userId 用户ID + * @param sessionId 会话ID + * @param type 会话类型 + * @param isPinned 是否置顶 + */ + static async togglePin( + userId: number, + sessionId: number, + type: "friend" | "group", + isPinned: boolean, + ): Promise { + try { + const serverId = `${type}_${sessionId}`; + const session = (await chatSessionService.findByPrimaryKey( + serverId, + )) as ChatSession; + + if (session) { + const updatedSession = { + ...session, + config: { + ...session.config, + top: isPinned, + }, + }; + + updatedSession.sortKey = this.generateSortKey(updatedSession); + + await chatSessionService.update(serverId, updatedSession); + await this.triggerCallbacks(userId); + } + } catch (error) { + console.error("置顶操作失败:", error); + throw error; + } + } + + /** + * 更新会话备注 + * @param userId 用户ID + * @param sessionId 会话ID + * @param type 会话类型 + * @param remark 新备注 + */ + static async updateRemark( + userId: number, + sessionId: number, + type: "friend" | "group", + remark: string, + ): Promise { + try { + const serverId = `${type}_${sessionId}`; + const session = (await chatSessionService.findByPrimaryKey( + serverId, + )) as ChatSession; + + if (session) { + const updatedSession = { + ...session, + conRemark: remark, + }; + + updatedSession.sortKey = this.generateSortKey(updatedSession); + + await chatSessionService.update(serverId, updatedSession); + await this.triggerCallbacks(userId); + } + } catch (error) { + console.error("更新备注失败:", error); + throw error; + } + } + + // ==================== 批量操作优化 ==================== + + private static updateBuffer: Array<{ + userId: number; + sessionId: number; + type: "friend" | "group"; + updates: Partial; + }> = []; + private static bufferTimer: NodeJS.Timeout | null = null; + + /** + * 批量更新会话(用于WebSocket消息批处理) + * @param userId 用户ID + * @param sessionId 会话ID + * @param type 会话类型 + * @param updates 更新内容 + */ + static batchUpdateSession( + userId: number, + sessionId: number, + type: "friend" | "group", + updates: Partial, + ): void { + this.updateBuffer.push({ userId, sessionId, type, updates }); + + if (this.bufferTimer) { + clearTimeout(this.bufferTimer); + } + + this.bufferTimer = setTimeout(async () => { + await this.flushUpdateBuffer(); + }, 100); // 100ms批量处理 + } + + /** + * 刷新更新缓冲区 + */ + private static async flushUpdateBuffer() { + if (this.updateBuffer.length === 0) return; + + const buffer = [...this.updateBuffer]; + this.updateBuffer = []; + + try { + await db.transaction("rw", [db.chatSessions], async () => { + for (const item of buffer) { + const serverId = `${item.type}_${item.sessionId}`; + const session = (await chatSessionService.findByPrimaryKey( + serverId, + )) as ChatSession; + + if (session) { + const updatedSession = { + ...session, + ...item.updates, + }; + + updatedSession.sortKey = this.generateSortKey(updatedSession); + + await chatSessionService.update(serverId, updatedSession); + } + } + }); + + // 触发回调 + const userIds = new Set(buffer.map(item => item.userId)); + for (const userId of userIds) { + await this.triggerCallbacks(userId); + } + } catch (error) { + console.error("批量更新会话失败:", error); + } + } + + // ==================== 清理操作 ==================== + + /** + * 清空指定用户的所有会话 + * @param userId 用户ID + */ + static async clearUserSessions(userId: number): Promise { + try { + await db.chatSessions.where("userId").equals(userId).delete(); + console.log(`用户 ${userId} 的会话数据已清空`); + } catch (error) { + console.error("清空用户会话失败:", error); + } + } +}