383 lines
12 KiB
JavaScript
383 lines
12 KiB
JavaScript
|
||
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 会话操作
|