mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
perf: 全栈查询优化、前端缓存去重与页面可见性优化
后端: - SQL count 查询统一改用 func.count() 子查询替代 query.count() - Dashboard/Audit 等页面多次独立查询合并为单次聚合查询 - Provider summary 列表改为批量查询消除 N+1 问题 - DailyStats 逐天循环查询改为 CASE 分桶单次查询 - 使用 load_only() 减少不必要的列加载 - cache_decorator 支持嵌套属性路径解析(dotted vary_by) - 多个管理/公共端点新增 @cache_result 缓存装饰 前端: - cache.ts 新增 in-flight 请求复用、dedupedRequest、buildCacheKey - 大量 API 调用添加前端缓存或去重 - 多个页面定时器在标签页隐藏时暂停、可见时恢复 - Auth 检查从 setInterval 改为 storage + visibilitychange 事件驱动 - 请求竞态防护(requestId 模式) 数据库: - Usage 表新增 idx_usage_status_user_created 复合索引
This commit is contained in:
@@ -987,6 +987,10 @@ const dailyTimeRange = ref<DateRangeParams>(getDateRangeFromPeriod('last7days'))
|
||||
// 统计周期
|
||||
const loadingDaily = ref(false)
|
||||
const loading = ref(false)
|
||||
let dailyStatsRequestId = 0
|
||||
let dailyStatsLoadPromise: Promise<void> | null = null
|
||||
let hasPendingDailyStatsLoad = false
|
||||
let dailyStatsDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
// 公告
|
||||
@@ -1279,7 +1283,8 @@ onMounted(async () => {
|
||||
}
|
||||
await Promise.all([
|
||||
loadDashboardData(),
|
||||
loadAnnouncements()
|
||||
loadAnnouncements(),
|
||||
loadDailyStats()
|
||||
])
|
||||
await nextTick()
|
||||
setupTimelineResizeObserver()
|
||||
@@ -1298,6 +1303,13 @@ onBeforeUnmount(() => {
|
||||
statsPanelObserver = null
|
||||
announcementsTimelineObserver?.disconnect()
|
||||
announcementsTimelineObserver = null
|
||||
if (dailyStatsDebounceTimer) {
|
||||
clearTimeout(dailyStatsDebounceTimer)
|
||||
dailyStatsDebounceTimer = null
|
||||
}
|
||||
hasPendingDailyStatsLoad = false
|
||||
dailyStatsLoadPromise = null
|
||||
dailyStatsRequestId += 1
|
||||
})
|
||||
|
||||
async function loadDashboardData() {
|
||||
@@ -1326,22 +1338,48 @@ async function loadDashboardData() {
|
||||
}
|
||||
|
||||
async function loadDailyStats() {
|
||||
loadingDaily.value = true
|
||||
try {
|
||||
const response = await dashboardApi.getDailyStats(dailyTimeRange.value)
|
||||
dailyStats.value = response.daily_stats
|
||||
providerSummary.value = response.provider_summary || []
|
||||
} catch {
|
||||
dailyStats.value = []
|
||||
providerSummary.value = []
|
||||
} finally {
|
||||
loadingDaily.value = false
|
||||
if (dailyStatsLoadPromise) {
|
||||
hasPendingDailyStatsLoad = true
|
||||
return dailyStatsLoadPromise
|
||||
}
|
||||
const requestId = ++dailyStatsRequestId
|
||||
loadingDaily.value = true
|
||||
dailyStatsLoadPromise = (async () => {
|
||||
try {
|
||||
const response = await dashboardApi.getDailyStats(dailyTimeRange.value)
|
||||
if (requestId !== dailyStatsRequestId) return
|
||||
dailyStats.value = response.daily_stats
|
||||
providerSummary.value = response.provider_summary || []
|
||||
} catch {
|
||||
if (requestId !== dailyStatsRequestId) return
|
||||
dailyStats.value = []
|
||||
providerSummary.value = []
|
||||
} finally {
|
||||
if (requestId === dailyStatsRequestId) {
|
||||
loadingDaily.value = false
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
dailyStatsLoadPromise = null
|
||||
if (hasPendingDailyStatsLoad) {
|
||||
hasPendingDailyStatsLoad = false
|
||||
void loadDailyStats()
|
||||
}
|
||||
})
|
||||
return dailyStatsLoadPromise
|
||||
}
|
||||
|
||||
watch(dailyTimeRange, async () => {
|
||||
await loadDailyStats()
|
||||
}, { deep: true })
|
||||
function scheduleDailyStatsLoad() {
|
||||
if (dailyStatsDebounceTimer) {
|
||||
clearTimeout(dailyStatsDebounceTimer)
|
||||
}
|
||||
dailyStatsDebounceTimer = setTimeout(() => {
|
||||
dailyStatsDebounceTimer = null
|
||||
void loadDailyStats()
|
||||
}, 120)
|
||||
}
|
||||
|
||||
watch(dailyTimeRange, scheduleDailyStatsLoad, { deep: true })
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
|
||||
@@ -247,14 +247,17 @@ const hasActiveRequests = computed(() => activeRequestIds.value.length > 0)
|
||||
// 自动刷新定时器
|
||||
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
let globalAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
let refreshInFlight: Promise<void> | null = null
|
||||
const AUTO_REFRESH_INTERVAL = 1000 // 1秒刷新一次(用于活跃请求)
|
||||
const GLOBAL_AUTO_REFRESH_INTERVAL = 5000 // 5秒刷新一次(全局自动刷新)
|
||||
const globalAutoRefresh = ref(false) // 全局自动刷新开关
|
||||
const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hidden)
|
||||
|
||||
// 轮询活跃请求状态(轻量级,只更新状态变化的记录)
|
||||
|
||||
let pollInFlight = false
|
||||
async function pollActiveRequests() {
|
||||
if (!isPageVisible.value) return
|
||||
if (!hasActiveRequests.value) return
|
||||
if (pollInFlight) return
|
||||
pollInFlight = true
|
||||
@@ -338,6 +341,7 @@ async function pollActiveRequests() {
|
||||
|
||||
// 启动自动刷新
|
||||
function startAutoRefresh() {
|
||||
if (!isPageVisible.value) return
|
||||
if (autoRefreshTimer) return
|
||||
autoRefreshTimer = setInterval(pollActiveRequests, AUTO_REFRESH_INTERVAL)
|
||||
}
|
||||
@@ -353,7 +357,7 @@ function stopAutoRefresh() {
|
||||
// 监听活跃请求状态,自动启动/停止刷新
|
||||
// 1秒轮询始终用于活跃请求的实时更新,不受全局刷新影响
|
||||
watch(hasActiveRequests, (hasActive) => {
|
||||
if (hasActive) {
|
||||
if (hasActive && isPageVisible.value) {
|
||||
startAutoRefresh()
|
||||
} else {
|
||||
stopAutoRefresh()
|
||||
@@ -362,6 +366,7 @@ watch(hasActiveRequests, (hasActive) => {
|
||||
|
||||
// 启动全局自动刷新
|
||||
function startGlobalAutoRefresh() {
|
||||
if (!isPageVisible.value) return
|
||||
if (globalAutoRefreshTimer) return
|
||||
globalAutoRefreshTimer = setInterval(refreshData, GLOBAL_AUTO_REFRESH_INTERVAL)
|
||||
}
|
||||
@@ -378,15 +383,34 @@ function stopGlobalAutoRefresh() {
|
||||
function handleAutoRefreshChange(value: boolean) {
|
||||
globalAutoRefresh.value = value
|
||||
if (value) {
|
||||
refreshData() // 立即刷新一次
|
||||
if (isPageVisible.value) {
|
||||
refreshData() // 立即刷新一次
|
||||
}
|
||||
startGlobalAutoRefresh()
|
||||
} else {
|
||||
stopGlobalAutoRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
isPageVisible.value = !document.hidden
|
||||
if (!isPageVisible.value) {
|
||||
stopAutoRefresh()
|
||||
stopGlobalAutoRefresh()
|
||||
return
|
||||
}
|
||||
if (hasActiveRequests.value) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
if (globalAutoRefresh.value) {
|
||||
refreshData()
|
||||
startGlobalAutoRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
stopAutoRefresh()
|
||||
stopGlobalAutoRefresh()
|
||||
})
|
||||
@@ -419,6 +443,8 @@ const selectedRequestId = ref<string | null>(null)
|
||||
|
||||
// 初始化加载
|
||||
onMounted(async () => {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
// 所有数据源并行加载(stats/heatmap/records/users 之间没有数据依赖)
|
||||
const statsTask = loadStats(timeRange.value).catch(err => {
|
||||
log.error('加载统计数据失败:', err)
|
||||
@@ -548,11 +574,28 @@ async function handleFilterStatusChange(value: string) {
|
||||
|
||||
// 刷新数据
|
||||
async function refreshData() {
|
||||
await loadStats(timeRange.value)
|
||||
if (isAdminPage.value) {
|
||||
await loadRecords({ page: currentPage.value, pageSize: pageSize.value }, getCurrentFilters())
|
||||
if (!isPageVisible.value) return
|
||||
if (refreshInFlight) return refreshInFlight
|
||||
|
||||
refreshInFlight = (async () => {
|
||||
if (isAdminPage.value) {
|
||||
// loadStats 会同步更新 currentDateRange,随后 loadRecords 复用同一时间范围
|
||||
await Promise.all([
|
||||
loadStats(timeRange.value),
|
||||
loadRecords({ page: currentPage.value, pageSize: pageSize.value }, getCurrentFilters())
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
await loadStats(timeRange.value)
|
||||
// 用户页面:loadStats 已包含记录加载
|
||||
})()
|
||||
|
||||
try {
|
||||
await refreshInFlight
|
||||
} finally {
|
||||
refreshInFlight = null
|
||||
}
|
||||
// 用户页面:loadStats 已包含记录加载
|
||||
}
|
||||
|
||||
// 显示请求详情
|
||||
|
||||
Reference in New Issue
Block a user