新增消息列表API,优化消息列表组件以支持加载状态和数据同步,提升用户体验和代码可读性。

This commit is contained in:
超级老白兔
2025-10-23 17:02:33 +08:00
parent 75e62cce72
commit 7f4bc0487e
9 changed files with 1343 additions and 104 deletions

View File

@@ -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;
}
}

View File

@@ -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 }) => {

View File

@@ -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<MessageListProps> = () => {
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<ChatSession[]>([]);
const [filteredSessions, setFilteredSessions] = useState<ChatSession[]>([]);
// 右键菜单相关状态
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<MessageListProps> = () => {
// 修改备注相关状态
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<MessageListProps> = () => {
const contextMenuRef = useRef<HTMLDivElement>(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<MessageListProps> = () => {
};
// 置顶/取消置顶
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<MessageListProps> = () => {
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<MessageListProps> = () => {
});
}
// 修改备注后会更新当前的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<MessageListProps> = () => {
};
}, [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 (
<div className={styles.messageList}>
{loading && <div className={styles.loading}>...</div>}
{refreshing && !loading && (
<div className={styles.refreshIndicator}>
<Spin size="small" /> ...
</div>
)}
<List
dataSource={chatSessions as (ContractData | weChatGroup)[]}
dataSource={filteredSessions as any[]}
renderItem={session => (
<List.Item
key={session.id}

View File

@@ -24,16 +24,33 @@ export interface Message {
[key: string]: any;
}
//Store State
//Store State - 会话列表状态管理(不存储数据,只管理状态)
export interface MessageState {
//消息列表
//加载状态
loading: boolean;
//后台同步状态
refreshing: boolean;
//刷新触发器(用于通知组件重新查询数据库)
refreshTrigger: number;
//最后刷新时间
lastRefreshTime: string | null;
//设置加载状态
setLoading: (loading: boolean) => 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;
}

View File

@@ -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<MessageState>()(
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();

View File

@@ -57,8 +57,28 @@ const messageHandlers: Record<string, MessageHandler> = {
},
//收到消息
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);

View File

@@ -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<KfUserListData, "id"> {
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<KfUserWithServerId>;
weChatGroup!: Table<weChatGroup>;
contracts!: Table<ContractData>;
newContractList!: Table<NewContactListData>;
newContactList!: Table<NewContactListData>;
messageList!: Table<MessageListData>;
// ==================== 新增统一表 ====================
chatSessions!: Table<ChatSession>; // 统一会话表
contactsUnified!: Table<Contact>; // 统一联系人表
contactLabelMap!: Table<ContactLabelMap>; // 联系人标签映射表
userLoginRecords!: Table<UserLoginRecord>; // 用户登录记录表
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<T> {
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;

View File

@@ -0,0 +1,7 @@
/**
* 数据库操作层统一导出
*/
export { MessageManager } from "./message";
export { ContactManager } from "./contact";
export { LoginManager } from "./loginManager";

View File

@@ -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<ChatSession[]> {
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<ChatSession | undefined> {
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<void> {
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<ChatSession> & {
userId: number;
id: number;
type: "friend" | "group";
},
): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<ChatSession>;
}> = [];
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<ChatSession>,
): 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<void> {
try {
await db.chatSessions.where("userId").equals(userId).delete();
console.log(`用户 ${userId} 的会话数据已清空`);
} catch (error) {
console.error("清空用户会话失败:", error);
}
}
}