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
+73 -12
View File
@@ -1158,7 +1158,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import {
Search,
Upload,
@@ -1243,11 +1243,19 @@ const proxyNodesStore = useProxyNodesStore()
// --- Overview ---
const poolProviders = ref<PoolOverviewItem[]>([])
const overviewLoading = ref(true)
let overviewRequestId = 0
let selectProviderRequestId = 0
let providerDataRequestId = 0
let keysRequestId = 0
let keysSearchDebounceTimer: number | null = null
let suppressFiltersWatch = false
async function loadOverview() {
const requestId = ++overviewRequestId
overviewLoading.value = true
try {
const res = await getPoolOverview()
if (requestId !== overviewRequestId) return
const enabledProviders = res.items.filter(item => item.pool_enabled)
poolProviders.value = enabledProviders
@@ -1266,9 +1274,12 @@ async function loadOverview() {
}
}
} catch (err) {
if (requestId !== overviewRequestId) return
showError(parseApiError(err))
} finally {
overviewLoading.value = false
if (requestId === overviewRequestId) {
overviewLoading.value = false
}
}
}
@@ -1306,6 +1317,7 @@ const showAccountQuotaColumn = computed(() => {
})
async function selectProvider(id: string) {
const requestId = ++selectProviderRequestId
selectedProviderId.value = id
editingKeyDetail.value = null
keyPermissionsDialogOpen.value = false
@@ -1315,16 +1327,27 @@ async function selectProvider(id: string) {
proxyMobilePopoverOpenKeyId.value = null
schedulingDetailDesktopPopoverOpenKeyId.value = null
schedulingDetailMobilePopoverOpenKeyId.value = null
suppressFiltersWatch = true
currentPage.value = 1
searchQuery.value = ''
statusFilter.value = 'all'
suppressFiltersWatch = false
if (keysSearchDebounceTimer !== null) {
clearTimeout(keysSearchDebounceTimer)
keysSearchDebounceTimer = null
}
await Promise.all([loadKeys(), loadProviderData(id)])
if (requestId !== selectProviderRequestId) return
}
async function loadProviderData(id: string) {
const requestId = ++providerDataRequestId
try {
selectedProviderData.value = await getProvider(id)
const providerData = await getProvider(id)
if (requestId !== providerDataRequestId || selectedProviderId.value !== id) return
selectedProviderData.value = providerData
} catch {
if (requestId !== providerDataRequestId || selectedProviderId.value !== id) return
selectedProviderData.value = null
}
}
@@ -1434,25 +1457,52 @@ async function refreshCurrentPage() {
async function loadKeys() {
if (!selectedProviderId.value) return
const requestId = ++keysRequestId
const providerId = selectedProviderId.value
const page = currentPage.value
const pageSizeValue = pageSize.value
const search = searchQuery.value || undefined
const status = statusFilter.value as 'all' | 'active' | 'cooldown' | 'inactive'
keysLoading.value = true
try {
keyPage.value = await listPoolKeys(selectedProviderId.value, {
page: currentPage.value,
page_size: pageSize.value,
search: searchQuery.value || undefined,
status: statusFilter.value as 'all' | 'active' | 'cooldown' | 'inactive',
const nextPage = await listPoolKeys(providerId, {
page,
page_size: pageSizeValue,
search,
status,
})
if (requestId !== keysRequestId || selectedProviderId.value !== providerId) return
keyPage.value = nextPage
} catch (err) {
if (requestId !== keysRequestId || selectedProviderId.value !== providerId) return
showError(parseApiError(err))
} finally {
keysLoading.value = false
if (requestId === keysRequestId) {
keysLoading.value = false
}
}
}
watch([currentPage, pageSize], () => loadKeys())
watch([searchQuery, statusFilter], () => {
watch([currentPage, pageSize], () => {
void loadKeys()
})
watch(statusFilter, () => {
if (suppressFiltersWatch) return
currentPage.value = 1
loadKeys()
void loadKeys()
})
watch(searchQuery, () => {
if (suppressFiltersWatch) return
currentPage.value = 1
if (keysSearchDebounceTimer !== null) {
clearTimeout(keysSearchDebounceTimer)
}
keysSearchDebounceTimer = window.setTimeout(() => {
keysSearchDebounceTimer = null
void loadKeys()
}, 300)
})
function normalizeAuthTypeForEdit(authType: string): EndpointAPIKey['auth_type'] {
@@ -2224,4 +2274,15 @@ onMounted(async () => {
await loadOverview()
void refreshCurrentPageQuotaInBackground({ silent: true })
})
onBeforeUnmount(() => {
if (keysSearchDebounceTimer !== null) {
clearTimeout(keysSearchDebounceTimer)
keysSearchDebounceTimer = null
}
overviewRequestId += 1
selectProviderRequestId += 1
providerDataRequestId += 1
keysRequestId += 1
})
</script>