新增消息列表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";
|
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 }) => {
|
export const getContactList = (params: { prevId: string; count: number }) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useRef } from "react";
|
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 {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
@@ -7,39 +7,42 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
import {
|
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||||
useCkChatStore,
|
|
||||||
toggleChatSessionPin,
|
|
||||||
deleteChatSession,
|
|
||||||
} from "@/store/module/ckchat/ckchat";
|
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
import { updateConfig } from "@/pages/pc/ckbox/api";
|
import { updateConfig } from "@/pages/pc/ckbox/api";
|
||||||
|
import { getMessageList } from "./api";
|
||||||
import { dataProcessing } from "./api";
|
import { dataProcessing } from "./api";
|
||||||
import styles from "./MessageList.module.scss";
|
import styles from "./MessageList.module.scss";
|
||||||
import { formatWechatTime } from "@/utils/common";
|
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 {}
|
interface MessageListProps {}
|
||||||
|
|
||||||
const MessageList: React.FC<MessageListProps> = () => {
|
const MessageList: React.FC<MessageListProps> = () => {
|
||||||
const { setCurrentContact, currentContract } = useWeChatStore();
|
const { setCurrentContact, currentContract } = useWeChatStore();
|
||||||
const getChatSessions = useCkChatStore(state => state.chatSessions);
|
|
||||||
const kfSelected = useCkChatStore(state => state.kfSelected);
|
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 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<{
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
session: ContractData | weChatGroup | null;
|
session: ChatSession | null;
|
||||||
}>({
|
}>({
|
||||||
visible: false,
|
visible: false,
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -50,7 +53,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
|||||||
// 修改备注相关状态
|
// 修改备注相关状态
|
||||||
const [editRemarkModal, setEditRemarkModal] = useState<{
|
const [editRemarkModal, setEditRemarkModal] = useState<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
session: ContractData | weChatGroup | null;
|
session: ChatSession | null;
|
||||||
remark: string;
|
remark: string;
|
||||||
}>({
|
}>({
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -61,10 +64,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
|||||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 右键菜单事件处理
|
// 右键菜单事件处理
|
||||||
const handleContextMenu = (
|
const handleContextMenu = (e: React.MouseEvent, session: ChatSession) => {
|
||||||
e: React.MouseEvent,
|
|
||||||
session: ContractData | weChatGroup,
|
|
||||||
) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
@@ -87,53 +87,94 @@ const MessageList: React.FC<MessageListProps> = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 置顶/取消置顶
|
// 置顶/取消置顶
|
||||||
const handleTogglePin = (session: ContractData | weChatGroup) => {
|
const handleTogglePin = async (session: ChatSession) => {
|
||||||
const currentPinned = (session.config as any)?.top || false;
|
const currentPinned = session.config?.top || false;
|
||||||
const newPinned = !currentPinned; // 切换置顶状态
|
const newPinned = !currentPinned;
|
||||||
|
|
||||||
updateConfig({
|
try {
|
||||||
id: session.id,
|
// 1. 立即更新UI(乐观更新)
|
||||||
config: { top: newPinned, chat: true },
|
setSessions(prev =>
|
||||||
})
|
prev.map(s =>
|
||||||
.then(() => {
|
s.id === session.id
|
||||||
// API调用成功,更新本地状态
|
? {
|
||||||
toggleChatSessionPin(session.id!, newPinned);
|
...s,
|
||||||
message.success(`${newPinned ? "置顶" : "取消置顶"}成功`);
|
config: { ...s.config, top: newPinned },
|
||||||
})
|
sortKey: "", // 会重新计算
|
||||||
.catch(() => {
|
}
|
||||||
message.error(`${newPinned ? "置顶" : "取消置顶"}失败`);
|
: 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();
|
hideContextMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除会话
|
// 删除会话
|
||||||
const handleDelete = (session: ContractData | weChatGroup) => {
|
const handleDelete = (session: ChatSession) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "确认删除",
|
title: "确认删除",
|
||||||
content: `确定要删除与 ${session.conRemark || session.nickname} 的会话吗?`,
|
content: `确定要删除与 ${session.conRemark || session.nickname} 的会话吗?`,
|
||||||
onOk: () => {
|
onOk: async () => {
|
||||||
updateConfig({
|
try {
|
||||||
id: session.id,
|
// 1. 立即从UI移除
|
||||||
config: { chat: false },
|
setSessions(prev => prev.filter(s => s.id !== session.id));
|
||||||
})
|
|
||||||
.then(() => {
|
// 2. 后台调用API
|
||||||
message.success(`删除成功`);
|
await updateConfig({
|
||||||
//根据id删除会话里Item
|
id: session.id,
|
||||||
deleteChatSession(session.id);
|
config: { chat: false },
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
message.error(`删除失败`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3. 后台从数据库删除
|
||||||
|
await MessageManager.deleteSession(
|
||||||
|
currentUserId,
|
||||||
|
session.id,
|
||||||
|
session.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
message.success("删除成功");
|
||||||
|
} catch (error) {
|
||||||
|
// 4. 失败时恢复UI
|
||||||
|
setSessions(prev => [...prev, session]);
|
||||||
|
message.error("删除失败");
|
||||||
|
}
|
||||||
|
|
||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改备注
|
// 修改备注
|
||||||
const handleEditRemark = (session: ContractData | weChatGroup) => {
|
const handleEditRemark = (session: ChatSession) => {
|
||||||
setEditRemarkModal({
|
setEditRemarkModal({
|
||||||
visible: true,
|
visible: true,
|
||||||
session,
|
session: session as any,
|
||||||
remark: session.conRemark || "",
|
remark: session.conRemark || "",
|
||||||
});
|
});
|
||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
@@ -145,13 +186,25 @@ const MessageList: React.FC<MessageListProps> = () => {
|
|||||||
|
|
||||||
const session = editRemarkModal.session;
|
const session = editRemarkModal.session;
|
||||||
const isGroup = "chatroomId" in session;
|
const isGroup = "chatroomId" in session;
|
||||||
|
const sessionData = sessions.find(s => s.id === session.id);
|
||||||
|
if (!sessionData) return;
|
||||||
|
|
||||||
|
const oldRemark = session.conRemark;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1. 立即更新UI
|
||||||
|
setSessions(prev =>
|
||||||
|
prev.map(s =>
|
||||||
|
s.id === session.id ? { ...s, conRemark: editRemarkModal.remark } : s,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. 后台调用API
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
// 群聊备注修改
|
// 群聊备注修改
|
||||||
sendCommand("CmdModifyGroupRemark", {
|
sendCommand("CmdModifyGroupRemark", {
|
||||||
wechatAccountId: session.wechatAccountId,
|
wechatAccountId: session.wechatAccountId,
|
||||||
chatroomId: session.chatroomId,
|
chatroomId: (session as any).chatroomId,
|
||||||
newRemark: editRemarkModal.remark,
|
newRemark: editRemarkModal.remark,
|
||||||
});
|
});
|
||||||
await dataProcessing({
|
await dataProcessing({
|
||||||
@@ -175,18 +228,22 @@ const MessageList: React.FC<MessageListProps> = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改备注后会更新当前的session的conRemark,然后使用updateChatSession方法更新会话列表
|
// 3. 后台更新数据库
|
||||||
const updatedSession = {
|
await MessageManager.updateRemark(
|
||||||
...session,
|
currentUserId,
|
||||||
conRemark: editRemarkModal.remark,
|
session.id,
|
||||||
};
|
sessionData.type,
|
||||||
|
editRemarkModal.remark,
|
||||||
// 更新会话列表中的备注
|
);
|
||||||
const { updateChatSession } = useCkChatStore.getState();
|
|
||||||
updateChatSession(updatedSession);
|
|
||||||
|
|
||||||
message.success("备注更新成功");
|
message.success("备注更新成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 4. 失败时回滚UI
|
||||||
|
setSessions(prev =>
|
||||||
|
prev.map(s =>
|
||||||
|
s.id === session.id ? { ...s, conRemark: oldRemark } : s,
|
||||||
|
),
|
||||||
|
);
|
||||||
console.error("更新备注失败:", error);
|
console.error("更新备注失败:", error);
|
||||||
message.error("备注更新失败,请重试");
|
message.error("备注更新失败,请重试");
|
||||||
}
|
}
|
||||||
@@ -218,32 +275,254 @@ const MessageList: React.FC<MessageListProps> = () => {
|
|||||||
};
|
};
|
||||||
}, [contextMenu.visible]);
|
}, [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(() => {
|
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) {
|
if (kfSelected !== 0) {
|
||||||
filteredSessions = filteredSessions.filter(
|
filtered = filtered.filter(v => v.wechatAccountId === kfSelected);
|
||||||
v => v.wechatAccountId === kfSelected,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据搜索关键词进行模糊匹配
|
// 根据搜索关键词进行模糊匹配
|
||||||
if (searchKeyword.trim()) {
|
if (searchKeyword.trim()) {
|
||||||
const keyword = searchKeyword.toLowerCase();
|
const keyword = searchKeyword.toLowerCase();
|
||||||
filteredSessions = filteredSessions.filter(v => {
|
filtered = filtered.filter(v => {
|
||||||
const nickname = (v.nickname || "").toLowerCase();
|
const nickname = (v.nickname || "").toLowerCase();
|
||||||
const conRemark = (v.conRemark || "").toLowerCase();
|
const conRemark = (v.conRemark || "").toLowerCase();
|
||||||
return nickname.includes(keyword) || conRemark.includes(keyword);
|
return nickname.includes(keyword) || conRemark.includes(keyword);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setChatSessions(filteredSessions);
|
setFilteredSessions(filtered);
|
||||||
}, [getChatSessions, kfSelected, searchKeyword]);
|
}, [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 (
|
return (
|
||||||
<div className={styles.messageList}>
|
<div className={styles.messageList}>
|
||||||
|
{loading && <div className={styles.loading}>加载中...</div>}
|
||||||
|
{refreshing && !loading && (
|
||||||
|
<div className={styles.refreshIndicator}>
|
||||||
|
<Spin size="small" /> 正在同步...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<List
|
<List
|
||||||
dataSource={chatSessions as (ContractData | weChatGroup)[]}
|
dataSource={filteredSessions as any[]}
|
||||||
renderItem={session => (
|
renderItem={session => (
|
||||||
<List.Item
|
<List.Item
|
||||||
key={session.id}
|
key={session.id}
|
||||||
|
|||||||
@@ -24,16 +24,33 @@ export interface Message {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Store State
|
//Store State - 会话列表状态管理(不存储数据,只管理状态)
|
||||||
export interface MessageState {
|
export interface MessageState {
|
||||||
//消息列表
|
//加载状态
|
||||||
|
loading: boolean;
|
||||||
|
//后台同步状态
|
||||||
|
refreshing: boolean;
|
||||||
|
//刷新触发器(用于通知组件重新查询数据库)
|
||||||
|
refreshTrigger: number;
|
||||||
|
//最后刷新时间
|
||||||
|
lastRefreshTime: string | null;
|
||||||
|
|
||||||
|
//设置加载状态
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
//设置同步状态
|
||||||
|
setRefreshing: (refreshing: boolean) => void;
|
||||||
|
//触发刷新(通知组件重新查询)
|
||||||
|
triggerRefresh: () => void;
|
||||||
|
|
||||||
|
// ==================== 保留原有接口(向后兼容) ====================
|
||||||
|
//消息列表(废弃,保留兼容)
|
||||||
messageList: Message[];
|
messageList: Message[];
|
||||||
//当前选中的消息
|
//当前选中的消息(废弃,保留兼容)
|
||||||
currentMessage: Message | null;
|
currentMessage: Message | null;
|
||||||
//更新消息列表
|
//更新消息列表(废弃,保留兼容)
|
||||||
updateMessageList: (messageList: Message[]) => void;
|
updateMessageList: (messageList: Message[]) => void;
|
||||||
//更新消息状态
|
//更新消息状态(废弃,保留兼容)
|
||||||
updateMessageStatus: (messageId: number, status: string) => void;
|
updateMessageStatus: (messageId: number, status: string) => void;
|
||||||
//更新当前选中的消息
|
//更新当前选中的消息(废弃,保留兼容)
|
||||||
updateCurrentMessage: (message: Message) => void;
|
updateCurrentMessage: (message: Message) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,48 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { Message, MessageState } from "./message.data";
|
import { Message, MessageState } from "./message.data";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话列表状态管理Store
|
||||||
|
* 职责:只管理状态,不存储会话列表数据
|
||||||
|
* 数据存储在:组件state + IndexedDB
|
||||||
|
*/
|
||||||
export const useMessageStore = create<MessageState>()(
|
export const useMessageStore = create<MessageState>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
messageList: [], //消息列表
|
// ==================== 新增状态管理 ====================
|
||||||
currentMessage: null, //当前选中的消息
|
loading: false,
|
||||||
updateMessageList: (messageList: Message[]) => set({ messageList }), //更新消息列表
|
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) =>
|
updateCurrentMessage: (message: Message) =>
|
||||||
set({ currentMessage: message }), //更新当前选中的消息
|
set({ currentMessage: message }),
|
||||||
updateMessageStatus: (messageId: number, status: string) =>
|
updateMessageStatus: (messageId: number, status: string) =>
|
||||||
set({
|
set({
|
||||||
messageList: get().messageList.map(message =>
|
messageList: get().messageList.map(message =>
|
||||||
message.id === messageId ? { ...message, status } : message,
|
message.id === messageId ? { ...message, status } : message,
|
||||||
),
|
),
|
||||||
}), //更新消息状态
|
}),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "message-storage",
|
name: "message-storage",
|
||||||
partialize: state => ({
|
partialize: state => ({
|
||||||
|
// 只持久化必要的状态,不持久化数据
|
||||||
|
lastRefreshTime: state.lastRefreshTime,
|
||||||
|
// 保留原有持久化字段(向后兼容)
|
||||||
messageList: [],
|
messageList: [],
|
||||||
currentMessage: null,
|
currentMessage: null,
|
||||||
}),
|
}),
|
||||||
@@ -54,3 +78,24 @@ export const getCurrentMessage = () =>
|
|||||||
*/
|
*/
|
||||||
export const updateMessageStatus = (messageId: number, status: string) =>
|
export const updateMessageStatus = (messageId: number, status: string) =>
|
||||||
useMessageStore.getState().updateMessageStatus(messageId, status);
|
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) => {
|
CmdNewMessage: (message: Messages) => {
|
||||||
// 在这里添加具体的处理逻辑
|
// 处理消息本身
|
||||||
receivedMsg(message.friendMessage || message.chatroomMessage);
|
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: () => {
|
CmdFriendInfoChanged: () => {
|
||||||
// console.log("好友信息变更", message);
|
// console.log("好友信息变更", message);
|
||||||
|
|||||||
@@ -4,25 +4,15 @@
|
|||||||
* 架构设计:
|
* 架构设计:
|
||||||
* 1. 使用serverId作为数据库主键,直接对应接口返回的id字段
|
* 1. 使用serverId作为数据库主键,直接对应接口返回的id字段
|
||||||
* 2. 保留原始的id字段,用于存储接口数据的完整性
|
* 2. 保留原始的id字段,用于存储接口数据的完整性
|
||||||
* 3. 简化数据处理逻辑,避免ID映射的复杂性
|
* 3. 添加userId字段实现多用户数据隔离
|
||||||
|
* 4. 统一会话表和联系人表,兼容好友和群聊
|
||||||
*
|
*
|
||||||
* 优势:
|
* 优势:
|
||||||
* - 直接使用服务器ID作为主键,避免ID冲突
|
* - 直接使用服务器ID作为主键,避免ID冲突
|
||||||
* - 保持数据的一致性和可追溯性
|
* - 通过userId实现多用户数据隔离
|
||||||
* - 简化查询逻辑,提高性能
|
* - 统一的会话和联系人表结构,兼容好友和群聊
|
||||||
* - 支持重复数据检测和去重
|
* - 支持复合索引,提高查询性能
|
||||||
*
|
* - 支持用户登录记录和自动清理
|
||||||
* 使用方法:
|
|
||||||
* - 存储接口数据:使用 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); // 根据主键查询(内部使用)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Dexie, { Table } from "dexie";
|
import Dexie, { Table } from "dexie";
|
||||||
@@ -33,33 +23,136 @@ import {
|
|||||||
MessageListData,
|
MessageListData,
|
||||||
} from "@/pages/pc/ckbox/data";
|
} 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"> {
|
export interface KfUserWithServerId extends Omit<KfUserListData, "id"> {
|
||||||
serverId: number | string; // 服务器ID作为主键
|
serverId: number | string; // 服务器ID作为主键
|
||||||
|
userId: number; // 用户ID(数据隔离)
|
||||||
id?: number; // 接口数据的原始ID字段
|
id?: number; // 接口数据的原始ID字段
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新联系人列表数据接口
|
|
||||||
export interface NewContactListData {
|
export interface NewContactListData {
|
||||||
serverId: number | string; // 服务器ID作为主键
|
serverId: number | string; // 服务器ID作为主键
|
||||||
|
userId: number; // 用户ID(数据隔离)
|
||||||
id?: number; // 接口数据的原始ID字段
|
id?: number; // 接口数据的原始ID字段
|
||||||
groupName: string;
|
groupName: string;
|
||||||
contacts: ContractData[];
|
contacts: Contact[];
|
||||||
weChatGroup: weChatGroup[];
|
weChatGroup: Contact[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据库类
|
// 数据库类
|
||||||
class CunkebaoDatabase extends Dexie {
|
class CunkebaoDatabase extends Dexie {
|
||||||
|
// ==================== 保留原有表(向后兼容) ====================
|
||||||
kfUsers!: Table<KfUserWithServerId>;
|
kfUsers!: Table<KfUserWithServerId>;
|
||||||
weChatGroup!: Table<weChatGroup>;
|
weChatGroup!: Table<weChatGroup>;
|
||||||
contracts!: Table<ContractData>;
|
contracts!: Table<ContractData>;
|
||||||
newContractList!: Table<NewContactListData>;
|
newContactList!: Table<NewContactListData>;
|
||||||
messageList!: Table<MessageListData>;
|
messageList!: Table<MessageListData>;
|
||||||
|
|
||||||
|
// ==================== 新增统一表 ====================
|
||||||
|
chatSessions!: Table<ChatSession>; // 统一会话表
|
||||||
|
contactsUnified!: Table<Contact>; // 统一联系人表
|
||||||
|
contactLabelMap!: Table<ContactLabelMap>; // 联系人标签映射表
|
||||||
|
userLoginRecords!: Table<UserLoginRecord>; // 用户登录记录表
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("CunkebaoDatabase");
|
super("CunkebaoDatabase");
|
||||||
|
|
||||||
// 版本1:使用serverId作为主键的架构
|
// 版本1:使用serverId作为主键的架构(保留原有表)
|
||||||
this.version(1).stores({
|
this.version(1).stores({
|
||||||
kfUsers:
|
kfUsers:
|
||||||
"serverId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
|
"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",
|
"serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, notice, selfDisplyName",
|
||||||
contracts:
|
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",
|
"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:
|
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",
|
"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 kfUserService = new DatabaseService(db.kfUsers);
|
||||||
export const weChatGroupService = new DatabaseService(db.weChatGroup);
|
export const weChatGroupService = new DatabaseService(db.weChatGroup);
|
||||||
export const contractService = new DatabaseService(db.contracts);
|
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 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;
|
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