diff --git a/src/pages/admin/statistics/SystemStatisticsPage.vue b/src/pages/admin/statistics/SystemStatisticsPage.vue index 8b86886..5083086 100644 --- a/src/pages/admin/statistics/SystemStatisticsPage.vue +++ b/src/pages/admin/statistics/SystemStatisticsPage.vue @@ -122,7 +122,7 @@

实时API调用动态

- 队列缓冲 · 最多显示100条 + 实时更新 · 最多显示100条
@@ -582,38 +582,25 @@ const apiPopularityData = ref([]) // 今日认证企业数据 const todayCertifiedEnterprises = ref([]) -// 弹幕相关数据(队列FIFO) +// 弹幕相关数据 - 重构为实时显示模式 const danmakuItems = ref([]) // 正在显示的弹幕列表 -const danmakuQueue = ref([]) // 待显示的弹幕队列(从数据库查询到的新记录先存入此队列) const danmakuContainer = ref(null) const danmakuPollingTimer = ref(null) // 数据库查询定时器 -const danmakuDisplayTimer = ref(null) // 从队列取出弹幕显示的定时器 -const lastEnqueuedId = ref(null) // 最后入队记录的ID(用于查询新记录,比时间戳更可靠) -const lastEnqueueTime = ref(null) // 最后入队记录的时间点(备用) -const isFirstLoad = ref(true) // 标记是否是首次加载 -// 缓存每个用户-产品组合的调用次数,避免重复查询 -const callCountCache = ref(new Map()) // key: `${userId}_${productId}`, value: { count: number, lastUpdated: timestamp } +const lastQueryTime = ref(null) // 最后查询的时间点 const processedRecordIds = ref(new Set()) // 已处理的记录ID集合,用于去重 -const MAX_DANMAKU_ITEMS = 100 // 最多显示的弹幕数量 -const MAX_QUEUE_SIZE = 200 // 队列最大容量 -const DANMAKU_POLLING_INTERVAL = 5000 // 数据库查询间隔(毫秒)- 5秒,降低查询频率 -const DANMAKU_DISPLAY_INTERVAL = 800 // 从队列取出弹幕显示的间隔(毫秒)- 0.8秒,控制显示速度 -// 暂停的弹幕ID集合 -const pausedDanmakuIds = ref(new Set()) -// 跟踪每行最后弹幕的显示时间,用于避免同一行弹幕迸发(保留用于延迟计算) -const lastRowDisplayTime = ref([0, 0]) // [第0行最后显示时间, 第1行最后显示时间] +const pausedDanmakuIds = ref(new Set()) // 暂停的弹幕ID集合 const danmakuDisplayOrder = ref(0) // 弹幕显示顺序,用于z-index -// 弹幕尺寸配置 -const DANMAKU_ITEM_HEIGHT = 28 // 每条弹幕的高度(更紧凑) +const isProcessing = ref(false) // 是否正在处理新记录,避免并发 + +// 配置常量 +const MAX_DANMAKU_ITEMS = 100 // 最多显示的弹幕数量 +const DANMAKU_POLLING_INTERVAL = 1500 // 数据库查询间隔(毫秒)- 1.5秒,提高实时性 +const DANMAKU_ITEM_HEIGHT = 28 // 每条弹幕的高度 const DANMAKU_CONTAINER_HEIGHT = 80 // 容器高度 const DANMAKU_MIN_SPACING = 32 // 最小垂直间距(避免重叠) const DANMAKU_ANIMATION_DURATION = 15 // 动画持续时间(秒) -const DANMAKU_ESTIMATED_WIDTH = 250 // 估算的弹幕宽度(像素),用于计算右边缘离开时间(减小以缩短延迟) -const DANMAKU_DELAY_REDUCTION_FACTOR = 0.6 // 延迟缩减系数,让弹幕显示更紧凑 -// 错误重试相关 -const danmakuRetryCount = ref(0) // 连续失败次数 -const MAX_RETRY_COUNT = 3 // 最大重试次数 -const RETRY_DELAY = 2000 // 重试延迟(毫秒) +const DANMAKU_ESTIMATED_WIDTH = 250 // 估算的弹幕宽度(像素) +const MAX_PROCESSED_IDS = 1000 // 最大已处理ID数量,避免内存泄漏 // 快捷选择配置 @@ -1445,96 +1432,27 @@ const formatDateTime = (date) => { return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } -// 批量查询调用次数(优化性能) -const batchQueryCallCounts = async (records) => { - // 按用户-产品分组,并为每个记录计算调用次数 - const callCountMap = new Map() - - // 为每条记录单独计算调用次数(简化处理,避免复杂的批量查询) - // 使用缓存优化性能 - const callCountPromises = records.map(async (record) => { - const userId = record.user_id || record.userId || 'unknown' - const productId = record.product_id || record.productId || '' - const createdAt = record.created_at || record.createdAt || new Date().toISOString() - const recordId = record.id || `${userId}_${productId}_${createdAt}` - - if (userId === 'unknown' || !productId) { - return { recordId, callCount: 1 } - } - - // 检查缓存(使用更精确的缓存键:用户+产品+时间) - const cacheKey = `${userId}_${productId}` - const cached = callCountCache.value.get(cacheKey) - - // 如果缓存存在且时间在1分钟内,使用缓存值并递增 - if (cached && Date.now() - cached.lastUpdated < 60000) { - const callCount = cached.count + 1 - // 更新缓存 - callCountCache.value.set(cacheKey, { - count: callCount, - lastUpdated: Date.now() - }) - return { recordId, callCount } - } - - try { - // 查询该用户对该产品在创建时间之前的调用记录数量 - const countParams = { - page: 1, - page_size: 100, // 获取足够多的记录以便统计 - user_id: userId, - end_time: formatDateTime(new Date(createdAt)), - sort_by: 'created_at', - sort_order: 'desc' - } - - const countResponse = await apiCallApi.getAdminApiCalls(countParams) - if (countResponse.success && countResponse.data?.items) { - // 在客户端过滤出该产品的记录 - const productCalls = countResponse.data.items.filter(item => { - const itemProductId = item.product_id || item.productId || '' - const itemCreatedAt = item.created_at || item.createdAt || '' - if (itemProductId === productId) { - if (itemCreatedAt && createdAt) { - return new Date(itemCreatedAt) <= new Date(createdAt) - } - return true - } - return false - }) - - const callCount = productCalls.length + 1 // +1 包括当前记录 - - // 更新缓存 - callCountCache.value.set(cacheKey, { - count: callCount, - lastUpdated: Date.now() - }) - - return { recordId, callCount } - } - } catch (err) { - console.error('查询调用次数失败:', err, { userId, productId }) - } - - return { recordId, callCount: 1 } - }) - - const results = await Promise.all(callCountPromises) - results.forEach(({ recordId, callCount }) => { - callCountMap.set(recordId, callCount) - }) - - return callCountMap +// 获取调用次数(简化版本,实时显示不需要精确计数) +const getCallCount = (record) => { + // 实时弹幕中,调用次数可以简化为1,或者从记录中获取 + // 如果需要精确计数,可以后续优化 + return record.call_count || 1 } -// 加载弹幕数据(只负责查询数据库并将新记录存入队列) +// 加载弹幕数据 - 实时模式:查询到新记录立即显示 const loadDanmakuData = async () => { + // 如果正在处理,跳过本次查询 + if (isProcessing.value) { + return + } + try { + isProcessing.value = true const now = new Date() let params = {} - - if (isFirstLoad.value) { + + // 构建查询参数 + if (!lastQueryTime.value) { // 首次加载:获取最近30条记录 params = { page: 1, @@ -1543,38 +1461,21 @@ const loadDanmakuData = async () => { sort_order: 'desc' } } else { - // 持续轮询:获取最新的记录 - if (!lastEnqueueTime.value) { - // 如果没有上次入队时间,获取最近1分钟的记录 - const oneMinuteAgo = new Date(now.getTime() - 60 * 1000) - params = { - page: 1, - page_size: 50, - start_time: formatDateTime(oneMinuteAgo), - sort_by: 'created_at', - sort_order: 'desc' - } - } else { - // 获取自上次入队时间之后的新记录 - const nextTime = new Date(lastEnqueueTime.value.getTime() + 1) - params = { - page: 1, - page_size: 50, - start_time: formatDateTime(nextTime), - sort_by: 'created_at', - sort_order: 'desc' - } + // 获取自上次查询时间之后的新记录 + params = { + page: 1, + page_size: 50, + start_time: formatDateTime(lastQueryTime.value), + sort_by: 'created_at', + sort_order: 'desc' } } const response = await apiCallApi.getAdminApiCalls(params) - - // 重置重试计数(请求成功) - danmakuRetryCount.value = 0 - + if (response.success && response.data?.items) { const records = response.data.items - + // 过滤掉已处理的记录(使用ID去重) const newRecords = records.filter(record => { const recordId = record.id || `${record.user_id || record.userId}_${record.created_at || record.createdAt}` @@ -1582,113 +1483,51 @@ const loadDanmakuData = async () => { return false } processedRecordIds.value.add(recordId) + // 限制已处理ID集合的大小,避免内存泄漏 - if (processedRecordIds.value.size > 1000) { + if (processedRecordIds.value.size > MAX_PROCESSED_IDS) { const idsArray = Array.from(processedRecordIds.value) - processedRecordIds.value = new Set(idsArray.slice(-500)) + processedRecordIds.value = new Set(idsArray.slice(-MAX_PROCESSED_IDS / 2)) } return true }) - - if (newRecords.length === 0) { - // 没有新记录,但更新lastEnqueueTime为查询到的最新记录时间 - if (records.length > 0) { - const latestRecord = records[0] - const latestTime = new Date(latestRecord.created_at || latestRecord.createdAt || now) - if (!lastEnqueueTime.value || latestTime > lastEnqueueTime.value) { - lastEnqueueTime.value = latestTime - if (latestRecord.id) { - lastEnqueuedId.value = latestRecord.id - } - } + + // 更新查询时间 + if (records.length > 0) { + const latestRecord = records.reduce((latest, current) => { + const latestTime = new Date(latest.created_at || latest.createdAt || 0).getTime() + const currentTime = new Date(current.created_at || current.createdAt || 0).getTime() + return currentTime > latestTime ? current : latest + }) + const latestTime = new Date(latestRecord.created_at || latestRecord.createdAt || now) + if (!lastQueryTime.value || latestTime > lastQueryTime.value) { + lastQueryTime.value = latestTime } - return - } - - // 批量查询调用次数(优化性能) - const callCountMap = await batchQueryCallCounts(newRecords) - - // 将记录转换为弹幕项并存入队列 - const newItems = newRecords.map((record, index) => { - const username = getDisplayUsername(record) - const apiName = getApiName(record) - const userId = record.user_id || record.userId || 'unknown' - const createdAt = record.created_at || record.createdAt || new Date().toISOString() - const productId = record.product_id || record.productId || '' - - // 使用记录ID作为唯一标识(优先使用后端ID) - const tempRecordId = record.id || `${userId}_${productId}_${createdAt}` - const recordId = record.id || `${tempRecordId}_${index}_${Math.random().toString(36).substr(2, 9)}` - - // 从批量查询结果中获取调用次数 - const callCount = callCountMap.get(tempRecordId) || 1 - - return { - id: recordId, - userId: userId, - username: username, - apiName: apiName, - productId: productId, - createdAt: createdAt, - timestamp: new Date(createdAt).getTime(), - callCount: callCount, - // 详细信息用于悬停显示 - clientIp: record.client_ip || record.clientIp || '', - status: record.status || '', - cost: record.cost || '', - transactionId: record.transaction_id || record.transactionId || '', - // 弹幕显示相关属性(将在显示时计算) - displayDelay: 0, - rowIndex: -1, - displayOrder: 0, - randomTop: undefined - } - }) - - // 将新记录存入队列(按时间排序,旧的在前,新的在后) - const sortedItems = newItems.sort((a, b) => a.timestamp - b.timestamp) - danmakuQueue.value.push(...sortedItems) - - // 限制队列大小,避免内存泄漏 - if (danmakuQueue.value.length > MAX_QUEUE_SIZE) { - danmakuQueue.value = danmakuQueue.value.slice(-MAX_QUEUE_SIZE) - } - - // 更新lastEnqueueTime为最后入队记录的时间 - if (sortedItems.length > 0) { - const lastItem = sortedItems[sortedItems.length - 1] - lastEnqueueTime.value = new Date(lastItem.createdAt) - if (lastItem.id) { - lastEnqueuedId.value = lastItem.id + } else { + // 如果没有记录,更新时间到当前时间减1秒 + const oneSecondAgo = new Date(now.getTime() - 1000) + if (!lastQueryTime.value || oneSecondAgo > lastQueryTime.value) { + lastQueryTime.value = oneSecondAgo } } - - console.log(`查询到 ${newRecords.length} 条新记录,已存入队列。当前队列长度: ${danmakuQueue.value.length}`) - - // 标记首次加载完成 - if (isFirstLoad.value) { - isFirstLoad.value = false - } - } else { - // 请求失败时,保持lastEnqueueTime不变,确保下次查询不会跳过记录 - if (!isFirstLoad.value) { - console.log('请求失败,保持lastEnqueueTime不变:', lastEnqueueTime.value) + + // 如果有新记录,立即处理并显示 + if (newRecords.length > 0) { + // 按时间排序(旧的在前,新的在后) + const sortedRecords = newRecords.sort((a, b) => { + const timeA = new Date(a.created_at || a.createdAt || 0).getTime() + const timeB = new Date(b.created_at || b.createdAt || 0).getTime() + return timeA - timeB + }) + + // 立即处理并显示新记录 + await processAndDisplayRecords(sortedRecords) } } } catch (err) { console.error('获取弹幕数据失败:', err) - // 增加重试计数 - danmakuRetryCount.value++ - - // 如果连续失败次数超过阈值,使用指数退避延迟重试 - if (danmakuRetryCount.value >= MAX_RETRY_COUNT) { - console.warn('弹幕数据获取连续失败,将在', RETRY_DELAY, 'ms后重试') - setTimeout(() => { - danmakuRetryCount.value = 0 // 重置计数 - loadDanmakuData() - }, RETRY_DELAY * danmakuRetryCount.value) - } - // 不显示错误消息,避免干扰用户体验 + } finally { + isProcessing.value = false } } @@ -1696,7 +1535,7 @@ const loadDanmakuData = async () => { // 计算弹幕右边缘离开容器右边界所需的时间(秒) const getDanmakuExitRightTime = () => { if (!danmakuContainer.value) { - return 1.5 * DANMAKU_DELAY_REDUCTION_FACTOR // 默认值,应用缩减系数 + return 1.0 // 默认值 } const containerWidth = danmakuContainer.value.clientWidth || 1200 @@ -1710,8 +1549,8 @@ const getDanmakuExitRightTime = () => { // 时间 = (弹幕宽度 / 总移动距离) * 动画持续时间 const exitTime = (danmakuWidth / totalDistance) * DANMAKU_ANIMATION_DURATION - // 应用缩减系数并限制在合理范围内(0.8-2.5秒) - return Math.max(0.8, Math.min(exitTime * DANMAKU_DELAY_REDUCTION_FACTOR, 2.5)) + // 限制在合理范围内(0.5-2秒) + return Math.max(0.5, Math.min(exitTime, 2.0)) } // 获取随机弹幕位置(避免重叠) @@ -1871,64 +1710,87 @@ const resumeDanmaku = (itemId) => { pausedDanmakuIds.value.delete(itemId) } -// 从队列中取出弹幕并显示 -const processDanmakuQueue = () => { - // 如果队列为空,等待下次调用 - if (danmakuQueue.value.length === 0) { +// 处理并显示记录 - 实时模式:立即显示新记录 +const processAndDisplayRecords = async (records) => { + if (!records || records.length === 0) { return } - - // 从队列头部取出一条弹幕(FIFO:旧的先显示) - const item = danmakuQueue.value.shift() - if (!item) { - return - } - - // 计算显示属性 + const now = Date.now() - - // 计算随机top位置(避免重叠) - const randomTop = getRandomDanmakuTop(danmakuItems.value, danmakuItems.value.length) - item.randomTop = randomTop - - // 计算延迟:确保前一条弹幕的右边缘离开容器右边界后再显示下一条 - let delay = 0 - if (danmakuItems.value.length > 0) { - // 找到最后一条弹幕,计算它的右边缘离开右边界的时间 - const lastItem = danmakuItems.value[danmakuItems.value.length - 1] - const lastItemDisplayTime = lastItem.actualDisplayTime || (now - (lastItem.displayDelay || 0) * 1000) - const exitRightTime = getDanmakuExitRightTime() * 1000 // 转换为毫秒 - const lastExitTime = lastItemDisplayTime + exitRightTime + let nextDisplayTime = now + + // 处理每条记录 + records.forEach((record, index) => { + const username = getDisplayUsername(record) + const apiName = getApiName(record) + const userId = record.user_id || record.userId || 'unknown' + const createdAt = record.created_at || record.createdAt || new Date().toISOString() + const productId = record.product_id || record.productId || '' - const timeUntilLastExit = lastExitTime - now - if (timeUntilLastExit > 0) { - delay = timeUntilLastExit / 1000 - } else { - delay = Math.random() * 0.2 // 小的随机延迟,让显示更自然 + // 使用记录ID作为唯一标识 + const recordId = record.id || `${userId}_${productId}_${createdAt}_${index}_${Math.random().toString(36).substr(2, 9)}` + + // 计算调用次数 + const callCount = getCallCount(record) + + // 计算随机top位置(避免重叠) + const randomTop = getRandomDanmakuTop(danmakuItems.value, danmakuItems.value.length + index) + + // 计算显示延迟:确保弹幕不会重叠 + let displayDelay = 0 + if (danmakuItems.value.length > 0) { + // 计算最后一条弹幕的退出时间 + const lastItem = danmakuItems.value[danmakuItems.value.length - 1] + const lastItemStartTime = lastItem.actualDisplayTime || now + const exitRightTime = getDanmakuExitRightTime() * 1000 + const lastItemExitTime = lastItemStartTime + exitRightTime + + // 如果最后一条弹幕还没退出,需要等待 + if (lastItemExitTime > now) { + displayDelay = (lastItemExitTime - now) / 1000 + } else { + // 否则可以立即显示,但添加小随机延迟避免重叠 + displayDelay = Math.random() * 0.3 + } } - } - - item.displayDelay = delay - item.displayOrder = danmakuDisplayOrder.value++ - - // 如果延迟大于0,使用setTimeout延迟显示;否则立即显示 - if (delay > 0) { - item.actualDisplayTime = now + (delay * 1000) - setTimeout(() => { - danmakuItems.value.push(item) - // 限制弹幕数量,移除最旧的弹幕 + + // 创建弹幕项 + const danmakuItem = { + id: recordId, + userId: userId, + username: username, + apiName: apiName, + productId: productId, + createdAt: createdAt, + timestamp: new Date(createdAt).getTime(), + callCount: callCount, + clientIp: record.client_ip || record.clientIp || '', + status: record.status || '', + cost: record.cost || '', + transactionId: record.transaction_id || record.transactionId || '', + displayDelay: displayDelay, + displayOrder: danmakuDisplayOrder.value++, + randomTop: randomTop, + actualDisplayTime: now + (displayDelay * 1000) + } + + // 延迟显示或立即显示 + if (displayDelay > 0) { + setTimeout(() => { + danmakuItems.value.push(danmakuItem) + // 限制弹幕数量 + if (danmakuItems.value.length > MAX_DANMAKU_ITEMS) { + danmakuItems.value = danmakuItems.value.slice(-MAX_DANMAKU_ITEMS) + } + }, displayDelay * 1000) + } else { + danmakuItems.value.push(danmakuItem) + // 限制弹幕数量 if (danmakuItems.value.length > MAX_DANMAKU_ITEMS) { danmakuItems.value = danmakuItems.value.slice(-MAX_DANMAKU_ITEMS) } - }, delay * 1000) - } else { - item.actualDisplayTime = now - danmakuItems.value.push(item) - // 限制弹幕数量,移除最旧的弹幕 - if (danmakuItems.value.length > MAX_DANMAKU_ITEMS) { - danmakuItems.value = danmakuItems.value.slice(-MAX_DANMAKU_ITEMS) } - } + }) } // 弹幕动画结束回调 - 从显示列表中移除已播放完成的弹幕 @@ -1942,34 +1804,23 @@ const onDanmakuAnimationEnd = (itemId) => { } } -// 启动弹幕系统(队列机制) +// 启动弹幕系统 - 实时模式 const startDanmakuPolling = () => { - // 立即查询一次数据库,将新记录存入队列 + // 立即查询一次数据库 loadDanmakuData() - // 定时查询数据库(降低频率,因为数据已在队列中缓冲) + // 定时查询数据库,查询到新记录立即显示 danmakuPollingTimer.value = setInterval(() => { loadDanmakuData() }, DANMAKU_POLLING_INTERVAL) - - // 定时从队列中取出弹幕并显示 - danmakuDisplayTimer.value = setInterval(() => { - processDanmakuQueue() - }, DANMAKU_DISPLAY_INTERVAL) } // 停止弹幕系统 const stopDanmakuPolling = () => { - // 停止数据库查询定时器 if (danmakuPollingTimer.value) { clearInterval(danmakuPollingTimer.value) danmakuPollingTimer.value = null } - // 停止队列处理定时器 - if (danmakuDisplayTimer.value) { - clearInterval(danmakuDisplayTimer.value) - danmakuDisplayTimer.value = null - } } onMounted(() => {