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} 消息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} messages - 消息对象数组 * @returns {Promise} */ 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>} 消息列表 (按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} 更新的消息数量 */ 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} 更新记录数 (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} */ 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} 会话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>} 会话列表 (按置顶和更新时间排序) */ 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} 会话对象 */ 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} 更新的记录数 */ 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} 更新的记录数 */ export function clearConversationUnreadNum(talkType, receiverId) { return updateConversationUnreadNum(talkType, receiverId, 0); } /** * 删除会话及其相关的消息 * @param {number} conversationId - 会话ID * @param {boolean} [deleteMessages=false] - 是否同时删除相关的消息记录 * @returns {Promise} */ 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} 更新的记录数 */ 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 会话操作