This commit is contained in:
2025-12-08 15:03:37 +08:00
parent f293626ce5
commit 9f811b86e9

View File

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