import Dexie from 'dexie'; export const db = new Dexie('chatHistory'); // 定义数据库结构 db.version(2).stores({ // 聊天记录表 messages: 'msg_id, sequence, talk_type, msg_type, user_id, receiver_id, is_read, created_at', // 会话表(包含私聊和群聊) conversations: 'id, talk_type, receiver_id, index_name, updated_at, unread_num' }); db.on('ready', function() { 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, // 群聊 }; /** * 添加一条聊天记录 * @param {Object} message - 消息对象 * @returns {Promise} - 返回消息ID */ export async function addMessage(message) { try { // 确保消息有唯一ID if (!message.msg_id) { message.msg_id = generateUUID(); } // 添加时间戳(如果没有) if (!message.created_at) { message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19); } // 添加到数据库 const id = await db.messages.add(message); return message.msg_id; } catch (error) { console.error('添加消息失败:', error); throw error; } } /** * 批量添加或更新聊天记录 * @param {Array} messages - 消息对象数组 * @param {number} talkType - 会话类型(1:私聊, 2:群聊) * @param {number} receiverId - 接收者ID(私聊为对方用户ID,群聊为群ID) * @param {boolean} replaceExisting - 是否替换已存在的消息 * @param {string} sortKey - 排序和比较的键,可选值:'sequence'或'created_at' * @returns {Promise<{added: number, updated: number, total: number}>} - 返回添加和更新的消息数量 */ export async function batchAddOrUpdateMessages(messages, talkType, receiverId, replaceExisting = true, sortKey = 'sequence') { try { if (!Array.isArray(messages) || messages.length === 0) { return { added: 0, updated: 0, total: 0 }; } // 获取现有消息 let existingMessages = []; if (replaceExisting) { // 不传入 maxSequence 参数,获取所有消息 existingMessages = await getMessages(talkType, null, receiverId, 1000, 0); } // 创建msg_id到消息的映射,用于快速查找 const existingMap = new Map(); existingMessages.forEach(msg => { existingMap.set(msg.msg_id, msg); }); // 创建sequence/created_at到消息的映射,用于比较和替换 const existingKeyMap = new Map(); existingMessages.forEach(msg => { if (msg[sortKey]) { existingKeyMap.set(msg[sortKey], msg); } }); let added = 0; let updated = 0; // 批量处理事务 await db.transaction('rw', db.messages, async () => { for (const message of messages) { // 确保消息有必要的字段 if (!message.msg_id) { message.msg_id = generateUUID(); } if (!message.created_at) { message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19); } // 检查消息是否已存在 const existing = existingMap.get(message.msg_id); if (existing) { // 更新现有消息,使用 msg_id 而不是 id await db.messages.update(existing.msg_id, message); updated++; } else if (replaceExisting && message[sortKey] && existingKeyMap.has(message[sortKey])) { // 根据sortKey替换现有消息 const existingByKey = existingKeyMap.get(message[sortKey]); await db.messages.update(existingByKey.msg_id, message); updated++; } else { console.log('message',message) // 添加新消息 await db.messages.add(message); added++; } } }); // 如果有新消息添加,更新会话的最后一条消息 if (added > 0 || updated > 0) { // 假设消息数组中的最后一条消息是最新的(保持原始顺序) // 如果需要找出最新消息,可以使用数组中的最后一条消息 const latestMessage = messages[messages.length - 1]; // 更新会话的最后一条消息 await updateConversationLastMessage(latestMessage); } return { added, updated, total: added + updated }; } 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 - 限制返回的记录数量 * @param {number} offset - 偏移量,用于分页 * @returns {Promise} - 返回消息列表 */ /** * 获取消息列表 * @param {number} talkType - 会话类型(1:私聊, 2:群聊) * @param {number} userId - 用户ID(私聊时用于区分消息方向) * @param {number} receiverId - 接收者ID(私聊时为对方ID,群聊时为群ID) * @param {number} limit - 每页条数 * @param {number} offset - 偏移量 * @param {number} maxSequence - 最大sequence值,用于分页加载更早的消息 * @returns {Promise} - 消息列表 */ export async function getMessages(talkType, userId, receiverId, limit = 30, offset = 0, maxSequence = null) { try { let query; if (talkType === TalkType.PRIVATE) { // 私聊消息(双向获取) query = db.messages .where('talk_type').equals(TalkType.PRIVATE) .and(item => { return (item.user_id === userId && item.receiver_id === receiverId) || (item.user_id === receiverId && item.receiver_id === userId); }); } else { // 群聊消息 query = db.messages .where('talk_type').equals(TalkType.GROUP) .and(item => item.receiver_id === receiverId); } // 如果提供了maxSequence,则只获取sequence小于该值的消息 if (maxSequence !== null) { query = query.and(item => item.sequence < maxSequence); } // 按sequence升序排序 let messages = await query .sortBy('sequence'); // 获取尾部的limit条记录(最新的消息) if (messages.length > limit) { messages = messages.slice(Math.max(0, messages.length - limit)); } return messages; } catch (error) { console.error('获取消息失败:', error); throw error; } } /** * 标记消息为已读 * @param {string} msgId - 消息ID * @returns {Promise} - 操作是否成功 */ export async function markMessageAsRead(msgId) { try { await db.messages.update(msgId, { is_read: 1 }); return true; } catch (error) { console.error('标记消息已读失败:', error); return false; } } /** * 批量标记消息为已读 * @param {number} talkType - 会话类型 * @param {number} userId - 当前用户ID * @param {number} receiverId - 接收者ID * @returns {Promise} - 返回更新的消息数量 */ export async function markMessagesAsRead(talkType, userId, receiverId) { try { let count; if (talkType === TalkType.PRIVATE) { // 私聊消息(只标记对方发给我的消息) count = await db.messages .where('talk_type').equals(TalkType.PRIVATE) .and(item => item.user_id === receiverId && item.receiver_id === userId) .and(item => item.is_read === 0) .modify({ is_read: 1 }); } else { // 群聊消息(标记该群的所有未读消息) count = await db.messages .where('talk_type').equals(TalkType.GROUP) .and(item => item.receiver_id === receiverId) .and(item => item.is_read === 0 && item.user_id !== userId) .modify({ is_read: 1 }); } return count; } catch (error) { console.error('批量标记消息已读失败:', error); throw error; } } /** * 撤回消息 * @param {string} msgId - 消息ID * @returns {Promise} - 操作是否成功 */ export async function revokeMessage(msgId) { try { await db.messages.update(msgId, { is_revoke: 1 }); return true; } catch (error) { console.error('撤回消息失败:', error); return false; } } /** * 删除消息 * @param {string} msgId - 消息ID * @returns {Promise} - 操作是否成功 */ export async function deleteMessage(msgId) { try { await db.messages.delete(msgId); return true; } catch (error) { console.error('删除消息失败:', error); return false; } } /** * 获取未读消息数量 * @param {number} userId - 当前用户ID * @returns {Promise} - 返回未读消息统计信息 */ export async function getUnreadCount(userId) { try { // 私聊未读消息数量 const privateUnread = await db.messages .where('talk_type').equals(TalkType.PRIVATE) .and(item => item.receiver_id === userId && item.is_read === 0) .count(); // 群聊未读消息数量 const groupUnread = await db.messages .where('talk_type').equals(TalkType.GROUP) .and(item => item.user_id !== userId && item.is_read === 0) .count(); // 按会话分组的未读消息数量 const privateUnreadBySession = await db.messages .where('talk_type').equals(TalkType.PRIVATE) .and(item => item.receiver_id === userId && item.is_read === 0) .toArray() .then(messages => { const result = {}; messages.forEach(msg => { if (!result[msg.user_id]) { result[msg.user_id] = 0; } result[msg.user_id]++; }); return result; }); // 按群组分组的未读消息数量 const groupUnreadBySession = await db.messages .where('talk_type').equals(TalkType.GROUP) .and(item => item.user_id !== userId && item.is_read === 0) .toArray() .then(messages => { const result = {}; messages.forEach(msg => { if (!result[msg.receiver_id]) { result[msg.receiver_id] = 0; } result[msg.receiver_id]++; }); return result; }); return { total: privateUnread + groupUnread, private: privateUnread, group: groupUnread, privateBySession: privateUnreadBySession, groupBySession: groupUnreadBySession }; } catch (error) { console.error('获取未读消息数量失败:', error); throw error; } } /** * 生成UUID * @returns {string} - 返回UUID */ function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /** * 添加或更新会话 * @param {Object} conversation - 会话对象 * @returns {Promise} - 返回会话ID */ export async function addOrUpdateConversation(conversation) { try { // 创建一个纯数据副本,移除所有不可序列化的内容 const serializableConversation = JSON.parse(JSON.stringify(conversation)); // 检查会话是否已存在 const existingConversation = await db.conversations .where('index_name') .equals(serializableConversation.index_name) .first(); if (existingConversation) { // 更新现有会话 await db.conversations.update(existingConversation.id, serializableConversation); return existingConversation.id; } else { // 添加新会话 const id = await db.conversations.add(serializableConversation); return id; } } catch (error) { console.error('添加或更新会话失败:', error); throw error; } } /** * 获取所有会话列表 * @param {boolean} includeEmpty - 是否包含没有消息的会话 * @returns {Promise} - 返回会话列表,按更新时间倒序排列 */ export async function getConversations(includeEmpty = false) { try { let query = db.conversations; if (!includeEmpty) { // 只获取有消息的会话 query = query.filter(item => item.msg_text && item.msg_text.length > 0); } // 按更新时间倒序排列 const conversations = await query .toArray() .then(items => items.sort((a, b) => { // 置顶的会话优先 if (a.is_top !== b.is_top) return b.is_top - a.is_top; // 然后按更新时间排序 return new Date(b.updated_at) - new Date(a.updated_at); })); return conversations; } catch (error) { console.error('获取会话列表失败:', error); throw error; } } /** * 获取指定会话 * @param {number} talkType - 会话类型(1:私聊, 2:群聊) * @param {number} receiverId - 接收者ID(私聊为对方用户ID,群聊为群ID) * @returns {Promise} - 返回会话对象,如果不存在则返回null */ export async function getConversation(talkType, receiverId) { try { const indexName = `${talkType}_${receiverId}`; const conversation = await db.conversations .where('index_name') .equals(indexName) .first(); return conversation || null; } catch (error) { console.error('获取会话失败:', error); throw error; } } /** * 更新会话的未读消息数 * @param {number} talkType - 会话类型 * @param {number} receiverId - 接收者ID * @param {number} unreadNum - 未读消息数量,如果为null则增加1 * @returns {Promise} - 操作是否成功 */ export async function updateConversationUnreadNum(talkType, receiverId, unreadNum = null) { try { const indexName = `${talkType}_${receiverId}`; const conversation = await db.conversations .where('index_name') .equals(indexName) .first(); if (conversation) { if (unreadNum === null) { // 增加未读消息数 unreadNum = (conversation.unread_num || 0) + 1; } await db.conversations.update(conversation.id, { unread_num: unreadNum }); return true; } return false; } catch (error) { console.error('更新会话未读消息数失败:', error); return false; } } /** * 清空会话的未读消息数 * @param {number} talkType - 会话类型 * @param {number} receiverId - 接收者ID * @returns {Promise} - 操作是否成功 */ export async function clearConversationUnreadNum(talkType, receiverId) { return updateConversationUnreadNum(talkType, receiverId, 0); } /** * 删除会话 * @param {number} conversationId - 会话ID * @param {boolean} deleteMessages - 是否同时删除相关的消息记录 * @returns {Promise} - 操作是否成功 */ export async function deleteConversation(conversationId, deleteMessages = false) { try { const conversation = await db.conversations.get(conversationId); if (!conversation) return false; // 删除会话 await db.conversations.delete(conversationId); // 如果需要,同时删除相关的消息记录 if (deleteMessages) { const { talk_type, receiver_id } = conversation; if (talk_type === TalkType.PRIVATE) { // 删除私聊消息 await db.messages .where('talk_type').equals(TalkType.PRIVATE) .and(item => { return (item.user_id === receiver_id || item.receiver_id === receiver_id); }) .delete(); } else { // 删除群聊消息 await db.messages .where('talk_type').equals(TalkType.GROUP) .and(item => item.receiver_id === receiver_id) .delete(); } } return true; } catch (error) { console.error('删除会话失败:', error); return false; } } /** * 更新会话的最后一条消息 * @param {Object} message - 消息对象 * @returns {Promise} - 操作是否成功 */ export async function updateConversationLastMessage(message) { try { const { talk_type, user_id, receiver_id, msg_type } = message; let targetReceiverId; if (talk_type === TalkType.PRIVATE) { // 私聊:对方ID作为会话的receiver_id targetReceiverId = user_id === receiver_id ? user_id : receiver_id; } else { // 群聊:群ID作为会话的receiver_id targetReceiverId = receiver_id; } const indexName = `${talk_type}_${targetReceiverId}`; const conversation = await db.conversations .where('index_name') .equals(indexName) .first(); if (!conversation) return false; // 根据消息类型生成显示文本 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 = '[未知类型消息]'; } // 更新会话的最后消息和时间 await db.conversations.update(conversation.id, { msg_text: msgText, content: message.content || '', updated_at: message.created_at }); return true; } catch (error) { console.error('更新会话最后消息失败:', error); return false; } } /** * 示例:如何使用聊天记录数据库 * * // 1. 添加一条消息 * const newMessage = { * msg_id: '5a266eb831594b2b8be7c9fdf34986df', * sequence: 184, * talk_type: 1, // 私聊 * msg_type: 4, // 语音消息 * user_id: 1496, * receiver_id: 1774, * nickname: '王一峰', * avatar: 'https://cdn-test.szjixun.cn/fonchain-main/dev/image/4692/oa/36b09c00-c2c3-453d-9aaf-ac41d47d564b.png', * is_revoke: 0, * is_mark: 0, * is_read: 0, * created_at: '2025-06-30 09:11:14', * extra: { * duration: 0, * name: '', * size: 15984, * url: 'https://cdn-test.szjixun.cn/fonchain-main/test/file/default/fonchain-chat/2b595dbc-6a2b-498e-802a-880e93d1e45a.mp3' * }, * erp_user_id: 4692 * }; * * addMessage(newMessage).then(msgId => { * console.log('消息添加成功,ID:', msgId); * }); * * // 2. 获取私聊消息列表 * getMessages(TalkType.PRIVATE, 1496, 1774, 20, 0).then(messages => { * console.log('获取到20条私聊消息:', messages); * }); * * // 获取特定sequence之前的消息 * getMessages(TalkType.PRIVATE, 1496, 1774, 20, 0, 1000).then(messages => { * console.log('获取sequence小于1000的20条私聊消息:', messages); * }); * * // 3. 标记消息为已读 * markMessageAsRead('5a266eb831594b2b8be7c9fdf34986df').then(success => { * console.log('标记消息已读:', success ? '成功' : '失败'); * }); * * // 4. 批量标记会话消息为已读 * markMessagesAsRead(TalkType.PRIVATE, 1496, 1774).then(count => { * console.log(`成功标记 ${count} 条消息为已读`); * }); * * // 5. 撤回消息 * revokeMessage('5a266eb831594b2b8be7c9fdf34986df').then(success => { * console.log('撤回消息:', success ? '成功' : '失败'); * }); * * // 6. 获取未读消息数量 * getUnreadCount(1496).then(result => { * console.log('未读消息统计:', result); * }); * * // 7. 添加或更新会话 * const newConversation = { * talk_type: 1, // 私聊 * receiver_id: 1774, * name: "周俊耀", * remark: "", * avatar: "https://e-cdn.fontree.cn/fonchain-main/prod/image/18248/avatar/a0b2bee7-947f-465a-986e-10a1b2b87032.png", * is_disturb: 0, * is_top: 1, * is_online: 0, * is_robot: 0, * is_dismiss: 0, * is_quit: 0, * unread_num: 0, * content: "......", * draft_text: "", * msg_text: "[语音消息]", * index_name: "1_1774", * updated_at: "2025-06-30 10:27:27", * atsign_num: 0, * erp_user_id: 3346, * group_member_num: 0, * group_type: 0 * }; * * addOrUpdateConversation(newConversation).then(id => { * console.log('会话添加或更新成功,ID:', id); * }); * * // 8. 获取所有会话列表 * getConversations().then(conversations => { * console.log('获取所有会话列表:', conversations); * }); * * // 9. 获取指定会话 * getConversation(TalkType.PRIVATE, 1774).then(conversation => { * console.log('获取指定会话:', conversation); * }); * * // 10. 更新会话未读消息数 * updateConversationUnreadNum(TalkType.PRIVATE, 1774, 5).then(success => { * console.log('更新会话未读消息数:', success ? '成功' : '失败'); * }); * * // 11. 清空会话未读消息数 * clearConversationUnreadNum(TalkType.PRIVATE, 1774).then(success => { * console.log('清空会话未读消息数:', success ? '成功' : '失败'); * }); * * // 12. 更新会话最后一条消息 * updateConversationLastMessage(newMessage).then(success => { * console.log('更新会话最后一条消息:', success ? '成功' : '失败'); * }); * * // 13. 删除会话 * deleteConversation(5811, false).then(success => { * console.log('删除会话:', success ? '成功' : '失败'); * }); * * // 14. 批量添加或更新聊天记录 * const messagesFromServer = [ * { * msg_id: 'server-msg-id-1', * talk_type: TalkType.PRIVATE, * msg_type: MessageType.TEXT, * user_id: 1000, * receiver_id: 2000, * content: '你好,这是第一条消息', * is_read: 1, * sequence: 1, * created_at: '2023-05-01 10:00:00' * }, * { * msg_id: 'server-msg-id-2', * talk_type: TalkType.PRIVATE, * msg_type: MessageType.TEXT, * user_id: 2000, * receiver_id: 1000, * content: '你好,这是回复消息', * is_read: 0, * sequence: 2, * created_at: '2023-05-01 10:01:00' * } * ]; * * // 使用sequence作为排序和替换的键 * batchAddOrUpdateMessages( * messagesFromServer, * TalkType.PRIVATE, * 2000, // 接收者ID(私聊为对方用户ID,群聊为群ID) * true, // 替换已存在的消息 * 'sequence' // 使用sequence作为排序和替换的键 * ).then(result => { * console.log(`添加了${result.added}条新消息,更新了${result.updated}条消息`); * }); */