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
+115 -20
View File
@@ -144,7 +144,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { Card, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
import LineChart from '@/components/charts/LineChart.vue'
import { LoadingState, TimeRangePicker } from '@/components/common'
@@ -184,6 +184,15 @@ const summaryLoading = ref(false)
const series = ref<TimeSeriesItem[]>([])
const comparisonSeries = ref<TimeSeriesItem[]>([])
const seriesLoading = ref(false)
let leaderboardRequestId = 0
let summaryRequestId = 0
let seriesRequestId = 0
let leaderboardLoadPromise: Promise<void> | null = null
let hasPendingLeaderboardLoad = false
let leaderboardDebounceTimer: ReturnType<typeof setTimeout> | null = null
let userPanelsLoadPromise: Promise<void> | null = null
let hasPendingUserPanelsLoad = false
let userPanelsDebounceTimer: ReturnType<typeof setTimeout> | null = null
function buildTimeRangeParams() {
return {
@@ -204,6 +213,12 @@ async function loadUsers() {
}
async function loadLeaderboard() {
if (leaderboardLoadPromise) {
hasPendingLeaderboardLoad = true
return leaderboardLoadPromise
}
leaderboardLoadPromise = (async () => {
const requestId = ++leaderboardRequestId
leaderboardLoading.value = true
try {
const response = await adminApi.getLeaderboardUsers({
@@ -211,46 +226,90 @@ async function loadLeaderboard() {
metric: metric.value,
limit: 10
})
if (requestId !== leaderboardRequestId) return
leaderboard.value = response.items
} finally {
leaderboardLoading.value = false
if (requestId === leaderboardRequestId) {
leaderboardLoading.value = false
}
}
})().finally(() => {
leaderboardLoadPromise = null
if (hasPendingLeaderboardLoad) {
hasPendingLeaderboardLoad = false
void loadLeaderboard()
}
})
return leaderboardLoadPromise
}
async function loadSummary() {
if (!selectedUserId.value) return
const requestId = ++summaryRequestId
summaryLoading.value = true
try {
userSummary.value = await usageApi.getUsageStats({
const summary = await usageApi.getUsageStats({
...buildTimeRangeParams(),
user_id: selectedUserId.value
})
if (requestId !== summaryRequestId) return
userSummary.value = summary
} finally {
summaryLoading.value = false
if (requestId === summaryRequestId) {
summaryLoading.value = false
}
}
}
async function loadSeries() {
if (!selectedUserId.value) return
const requestId = ++seriesRequestId
seriesLoading.value = true
try {
series.value = await adminApi.getTimeSeries({
const baseParams = {
...buildTimeRangeParams(),
user_id: selectedUserId.value
})
comparisonSeries.value = []
if (compareUserId.value && compareUserId.value !== '__none__') {
comparisonSeries.value = await adminApi.getTimeSeries({
}
const shouldCompare = Boolean(compareUserId.value && compareUserId.value !== '__none__')
const comparePromise: Promise<TimeSeriesItem[]> = shouldCompare
? adminApi.getTimeSeries({
...buildTimeRangeParams(),
user_id: compareUserId.value
})
}
: Promise.resolve([])
const [primarySeries, compareSeries] = await Promise.all([
adminApi.getTimeSeries(baseParams),
comparePromise
])
if (requestId !== seriesRequestId) return
series.value = primarySeries
comparisonSeries.value = compareSeries
} finally {
seriesLoading.value = false
if (requestId === seriesRequestId) {
seriesLoading.value = false
}
}
}
async function loadUserPanels() {
if (userPanelsLoadPromise) {
hasPendingUserPanelsLoad = true
return userPanelsLoadPromise
}
userPanelsLoadPromise = Promise.all([loadSummary(), loadSeries()])
.then(() => undefined)
.finally(() => {
userPanelsLoadPromise = null
if (hasPendingUserPanelsLoad) {
hasPendingUserPanelsLoad = false
void loadUserPanels()
}
})
return userPanelsLoadPromise
}
const seriesChartData = computed(() => ({
labels: series.value.map(item => item.date),
datasets: [
@@ -284,16 +343,52 @@ const comparisonChartData = computed(() => ({
]
}))
watch([timeRange, metric], loadLeaderboard, { deep: true })
watch([timeRange, selectedUserId, compareUserId], () => {
loadSummary()
loadSeries()
}, { deep: true })
function scheduleLeaderboardLoad() {
if (leaderboardDebounceTimer) {
clearTimeout(leaderboardDebounceTimer)
}
leaderboardDebounceTimer = setTimeout(() => {
leaderboardDebounceTimer = null
void loadLeaderboard()
}, 120)
}
function scheduleUserPanelsLoad() {
if (userPanelsDebounceTimer) {
clearTimeout(userPanelsDebounceTimer)
}
userPanelsDebounceTimer = setTimeout(() => {
userPanelsDebounceTimer = null
void loadUserPanels()
}, 120)
}
watch([timeRange, metric], scheduleLeaderboardLoad, { deep: true })
watch([timeRange, selectedUserId, compareUserId], scheduleUserPanelsLoad, { deep: true })
onMounted(async () => {
await loadUsers()
await loadLeaderboard()
await loadSummary()
await loadSeries()
await Promise.all([
loadLeaderboard(),
loadUserPanels()
])
})
onUnmounted(() => {
if (leaderboardDebounceTimer) {
clearTimeout(leaderboardDebounceTimer)
leaderboardDebounceTimer = null
}
if (userPanelsDebounceTimer) {
clearTimeout(userPanelsDebounceTimer)
userPanelsDebounceTimer = null
}
hasPendingLeaderboardLoad = false
hasPendingUserPanelsLoad = false
leaderboardLoadPromise = null
userPanelsLoadPromise = null
leaderboardRequestId += 1
summaryRequestId += 1
seriesRequestId += 1
})
</script>