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
+69 -9
View File
@@ -72,7 +72,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import Card from '@/components/ui/card.vue'
import { TimeRangePicker } from '@/components/common'
import { CostForecastChart, QuotaProgressCard } from '@/components/stats'
@@ -93,6 +93,13 @@ const providerStats = ref<ProviderStatsItem[]>([])
const forecastLoading = ref(false)
const quotaLoading = ref(false)
let forecastRequestId = 0
let savingsRequestId = 0
let quotaRequestId = 0
let providerStatsRequestId = 0
let loadAllPromise: Promise<void> | null = null
let hasPendingLoadAll = false
let loadAllDebounceTimer: ReturnType<typeof setTimeout> | null = null
const forecastHistory = computed(() => forecast.value?.history || [])
const forecastFuture = computed(() => forecast.value?.forecast || [])
@@ -108,40 +115,93 @@ function buildTimeRangeParams() {
}
async function loadForecast() {
const requestId = ++forecastRequestId
forecastLoading.value = true
try {
forecast.value = await adminApi.getCostForecast(buildTimeRangeParams())
const data = await adminApi.getCostForecast(buildTimeRangeParams())
if (requestId !== forecastRequestId) return
forecast.value = data
} finally {
forecastLoading.value = false
if (requestId === forecastRequestId) {
forecastLoading.value = false
}
}
}
async function loadSavings() {
costSavings.value = await adminApi.getCostSavings(buildTimeRangeParams())
const requestId = ++savingsRequestId
const data = await adminApi.getCostSavings(buildTimeRangeParams())
if (requestId !== savingsRequestId) return
costSavings.value = data
}
async function loadQuotaUsage() {
const requestId = ++quotaRequestId
quotaLoading.value = true
try {
const response = await adminApi.getQuotaUsage()
if (requestId !== quotaRequestId) return
quotaProviders.value = response.providers
} finally {
quotaLoading.value = false
if (requestId === quotaRequestId) {
quotaLoading.value = false
}
}
}
async function loadProviderStats() {
providerStats.value = await usageApi.getUsageByProvider({
const requestId = ++providerStatsRequestId
const stats = await usageApi.getUsageByProvider({
...buildTimeRangeParams(),
limit: 8
})
if (requestId !== providerStatsRequestId) return
providerStats.value = stats
}
async function loadAll() {
await Promise.all([loadForecast(), loadSavings(), loadQuotaUsage(), loadProviderStats()])
if (loadAllPromise) {
hasPendingLoadAll = true
return loadAllPromise
}
loadAllPromise = Promise.all([loadForecast(), loadSavings(), loadQuotaUsage(), loadProviderStats()])
.then(() => undefined)
.finally(() => {
loadAllPromise = null
if (hasPendingLoadAll) {
hasPendingLoadAll = false
void loadAll()
}
})
return loadAllPromise
}
watch(timeRange, loadAll, { deep: true })
function scheduleLoadAll() {
if (loadAllDebounceTimer) {
clearTimeout(loadAllDebounceTimer)
}
loadAllDebounceTimer = setTimeout(() => {
loadAllDebounceTimer = null
void loadAll()
}, 120)
}
onMounted(loadAll)
watch(timeRange, scheduleLoadAll, { deep: true })
onMounted(() => {
void loadAll()
})
onUnmounted(() => {
if (loadAllDebounceTimer) {
clearTimeout(loadAllDebounceTimer)
loadAllDebounceTimer = null
}
hasPendingLoadAll = false
loadAllPromise = null
forecastRequestId += 1
savingsRequestId += 1
quotaRequestId += 1
providerStatsRequestId += 1
})
</script>