chat-pc/src/utils/db.js
Phoenix f010287bfa refactor(db): 优化数据库结构和查询性能
重构数据库表结构和索引,使用复合索引提高查询效率
简化消息和会话操作方法,使用批量操作提升性能
移除冗余代码和调试日志,清理代码风格
2025-07-01 09:57:51 +08:00

383 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Dexie from 'dexie';
export const db = new Dexie('chatHistory');
// 定义数据库表结构和索引
// 版本3优化了索引提高了查询和排序性能
db.version(4).stores({
/**
* 聊天记录表
* - msg_id: 消息唯一ID (主键)
* - sequence: 消息序列号,用于排序
* - [talk_type+receiver_id]: 复合索引,用于快速查询会话消息
* - created_at: 消息创建时间,用于排序
* - [talk_type+receiver_id+sequence]: 复合索引,用于高效分页查询
*/
messages: 'msg_id, sequence, [talk_type+receiver_id], created_at, [talk_type+receiver_id+sequence]',
/**
* 会话表
* - ++id: 自增主键
* - &index_name: 唯一索引 (talk_type + '_' + receiver_id)
* - updated_at: 索引,用于排序
* - is_top: 索引,用于置顶排序
*/
conversations: 'id, &index_name, talk_type, receiver_id, updated_at, unread_num, is_top',
});
db.on('ready', () => {
console.log(`数据库已就绪,版本: ${db.verno}`);
});
/** 消息类型常量 */
export const MessageType = {
TEXT: 1, // 文本消息
IMAGE: 2, // 图片消息
FILE: 3, // 文件消息
AUDIO: 4, // 语音消息
VIDEO: 5, // 视频消息
LOCATION: 6, // 位置消息
CARD: 7, // 名片消息
};
/** 会话类型常量 */
export const TalkType = {
PRIVATE: 1, // 私聊
GROUP: 2, // 群聊
};
/**
* 生成一个简单的UUID
* @returns {string} UUID
*/
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// #region 消息操作
/**
* 添加或更新一条聊天记录
* @param {object} message - 消息对象
* @returns {Promise<string>} 消息ID
*/
export async function addMessage(message) {
try {
if (!message.msg_id) {
message.msg_id = generateUUID();
}
if (!message.created_at) {
message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19);
}
// 使用 put 方法,如果主键已存在则更新,否则添加
await db.messages.put(message);
return message.msg_id;
} catch (error) {
console.error('添加或更新消息失败:', error);
throw error;
}
}
/**
* 批量添加或更新聊天记录
* @param {Array<object>} messages - 消息对象数组
* @returns {Promise<void>}
*/
export async function batchAddOrUpdateMessages(messages) {
try {
if (!Array.isArray(messages) || messages.length === 0) {
return;
}
const messagesToStore = messages.map(message => {
if (!message.msg_id) {
message.msg_id = generateUUID();
}
if (!message.created_at) {
message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19);
}
return message;
});
// 使用 bulkPut 高效地批量添加或更新
await db.messages.bulkPut(messagesToStore);
// 更新最后一条消息到会话
const latestMessage = messagesToStore[messagesToStore.length - 1];
if (latestMessage) {
await updateConversationLastMessage(latestMessage);
}
} catch (error) {
console.error('批量添加或更新消息失败:', error);
throw error;
}
}
/**
* 获取指定会话的聊天记录
* @param {number} talkType - 会话类型 (1:私聊, 2:群聊)
* @param {number} userId - 当前用户ID
* @param {number} receiverId - 接收者ID (私聊为对方用户ID群聊为群ID)
* @param {number} [limit=30] - 限制返回的记录数量
* @param {number|null} [maxSequence=null] - 最大sequence值用于分页加载更早的消息
* @returns {Promise<Array<object>>} 消息列表 (按sequence升序排列)
*/
export async function getMessages(talkType, userId, receiverId, limit = 30, maxSequence = null) {
try {
let collection;
// 使用索引进行高效查询
if (maxSequence !== null) {
// 加载更多:查询 sequence 小于 maxSequence 的消息
collection = db.messages
.where('[talk_type+receiver_id+sequence]')
.between([talkType, receiverId, 0], [talkType, receiverId, maxSequence], true, false);
} else {
// 首次加载:查询指定会话的所有消息
collection = db.messages.where({ '[talk_type+receiver_id]': [talkType, receiverId] });
}
// 1. reverse() - 利用索引倒序排列,获取最新的消息
// 2. limit() - 限制数量,实现分页
// 3. toArray() - 执行查询
const messages = await collection.reverse().limit(limit).toArray();
// 再次 reverse() - 将获取到的分页消息按时间正序排列,以便于在界面上显示
return messages.reverse();
} catch (error) {
console.error('获取消息失败:', error);
throw error;
}
}
/**
* 标记指定会话的所有消息为已读
* @param {number} talkType - 会话类型
* @param {number} userId - 当前用户ID
* @param {number} receiverId - 接收者ID
* @returns {Promise<number>} 更新的消息数量
*/
export async function markMessagesAsRead(talkType, userId, receiverId) {
try {
let query;
if (talkType === TalkType.PRIVATE) {
// 私聊:只标记对方发给我的未读消息
query = db.messages
.where('[talk_type+receiver_id]')
.equals([talkType, userId])
.and(item => item.user_id === receiverId && item.is_read === 0);
} else {
// 群聊:标记群里所有非自己的未读消息
query = db.messages
.where('[talk_type+receiver_id]')
.equals([talkType, receiverId])
.and(item => item.user_id !== userId && item.is_read === 0);
}
return await query.modify({ is_read: 1 });
} catch (error) {
console.error('批量标记消息已读失败:', error);
throw error;
}
}
/**
* 撤回消息
* @param {string} msgId - 消息ID
* @returns {Promise<number>} 更新记录数 (1或0)
*/
export async function revokeMessage(msgId) {
try {
return await db.messages.update(msgId, { is_revoke: 1 });
} catch (error) {
console.error('撤回消息失败:', error);
throw error;
}
}
/**
* 删除消息
* @param {string} msgId - 消息ID
* @returns {Promise<void>}
*/
export async function deleteMessage(msgId) {
try {
await db.messages.delete(msgId);
} catch (error) {
console.error('删除消息失败:', error);
throw error;
}
}
// #endregion 消息操作
// #region 会话操作
/**
* 添加或更新会话
* @param {object} conversation - 会话对象
* @returns {Promise<number>} 会话ID
*/
export async function addOrUpdateConversation(conversation) {
try {
// put 方法会根据唯一索引 index_name 自动判断是添加还是更新
return await db.conversations.put(conversation);
} catch (error) {
console.error('添加或更新会话失败:', error);
throw error;
}
}
/**
* 获取所有会话列表
* @param {boolean} [includeEmpty=false] - 是否包含没有最后一条消息的会话
* @returns {Promise<Array<object>>} 会话列表 (按置顶和更新时间排序)
*/
export async function getConversations(includeEmpty = false) {
try {
const filterFn = item => !includeEmpty ? (item.msg_text && item.msg_text.length > 0) : true;
// 分别查询置顶和非置顶会话,以利用索引并优化性能
const topConversationsPromise = db.conversations
.where('is_top')
.equals(1)
.sortBy('updated_at')
.then(arr => arr.reverse().filter(filterFn));
const otherConversationsPromise = db.conversations
.where('is_top')
.notEqual(1)
.sortBy('updated_at')
.then(arr => arr.reverse().filter(filterFn));
const [topConversations, otherConversations] = await Promise.all([
topConversationsPromise,
otherConversationsPromise,
]);
return [...topConversations, ...otherConversations];
} catch (error) {
console.error('获取会话列表失败:', error);
throw error;
}
}
/**
* 获取指定会话
* @param {number} talkType - 会话类型
* @param {number} receiverId - 接收者ID
* @returns {Promise<object|undefined>} 会话对象
*/
export async function getConversation(talkType, receiverId) {
try {
const indexName = `${talkType}_${receiverId}`;
return await db.conversations.get({ index_name: indexName });
} catch (error) {
console.error('获取会话失败:', error);
throw error;
}
}
/**
* 更新会话的未读消息数
* @param {number} talkType - 会话类型
* @param {number} receiverId - 接收者ID
* @param {number|null} unreadNum - 未读消息数。如果为null则自增1
* @returns {Promise<number>} 更新的记录数
*/
export async function updateConversationUnreadNum(talkType, receiverId, unreadNum = null) {
try {
const indexName = `${talkType}_${receiverId}`;
const conversation = await db.conversations.get({ index_name: indexName });
if (conversation) {
const newUnreadNum = unreadNum === null ? (conversation.unread_num || 0) + 1 : unreadNum;
return await db.conversations.update(conversation.id, { unread_num: newUnreadNum });
}
return 0;
} catch (error) {
console.error('更新会话未读数失败:', error);
throw error;
}
}
/**
* 清空会话的未读消息数
* @param {number} talkType - 会话类型
* @param {number} receiverId - 接收者ID
* @returns {Promise<number>} 更新的记录数
*/
export function clearConversationUnreadNum(talkType, receiverId) {
return updateConversationUnreadNum(talkType, receiverId, 0);
}
/**
* 删除会话及其相关的消息
* @param {number} conversationId - 会话ID
* @param {boolean} [deleteMessages=false] - 是否同时删除相关的消息记录
* @returns {Promise<void>}
*/
export async function deleteConversation(conversationId, deleteMessages = false) {
try {
await db.transaction('rw', db.conversations, db.messages, async () => {
const conversation = await db.conversations.get(conversationId);
if (!conversation) return;
// 删除会话
await db.conversations.delete(conversationId);
// 如果需要,删除关联的消息
if (deleteMessages) {
const { talk_type, receiver_id } = conversation;
await db.messages.where({ '[talk_type+receiver_id]': [talk_type, receiver_id] }).delete();
}
});
} catch (error) {
console.error('删除会话失败:', error);
throw error;
}
}
/**
* 更新会话的最后一条消息摘要
* @param {object} message - 消息对象
* @returns {Promise<number>} 更新的记录数
*/
export async function updateConversationLastMessage(message) {
try {
const { talk_type, user_id, receiver_id, msg_type } = message;
const targetReceiverId = talk_type === TalkType.PRIVATE ? (user_id === receiver_id ? user_id : receiver_id) : receiver_id;
const indexName = `${talk_type}_${targetReceiverId}`;
const conversation = await db.conversations.get({ index_name: indexName });
if (!conversation) return 0;
let msgText = '';
switch (msg_type) {
case MessageType.TEXT: msgText = message.content || ''; break;
case MessageType.IMAGE: msgText = '[图片]'; break;
case MessageType.FILE: msgText = '[文件]'; break;
case MessageType.AUDIO: msgText = '[语音]'; break;
case MessageType.VIDEO: msgText = '[视频]'; break;
case MessageType.LOCATION: msgText = '[位置]'; break;
case MessageType.CARD: msgText = '[名片]'; break;
default: msgText = '[未知消息]';
}
return await db.conversations.update(conversation.id, {
msg_text: msgText,
content: message.content || '',
updated_at: message.created_at,
});
} catch (error) {
console.error('更新会话最后消息失败:', error);
throw error;
}
}
// #endregion 会话操作