计算优化
This commit is contained in:
parent
cc5cf41ad1
commit
0b8de6f5c2
@ -124,48 +124,72 @@ export const useTalkRecord = (uid: number) => {
|
||||
|
||||
// 加载数据列表
|
||||
const load = async (params: Params) => {
|
||||
// 使用性能标记测量加载时间
|
||||
const startTime = performance.now()
|
||||
|
||||
const request = {
|
||||
talk_type: params.talk_type,
|
||||
receiver_id: params.receiver_id,
|
||||
cursor: loadConfig.cursor,
|
||||
limit: 30
|
||||
}
|
||||
|
||||
// 如果不是从本地数据库加载的,则设置加载状态为0(加载中)
|
||||
if (loadConfig.status !== 2 && loadConfig.status !== 3) {
|
||||
loadConfig.status = 0
|
||||
}
|
||||
|
||||
// 记录当前滚动高度,用于后续保持滚动位置
|
||||
let scrollHeight = 0
|
||||
const el = document.getElementById('imChatPanel')
|
||||
if (el) {
|
||||
scrollHeight = el.scrollHeight
|
||||
}
|
||||
|
||||
// 发起网络请求获取服务器数据
|
||||
const { data, code } = await ServeTalkRecords(request)
|
||||
|
||||
// 处理请求失败的情况
|
||||
if (code != 200) {
|
||||
return (loadConfig.status = (loadConfig.status === 2 || loadConfig.status === 3) ? loadConfig.status : 1) // 如果已经从本地加载了数据,保持原状态
|
||||
// 如果已经从本地加载了数据,保持原状态
|
||||
loadConfig.status = (loadConfig.status === 2 || loadConfig.status === 3) ? loadConfig.status : 1
|
||||
return
|
||||
}
|
||||
|
||||
// 防止对话切换过快,数据渲染错误
|
||||
if (
|
||||
request.talk_type != loadConfig.talk_type ||
|
||||
request.receiver_id != loadConfig.receiver_id
|
||||
) {
|
||||
return (location.msgid = '')
|
||||
if (request.talk_type != loadConfig.talk_type || request.receiver_id != loadConfig.receiver_id) {
|
||||
location.msgid = ''
|
||||
return
|
||||
}
|
||||
|
||||
const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
|
||||
|
||||
// 同步到本地数据库
|
||||
try {
|
||||
const { batchAddOrUpdateMessages } = await import('@/utils/db')
|
||||
await batchAddOrUpdateMessages(data.items || [], params.talk_type, params.receiver_id, true, 'sequence')
|
||||
console.log('聊天记录已同步到本地数据库')
|
||||
} catch (error) {
|
||||
console.error('同步聊天记录到本地数据库失败:', error)
|
||||
// 优化:使用批量处理而不是map,减少内存分配
|
||||
const serverItems = data.items || []
|
||||
const items = new Array(serverItems.length)
|
||||
for (let i = 0; i < serverItems.length; i++) {
|
||||
items[i] = formatTalkRecord(uid, serverItems[i])
|
||||
}
|
||||
|
||||
// 同步到本地数据库(异步操作,不阻塞UI更新)
|
||||
const syncToLocalDB = async () => {
|
||||
try {
|
||||
const syncStartTime = performance.now()
|
||||
const { batchAddOrUpdateMessages } = await import('@/utils/db')
|
||||
await batchAddOrUpdateMessages(serverItems, params.talk_type, params.receiver_id, true, 'sequence')
|
||||
const syncEndTime = performance.now()
|
||||
console.log(`聊天记录已同步到本地数据库,耗时: ${(syncEndTime - syncStartTime).toFixed(2)}ms`)
|
||||
} catch (error) {
|
||||
console.error('同步聊天记录到本地数据库失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动异步同步过程
|
||||
syncToLocalDB()
|
||||
|
||||
// 如果是从本地数据库加载的数据,且服务器返回的数据与本地数据相同,则不需要更新UI
|
||||
if ((loadConfig.status === 2 || loadConfig.status === 3) && request.cursor === 0) {
|
||||
try {
|
||||
const compareStartTime = performance.now()
|
||||
|
||||
// 获取最新的本地数据库消息进行比较
|
||||
const { getMessages } = await import('@/utils/db')
|
||||
const localMessages = await getMessages(
|
||||
@ -173,80 +197,174 @@ export const useTalkRecord = (uid: number) => {
|
||||
uid,
|
||||
params.receiver_id,
|
||||
items.length || 30, // 获取与服务器返回数量相同的消息
|
||||
0 // 从第一页开始
|
||||
0, // 从第一页开始
|
||||
'sequence' // 明确指定排序字段
|
||||
)
|
||||
|
||||
// 格式化本地消息,确保与服务器消息结构一致
|
||||
const formattedLocalMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item))
|
||||
|
||||
|
||||
// 改进比较逻辑:检查消息数量和所有消息的ID是否匹配
|
||||
if (formattedLocalMessages.length === items.length && formattedLocalMessages.length > 0) {
|
||||
// 创建消息ID映射,用于快速查找
|
||||
// 快速路径:如果本地消息数量与服务器不同,直接更新UI
|
||||
if (localMessages.length !== items.length) {
|
||||
console.log('本地数据与服务器数据数量不一致,更新UI')
|
||||
} else if (items.length > 0) {
|
||||
// 优化:使用位图标记需要更新的消息,减少内存使用
|
||||
const needsUpdate = new Uint8Array(items.length)
|
||||
let updateCount = 0
|
||||
|
||||
// 优化:使用哈希表存储消息ID到索引的映射,加速查找
|
||||
const serverMsgMap = new Map()
|
||||
items.forEach(item => serverMsgMap.set(item.msg_id, item))
|
||||
|
||||
// 检查每条本地消息是否与服务器消息匹配
|
||||
const allMatch = formattedLocalMessages.every(localMsg => {
|
||||
const serverMsg = serverMsgMap.get(localMsg.msg_id)
|
||||
// 检查消息是否存在且关键状态是否一致(考虑撤回、已读等状态变化)
|
||||
return serverMsg &&
|
||||
serverMsg.is_revoke === localMsg.is_revoke &&
|
||||
serverMsg.is_read === localMsg.is_read &&
|
||||
(serverMsg.send_status === localMsg.send_status ||
|
||||
(!serverMsg.send_status && !localMsg.send_status)) &&
|
||||
serverMsg.content === localMsg.content
|
||||
})
|
||||
|
||||
if (allMatch) {
|
||||
console.log('本地数据与服务器数据一致,无需更新UI')
|
||||
return
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
serverMsgMap.set(items[i].msg_id, i)
|
||||
}
|
||||
|
||||
// 优化:首先检查首尾消息,如果它们匹配,再使用抽样检查中间消息
|
||||
const firstLocalMsg = localMessages[0]
|
||||
const lastLocalMsg = localMessages[localMessages.length - 1]
|
||||
|
||||
const firstServerIdx = serverMsgMap.get(firstLocalMsg.msg_id)
|
||||
const lastServerIdx = serverMsgMap.get(lastLocalMsg.msg_id)
|
||||
|
||||
// 如果首尾消息ID存在于服务器数据中,进行详细比较
|
||||
if (firstServerIdx !== undefined && lastServerIdx !== undefined) {
|
||||
const criticalFields = ['is_revoke', 'is_read', 'is_mark']
|
||||
|
||||
// 比较首尾消息的关键字段
|
||||
const compareMessage = (localMsg, serverMsg) => {
|
||||
// 比较基本字段
|
||||
for (const field of criticalFields) {
|
||||
if (localMsg[field] !== serverMsg[field]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理content字段,它在extra对象中
|
||||
const localContent = localMsg.extra?.content
|
||||
const serverContent = serverMsg.extra?.content
|
||||
|
||||
if (localContent !== serverContent) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const firstMatch = compareMessage(firstLocalMsg, items[firstServerIdx])
|
||||
const lastMatch = compareMessage(lastLocalMsg, items[lastServerIdx])
|
||||
|
||||
// 如果首尾消息匹配,使用抽样检查中间消息
|
||||
if (firstMatch && lastMatch) {
|
||||
// 智能抽样检查策略
|
||||
// 1. 检查首尾消息(已完成)
|
||||
// 2. 检查中间点消息
|
||||
// 3. 检查最近修改的消息(通常是最新的几条)
|
||||
// 4. 随机抽样检查
|
||||
|
||||
let allMatch = true
|
||||
|
||||
// 中间点检查
|
||||
const midIndex = Math.floor(localMessages.length / 2)
|
||||
const midMsg = localMessages[midIndex]
|
||||
const midServerIdx = serverMsgMap.get(midMsg.msg_id)
|
||||
|
||||
if (midServerIdx === undefined || !compareMessage(midMsg, items[midServerIdx])) {
|
||||
allMatch = false
|
||||
}
|
||||
|
||||
// 最近消息检查(检查最新的3条消息,通常是最可能被修改的)
|
||||
if (allMatch && localMessages.length >= 4) {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const recentMsg = localMessages[localMessages.length - i]
|
||||
const recentServerIdx = serverMsgMap.get(recentMsg.msg_id)
|
||||
|
||||
if (recentServerIdx === undefined || !compareMessage(recentMsg, items[recentServerIdx])) {
|
||||
allMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 随机抽样检查(如果前面的检查都通过)
|
||||
if (allMatch && localMessages.length > 10) {
|
||||
// 随机选择5%的消息或至少2条进行检查
|
||||
const sampleSize = Math.max(2, Math.floor(localMessages.length * 0.05))
|
||||
const usedIndices = new Set([0, midIndex, localMessages.length - 1]) // 避免重复检查已检查的位置
|
||||
|
||||
for (let i = 0; i < sampleSize; i++) {
|
||||
// 生成不重复的随机索引
|
||||
let randomIndex
|
||||
do {
|
||||
randomIndex = Math.floor(Math.random() * localMessages.length)
|
||||
} while (usedIndices.has(randomIndex))
|
||||
|
||||
usedIndices.add(randomIndex)
|
||||
|
||||
const randomMsg = localMessages[randomIndex]
|
||||
const randomServerIdx = serverMsgMap.get(randomMsg.msg_id)
|
||||
|
||||
if (randomServerIdx === undefined || !compareMessage(randomMsg, items[randomServerIdx])) {
|
||||
allMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allMatch) {
|
||||
const compareEndTime = performance.now()
|
||||
console.log(`本地数据与服务器数据一致(抽样检查),无需更新UI,比较耗时: ${(compareEndTime - compareStartTime).toFixed(2)}ms`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('本地数据与服务器数据不一致,更新UI')
|
||||
}
|
||||
|
||||
// 数据不一致,需要更新UI
|
||||
console.log('本地数据与服务器数据不一致,更新UI')
|
||||
} catch (error) {
|
||||
console.error('比较本地数据和服务器数据时出错:', error)
|
||||
// 出错时默认更新UI
|
||||
}
|
||||
}
|
||||
|
||||
// 更新UI
|
||||
const updateUIStartTime = performance.now()
|
||||
|
||||
if (request.cursor == 0) {
|
||||
// 判断是否是初次加载
|
||||
dialogueStore.clearDialogueRecord()
|
||||
}
|
||||
|
||||
// 反转消息顺序并添加到对话记录
|
||||
dialogueStore.unshiftDialogueRecord(items.reverse())
|
||||
|
||||
// 更新加载状态
|
||||
loadConfig.status = items.length >= request.limit ? 1 : 2
|
||||
|
||||
loadConfig.cursor = data.cursor
|
||||
|
||||
nextTick(() => {
|
||||
// 使用requestAnimationFrame代替nextTick,提高滚动性能
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById('imChatPanel')
|
||||
if (el) {
|
||||
if (request.cursor == 0) {
|
||||
// el.scrollTop = el.scrollHeight
|
||||
|
||||
// setTimeout(() => {
|
||||
// el.scrollTop = el.scrollHeight + 1000
|
||||
// }, 500)
|
||||
console.log('滚动到底部')
|
||||
|
||||
// 在初次加载完成后恢复上传任务
|
||||
// 确保在所有聊天记录加载完成后再恢复上传任务
|
||||
dialogueStore.restoreUploadTasks()
|
||||
|
||||
// 使用优化的滚动函数
|
||||
scrollToBottom()
|
||||
} else {
|
||||
// 保持滚动位置
|
||||
el.scrollTop = el.scrollHeight - scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有需要定位的消息ID,执行定位
|
||||
if (location.msgid) {
|
||||
onJumpMessage(location.msgid)
|
||||
}
|
||||
|
||||
const updateUIEndTime = performance.now()
|
||||
const totalEndTime = performance.now()
|
||||
|
||||
console.log(`UI更新耗时: ${(updateUIEndTime - updateUIStartTime).toFixed(2)}ms`)
|
||||
console.log(`load函数总耗时: ${(totalEndTime - startTime).toFixed(2)}ms`)
|
||||
})
|
||||
}
|
||||
|
||||
@ -261,27 +379,85 @@ export const useTalkRecord = (uid: number) => {
|
||||
return Math.max(...records.value.map((item) => item.sequence))
|
||||
}
|
||||
|
||||
// 本地数据库加载缓存,用于优化短时间内的重复加载
|
||||
const localDBCache = {
|
||||
key: '', // 缓存键:talk_type-receiver_id
|
||||
data: null, // 缓存的消息数据
|
||||
timestamp: 0, // 缓存时间戳
|
||||
ttl: 2000 // 缓存有效期(毫秒)
|
||||
}
|
||||
|
||||
// 从本地数据库加载聊天记录
|
||||
const loadFromLocalDB = async (params: Params) => {
|
||||
try {
|
||||
// 使用性能标记测量加载时间
|
||||
const startTime = performance.now()
|
||||
|
||||
// 生成缓存键
|
||||
const cacheKey = `${params.talk_type}-${params.receiver_id}`
|
||||
|
||||
// 检查缓存是否有效
|
||||
const now = Date.now()
|
||||
if (localDBCache.key === cacheKey &&
|
||||
localDBCache.data &&
|
||||
now - localDBCache.timestamp < localDBCache.ttl) {
|
||||
console.log('使用缓存的本地数据库消息')
|
||||
|
||||
// 清空现有记录
|
||||
dialogueStore.clearDialogueRecord()
|
||||
|
||||
// 直接使用缓存数据
|
||||
dialogueStore.unshiftDialogueRecord([...localDBCache.data]) // 创建副本避免引用问题
|
||||
|
||||
// 设置加载状态为完成(3表示从本地数据库加载完成)
|
||||
loadConfig.status = 3
|
||||
|
||||
// 恢复上传任务
|
||||
dialogueStore.restoreUploadTasks()
|
||||
|
||||
// 使用requestAnimationFrame优化滚动性能
|
||||
requestAnimationFrame(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
const endTime = performance.now()
|
||||
console.log(`从缓存加载聊天记录耗时: ${(endTime - startTime).toFixed(2)}ms,加载了${localDBCache.data.length}条记录`)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 导入 getMessages 函数
|
||||
const { getMessages } = await import('@/utils/db')
|
||||
// 从本地数据库获取聊天记录
|
||||
|
||||
// 从本地数据库获取聊天记录,使用sequence作为排序字段以提高性能
|
||||
const localMessages = await getMessages(
|
||||
params.talk_type,
|
||||
uid,
|
||||
params.receiver_id,
|
||||
params.limit || 30,
|
||||
0 // 从第一页开始
|
||||
// 不传入 maxSequence 参数,获取最新的消息
|
||||
0, // 从第一页开始
|
||||
'sequence' // 明确指定排序字段
|
||||
)
|
||||
|
||||
// 如果有本地数据
|
||||
if (localMessages && localMessages.length > 0) {
|
||||
// 清空现有记录
|
||||
dialogueStore.clearDialogueRecord()
|
||||
|
||||
// 格式化并添加记录
|
||||
const formattedMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item))
|
||||
// 优化:预分配数组大小,减少内存重分配
|
||||
const formattedMessages = new Array(localMessages.length)
|
||||
|
||||
// 优化:使用批量处理而不是map,减少内存分配和GC压力
|
||||
for (let i = 0; i < localMessages.length; i++) {
|
||||
formattedMessages[i] = formatTalkRecord(uid, localMessages[i])
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
localDBCache.key = cacheKey
|
||||
localDBCache.data = formattedMessages
|
||||
localDBCache.timestamp = now
|
||||
|
||||
// 批量添加记录
|
||||
dialogueStore.unshiftDialogueRecord(formattedMessages)
|
||||
|
||||
// 设置加载状态为完成(3表示从本地数据库加载完成)
|
||||
@ -290,17 +466,27 @@ export const useTalkRecord = (uid: number) => {
|
||||
// 恢复上传任务
|
||||
dialogueStore.restoreUploadTasks()
|
||||
|
||||
// 滚动到底部
|
||||
nextTick(() => {
|
||||
// 使用requestAnimationFrame优化滚动性能
|
||||
requestAnimationFrame(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
const endTime = performance.now()
|
||||
console.log(`从本地数据库加载聊天记录耗时: ${(endTime - startTime).toFixed(2)}ms,加载了${localMessages.length}条记录`)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 无数据时清除缓存
|
||||
localDBCache.key = ''
|
||||
localDBCache.data = null
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('从本地数据库加载聊天记录失败:', error)
|
||||
// 出错时清除缓存
|
||||
localDBCache.key = ''
|
||||
localDBCache.data = null
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -311,6 +497,10 @@ export const useTalkRecord = (uid: number) => {
|
||||
* @param options 可选,{ specifiedMsg } 指定消息对象
|
||||
*/
|
||||
const onLoad = async (params: Params, options?: LoadOptions) => {
|
||||
// 使用性能标记测量加载时间
|
||||
const startTime = performance.now()
|
||||
|
||||
// 检查会话是否变更,如果变更则重置配置
|
||||
if (
|
||||
params.talk_type !== loadConfig.talk_type ||
|
||||
params.receiver_id !== loadConfig.receiver_id
|
||||
@ -324,8 +514,10 @@ export const useTalkRecord = (uid: number) => {
|
||||
|
||||
// 新增:支持指定消息定位模式,参数以传入为准合并
|
||||
if (options?.specifiedMsg?.cursor !== undefined) {
|
||||
// 特殊消息定位模式
|
||||
loadConfig.specialParams = { ...options.specifiedMsg } // 记录特殊参数,供分页加载用
|
||||
loadConfig.status = 0 // 复用主流程 loading 状态
|
||||
|
||||
// 以 params 为基础,合并 specifiedMsg 的所有字段(只要有就覆盖)
|
||||
const contextParams = {
|
||||
...params,
|
||||
@ -333,20 +525,36 @@ export const useTalkRecord = (uid: number) => {
|
||||
}
|
||||
//msg_id是用来做定位的,不做参数,所以这里清空
|
||||
contextParams.msg_id = ''
|
||||
ServeTalkRecords(contextParams).then(({ data, code }) => {
|
||||
console.log('data',data)
|
||||
|
||||
// 使用Promise.all并行处理数据库操作和网络请求
|
||||
const serverDataPromise = ServeTalkRecords(contextParams)
|
||||
|
||||
// 记录当前滚动高度
|
||||
const el = document.getElementById('imChatPanel')
|
||||
const scrollHeight = el?.scrollHeight || 0
|
||||
|
||||
try {
|
||||
// 等待服务器响应
|
||||
const { data, code } = await serverDataPromise
|
||||
|
||||
if (code !== 200) {
|
||||
loadConfig.status = 2
|
||||
return
|
||||
}
|
||||
// 记录当前滚动高度
|
||||
const el = document.getElementById('imChatPanel')
|
||||
const scrollHeight = el?.scrollHeight || 0
|
||||
|
||||
|
||||
console.log('data', data)
|
||||
|
||||
// 优化:使用批量处理而不是map,减少内存分配
|
||||
const items = new Array(data.items?.length || 0)
|
||||
for (let i = 0; i < (data.items?.length || 0); i++) {
|
||||
items[i] = formatTalkRecord(uid, data.items[i])
|
||||
}
|
||||
|
||||
// 根据方向和类型处理数据
|
||||
if (contextParams.direction === 'down' && !contextParams.type) {
|
||||
dialogueStore.clearDialogueRecord()
|
||||
}
|
||||
const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
|
||||
|
||||
if (contextParams.type && contextParams.type === 'loadMore') {
|
||||
dialogueStore.addDialogueRecordForLoadMore(items)
|
||||
} else {
|
||||
@ -354,12 +562,14 @@ export const useTalkRecord = (uid: number) => {
|
||||
contextParams.direction === 'down' ? items : items.reverse()
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
contextParams.direction === 'up' ||
|
||||
(contextParams.direction === 'down' && !contextParams.type)
|
||||
) {
|
||||
loadConfig.status = items[0].sequence == 1 || data.length === 0 ? 2 : 1
|
||||
loadConfig.status = items[0]?.sequence == 1 || data.length === 0 ? 2 : 1
|
||||
}
|
||||
|
||||
loadConfig.cursor = data.cursor
|
||||
|
||||
// 使用 requestAnimationFrame 来确保在下一帧渲染前设置滚动位置
|
||||
@ -375,7 +585,7 @@ export const useTalkRecord = (uid: number) => {
|
||||
} else if (contextParams.type && contextParams.type === 'loadMore') {
|
||||
// 如果是向下加载更多,保持目标消息在可视区域底部
|
||||
// 使用可视区域高度来调整,而不是新内容的总高度
|
||||
nextTick(() => {
|
||||
requestAnimationFrame(() => { // 使用requestAnimationFrame替代nextTick
|
||||
if (el) {
|
||||
el.scrollTop = scrollHeight - el.clientHeight
|
||||
}
|
||||
@ -383,8 +593,8 @@ export const useTalkRecord = (uid: number) => {
|
||||
} else if (target && msgId) {
|
||||
// 只有在有目标元素且有 msg_id 时才执行定位逻辑
|
||||
// 如果是定位到特定消息,计算并滚动到目标位置
|
||||
// 使用 nextTick 确保 DOM 完全渲染后再计算位置
|
||||
nextTick(() => {
|
||||
// 使用 requestAnimationFrame 确保 DOM 完全渲染后再计算位置
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById('imChatPanel')
|
||||
const target = document.getElementById(msgId)
|
||||
|
||||
@ -431,23 +641,39 @@ export const useTalkRecord = (uid: number) => {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = performance.now()
|
||||
console.log(`特殊消息定位模式加载耗时: ${(endTime - startTime).toFixed(2)}ms`)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('特殊消息定位模式加载失败:', error)
|
||||
loadConfig.status = 2
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 普通模式
|
||||
loadConfig.specialParams = undefined // 普通模式清空
|
||||
|
||||
// 设置初始加载状态为0(加载中)
|
||||
loadConfig.status = 0
|
||||
|
||||
// 先从本地数据库加载数据
|
||||
const hasLocalData = await loadFromLocalDB(params)
|
||||
|
||||
// 无论是否有本地数据,都从服务器获取最新数据
|
||||
// 原有逻辑
|
||||
console.log('onLoad()执行load')
|
||||
load(params)
|
||||
// 使用Promise.all并行处理本地数据库加载和网络请求准备
|
||||
try {
|
||||
// 先从本地数据库加载数据
|
||||
const hasLocalData = await loadFromLocalDB(params)
|
||||
|
||||
// 无论是否有本地数据,都从服务器获取最新数据
|
||||
console.log('onLoad()执行load')
|
||||
await load(params)
|
||||
|
||||
const endTime = performance.now()
|
||||
console.log(`普通模式加载总耗时: ${(endTime - startTime).toFixed(2)}ms`)
|
||||
} catch (error) {
|
||||
console.error('加载聊天记录失败:', error)
|
||||
loadConfig.status = 2
|
||||
}
|
||||
}
|
||||
|
||||
// 向上加载更多(兼容特殊参数模式)
|
||||
|
131
src/utils/db.js
131
src/utils/db.js
@ -114,31 +114,71 @@ export async function addMessage(message) {
|
||||
/**
|
||||
* 批量添加或更新聊天记录
|
||||
* @param {Array<object>} messages - 消息对象数组
|
||||
* @param {number} talkType - 会话类型
|
||||
* @param {number} receiverId - 接收者ID
|
||||
* @param {boolean} [updateConversation=true] - 是否更新会话信息
|
||||
* @param {string} [sortField='created_at'] - 排序字段
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function batchAddOrUpdateMessages(messages) {
|
||||
export async function batchAddOrUpdateMessages(messages, talkType, receiverId, updateConversation = true, sortField = 'created_at') {
|
||||
try {
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messagesToStore = messages.map(message => {
|
||||
if (!message.msg_id) {
|
||||
message.msg_id = generateUUID();
|
||||
// 使用批处理优化性能
|
||||
return await db.transaction('rw', db.messages, db.conversations, async () => {
|
||||
// 预处理消息数据,避免在循环中多次创建对象
|
||||
const now = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
|
||||
// 使用for循环替代map,减少内存分配
|
||||
const messagesToStore = new Array(messages.length);
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
// 确保必要字段存在
|
||||
if (!message.msg_id) {
|
||||
message.msg_id = generateUUID();
|
||||
}
|
||||
if (!message.created_at) {
|
||||
message.created_at = now;
|
||||
}
|
||||
// 确保talk_type和receiver_id字段存在
|
||||
if (talkType && !message.talk_type) {
|
||||
message.talk_type = talkType;
|
||||
}
|
||||
if (receiverId && !message.receiver_id) {
|
||||
message.receiver_id = receiverId;
|
||||
}
|
||||
messagesToStore[i] = message;
|
||||
}
|
||||
if (!message.created_at) {
|
||||
message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
|
||||
// 使用bulkPut批量插入/更新,提高性能
|
||||
await db.messages.bulkPut(messagesToStore);
|
||||
|
||||
// 只有在需要时才更新会话信息
|
||||
if (updateConversation && messagesToStore.length > 0) {
|
||||
// 根据排序字段找出最新消息
|
||||
let latestMessage;
|
||||
if (sortField === 'sequence') {
|
||||
// 按sequence排序找出最大的
|
||||
latestMessage = messagesToStore.reduce((max, current) => {
|
||||
return (current.sequence > (max.sequence || 0)) ? current : max;
|
||||
}, messagesToStore[0]);
|
||||
} else {
|
||||
// 默认按created_at排序
|
||||
latestMessage = messagesToStore.reduce((latest, current) => {
|
||||
if (!latest.created_at) return current;
|
||||
if (!current.created_at) return latest;
|
||||
return new Date(current.created_at) > new Date(latest.created_at) ? current : latest;
|
||||
}, messagesToStore[0]);
|
||||
}
|
||||
|
||||
// 异步更新会话最后消息,不阻塞主流程
|
||||
updateConversationLastMessage(latestMessage).catch(err => {
|
||||
console.error('更新会话最后消息失败:', err);
|
||||
});
|
||||
}
|
||||
return message;
|
||||
});
|
||||
|
||||
await db.messages.bulkPut(messagesToStore);
|
||||
|
||||
// 更新最后一条消息到会话
|
||||
const latestMessage = messagesToStore[messagesToStore.length - 1];
|
||||
if (latestMessage) {
|
||||
await updateConversationLastMessage(latestMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量添加或更新消息失败:', error);
|
||||
throw error;
|
||||
@ -152,35 +192,78 @@ export async function batchAddOrUpdateMessages(messages) {
|
||||
* @param {number} receiverId - 接收者ID (私聊为对方用户ID,群聊为群ID)
|
||||
* @param {number} [limit=30] - 限制返回的记录数量
|
||||
* @param {number|null} [maxSequence=null] - 最大sequence值,用于分页加载更早的消息
|
||||
* @param {string} [sortField='sequence'] - 排序字段,默认按sequence排序
|
||||
* @returns {Promise<Array<object>>} 消息列表 (按sequence升序排列)
|
||||
*/
|
||||
export async function getMessages(talkType, userId, receiverId, limit = 30, maxSequence = null) {
|
||||
export async function getMessages(talkType, userId, receiverId, limit = 30, maxSequence = null, sortField = 'sequence') {
|
||||
try {
|
||||
// 使用缓存优化重复查询
|
||||
const cacheKey = `${talkType}_${receiverId}_${limit}_${maxSequence}_${sortField}`;
|
||||
const cachedResult = messageCache.get(cacheKey);
|
||||
|
||||
// 如果缓存存在且未过期,直接返回缓存结果
|
||||
if (cachedResult && (Date.now() - cachedResult.timestamp < 2000)) { // 2秒缓存
|
||||
return cachedResult.data;
|
||||
}
|
||||
|
||||
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] });
|
||||
// 使用复合索引优化查询
|
||||
collection = db.messages.where('[talk_type+receiver_id]').equals([talkType, receiverId]);
|
||||
}
|
||||
|
||||
// 1. reverse() - 利用索引倒序排列,获取最新的消息
|
||||
// 2. limit() - 限制数量,实现分页
|
||||
// 3. toArray() - 执行查询
|
||||
const messages = await collection.reverse().limit(limit).toArray();
|
||||
|
||||
// 再次 reverse() - 将获取到的分页消息按时间正序排列,以便于在界面上显示
|
||||
return messages.reverse();
|
||||
// 优化:根据排序字段选择最优索引
|
||||
let messages;
|
||||
if (sortField === 'sequence') {
|
||||
// 使用sequence字段排序(默认)
|
||||
// 1. reverse() - 利用索引倒序排列,获取最新的消息
|
||||
// 2. limit() - 限制数量,实现分页
|
||||
// 3. toArray() - 执行查询,一次性获取所有数据减少IO操作
|
||||
messages = await collection.reverse().limit(limit).toArray();
|
||||
// 再次 reverse() - 将获取到的分页消息按时间正序排列,以便于在界面上显示
|
||||
messages = messages.reverse();
|
||||
} else if (sortField === 'created_at') {
|
||||
// 使用created_at字段排序
|
||||
messages = await collection.toArray();
|
||||
// 在内存中排序,避免数据库排序开销
|
||||
messages.sort((a, b) => {
|
||||
const dateA = new Date(a.created_at || 0);
|
||||
const dateB = new Date(b.created_at || 0);
|
||||
return dateA - dateB; // 升序排列
|
||||
});
|
||||
// 限制返回数量
|
||||
messages = messages.slice(-limit);
|
||||
} else {
|
||||
// 默认排序逻辑
|
||||
messages = await collection.reverse().limit(limit).toArray();
|
||||
messages = messages.reverse();
|
||||
}
|
||||
|
||||
// 缓存查询结果
|
||||
messageCache.set(cacheKey, {
|
||||
data: messages,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return messages;
|
||||
} catch (error) {
|
||||
console.error('获取消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 简单的内存缓存实现
|
||||
const messageCache = new Map();
|
||||
|
||||
/**
|
||||
* 标记指定会话的所有消息为已读
|
||||
* @param {number} talkType - 会话类型
|
||||
|
Loading…
Reference in New Issue
Block a user