新增消息列表API,优化消息列表组件以支持加载状态和数据同步,提升用户体验和代码可读性。
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
7
Touchkebao/src/utils/dbAction/index.ts
Normal file
7
Touchkebao/src/utils/dbAction/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 数据库操作层统一导出
|
||||
*/
|
||||
|
||||
export { MessageManager } from "./message";
|
||||
export { ContactManager } from "./contact";
|
||||
export { LoginManager } from "./loginManager";
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user