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:
fawney19
2026-03-03 22:04:40 +08:00
parent 0a60492146
commit 97b0146ce9
66 changed files with 2306 additions and 657 deletions
+52 -14
View File
@@ -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)