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

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

View File

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