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

View File

@@ -1,4 +1,5 @@
import apiClient from './client'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
// LDAP 配置导出结构
export interface LDAPConfigExport {
@@ -743,10 +744,17 @@ export const adminApi = {
include_inactive?: boolean
exclude_admin?: boolean
}): Promise<LeaderboardResponse> {
const response = await apiClient.get<LeaderboardResponse>('/api/admin/stats/leaderboard/users', {
params
})
return response.data
const cacheKey = buildCacheKey('admin:stats:leaderboard:users', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<LeaderboardResponse>('/api/admin/stats/leaderboard/users', {
params
})
return response.data
},
20 * 1000
)
},
async getLeaderboardApiKeys(params?: {
@@ -764,10 +772,17 @@ export const adminApi = {
include_inactive?: boolean
exclude_admin?: boolean
}): Promise<LeaderboardResponse> {
const response = await apiClient.get<LeaderboardResponse>('/api/admin/stats/leaderboard/api-keys', {
params
})
return response.data
const cacheKey = buildCacheKey('admin:stats:leaderboard:api-keys', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<LeaderboardResponse>('/api/admin/stats/leaderboard/api-keys', {
params
})
return response.data
},
20 * 1000
)
},
async getLeaderboardModels(params?: {
@@ -783,10 +798,17 @@ export const adminApi = {
provider_name?: string
model?: string
}): Promise<LeaderboardResponse> {
const response = await apiClient.get<LeaderboardResponse>('/api/admin/stats/leaderboard/models', {
params
})
return response.data
const cacheKey = buildCacheKey('admin:stats:leaderboard:models', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<LeaderboardResponse>('/api/admin/stats/leaderboard/models', {
params
})
return response.data
},
20 * 1000
)
},
async getCostForecast(params?: {
@@ -798,10 +820,17 @@ export const adminApi = {
days?: number
forecast_days?: number
}): Promise<CostForecastResponse> {
const response = await apiClient.get<CostForecastResponse>('/api/admin/stats/cost/forecast', {
params
})
return response.data
const cacheKey = buildCacheKey('admin:stats:cost:forecast', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<CostForecastResponse>('/api/admin/stats/cost/forecast', {
params
})
return response.data
},
30 * 1000
)
},
async getCostSavings(params?: {
@@ -813,15 +842,28 @@ export const adminApi = {
provider_name?: string
model?: string
}): Promise<CostSavingsResponse> {
const response = await apiClient.get<CostSavingsResponse>('/api/admin/stats/cost/savings', {
params
})
return response.data
const cacheKey = buildCacheKey('admin:stats:cost:savings', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<CostSavingsResponse>('/api/admin/stats/cost/savings', {
params
})
return response.data
},
30 * 1000
)
},
async getQuotaUsage(): Promise<QuotaUsageResponse> {
const response = await apiClient.get<QuotaUsageResponse>('/api/admin/stats/providers/quota-usage')
return response.data
return cachedRequest(
'admin:stats:providers:quota-usage',
async () => {
const response = await apiClient.get<QuotaUsageResponse>('/api/admin/stats/providers/quota-usage')
return response.data
},
30 * 1000
)
},
async getPercentiles(params?: {
@@ -831,10 +873,17 @@ export const adminApi = {
timezone?: string
tz_offset_minutes?: number
}): Promise<PercentileItem[]> {
const response = await apiClient.get<PercentileItem[]>('/api/admin/stats/performance/percentiles', {
params
})
return response.data
const cacheKey = buildCacheKey('admin:stats:performance:percentiles', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<PercentileItem[]>('/api/admin/stats/performance/percentiles', {
params
})
return response.data
},
20 * 1000
)
},
async getErrorDistribution(params?: {
@@ -844,10 +893,17 @@ export const adminApi = {
timezone?: string
tz_offset_minutes?: number
}): Promise<ErrorDistributionResponse> {
const response = await apiClient.get<ErrorDistributionResponse>('/api/admin/stats/errors/distribution', {
params
})
return response.data
const cacheKey = buildCacheKey('admin:stats:errors:distribution', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<ErrorDistributionResponse>('/api/admin/stats/errors/distribution', {
params
})
return response.data
},
20 * 1000
)
},
async getComparison(params: {
@@ -882,8 +938,15 @@ export const adminApi = {
model?: string
provider_name?: string
}): Promise<Array<Record<string, unknown>>> {
const response = await apiClient.get<Array<Record<string, unknown>>>('/api/admin/stats/time-series', { params })
return response.data
const cacheKey = buildCacheKey('admin:stats:time-series', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<Array<Record<string, unknown>>>('/api/admin/stats/time-series', { params })
return response.data
},
20 * 1000
)
},
}

View File

@@ -3,6 +3,7 @@
*/
import api from './client'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
export interface CacheStats {
scheduler: string
@@ -314,8 +315,15 @@ export const cacheAnalysisApi = {
user_id?: string
include_user_info?: boolean
}): Promise<IntervalTimelineResponse> {
const response = await api.get('/api/admin/usage/cache-affinity/interval-timeline', { params })
return response.data
const cacheKey = buildCacheKey('cache-affinity:interval-timeline', params as Record<string, unknown> | undefined)
return cachedRequest(
cacheKey,
async () => {
const response = await api.get('/api/admin/usage/cache-affinity/interval-timeline', { params })
return response.data
},
30000
)
}
}

View File

@@ -1,4 +1,5 @@
import apiClient from './client'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
export interface DashboardStat {
name: string
@@ -289,8 +290,15 @@ export interface TimeRangeParams {
export const dashboardApi = {
// 获取仪表盘统计数据
async getStats(params?: TimeRangeParams): Promise<DashboardStatsResponse> {
const response = await apiClient.get<DashboardStatsResponse>('/api/dashboard/stats', { params })
return response.data
const cacheKey = buildCacheKey('dashboard:stats', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<DashboardStatsResponse>('/api/dashboard/stats', { params })
return response.data
},
10 * 1000
)
},
// 获取最近的请求记录
@@ -303,8 +311,14 @@ export const dashboardApi = {
// 获取提供商状态
async getProviderStatus(): Promise<ProviderStatus[]> {
const response = await apiClient.get<ProviderStatusResponse>('/api/dashboard/provider-status')
return response.data.providers
return cachedRequest(
'dashboard:provider-status',
async () => {
const response = await apiClient.get<ProviderStatusResponse>('/api/dashboard/provider-status')
return response.data.providers
},
20 * 1000
)
},
// 获取请求详情
@@ -316,10 +330,17 @@ export const dashboardApi = {
// 获取每日统计数据
async getDailyStats(params?: TimeRangeParams & { days?: number }): Promise<DailyStatsResponse> {
const response = await apiClient.get<DailyStatsResponse>('/api/dashboard/daily-stats', {
params
})
return response.data
const cacheKey = buildCacheKey('dashboard:daily-stats', params)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<DailyStatsResponse>('/api/dashboard/daily-stats', {
params
})
return response.data
},
20 * 1000
)
},
// 获取 cURL 命令数据(含明文 API Key

View File

@@ -1,4 +1,5 @@
import client from '../client'
import { dedupedRequest, buildCacheKey } from '@/utils/cache'
import type {
GlobalModelCreate,
GlobalModelUpdate,
@@ -27,16 +28,21 @@ export async function getGlobalModels(params?: {
is_active?: boolean
search?: string
}): Promise<GlobalModelListResponse> {
const response = await client.get('/api/admin/models/global', { params })
return response.data
const key = buildCacheKey('global-models:list', params as Record<string, unknown> | undefined)
return dedupedRequest(key, async () => {
const response = await client.get('/api/admin/models/global', { params })
return response.data
})
}
/**
* 获取单个 GlobalModel 详情
*/
export async function getGlobalModel(id: string): Promise<GlobalModelWithStats> {
const response = await client.get(`/api/admin/models/global/${id}`)
return response.data
return dedupedRequest(`global-models:detail:${id}`, async () => {
const response = await client.get(`/api/admin/models/global/${id}`)
return response.data
})
}
/**
@@ -112,10 +118,12 @@ export async function getGlobalModelProviders(globalModelId: string): Promise<{
providers: ModelCatalogProviderDetail[]
total: number
}> {
const response = await client.get(
`/api/admin/models/global/${globalModelId}/providers`
)
return response.data
return dedupedRequest(`global-models:providers:${globalModelId}`, async () => {
const response = await client.get(
`/api/admin/models/global/${globalModelId}/providers`
)
return response.data
})
}
/**

View File

@@ -1,4 +1,5 @@
import client from '../client'
import { dedupedRequest } from '@/utils/cache'
import type { AllowedModels, ProxyConfig } from './types/provider'
export interface PoolKeyStatus {
@@ -175,16 +176,21 @@ export interface PoolBatchAction {
}
export async function getPoolOverview(): Promise<PoolOverviewResponse> {
const response = await client.get('/api/admin/pool/overview')
return response.data
return dedupedRequest('pool:overview', async () => {
const response = await client.get<PoolOverviewResponse>('/api/admin/pool/overview')
return response.data
})
}
export async function listPoolKeys(
providerId: string,
params: PoolKeysQuery = {},
): Promise<PoolKeysPageResponse> {
const response = await client.get(`/api/admin/pool/${providerId}/keys`, { params })
return response.data
const key = `pool:keys:${providerId}|${params.page ?? ''}|${params.page_size ?? ''}|${params.search ?? ''}|${params.status ?? ''}`
return dedupedRequest(key, async () => {
const response = await client.get<PoolKeysPageResponse>(`/api/admin/pool/${providerId}/keys`, { params })
return response.data
})
}
export async function batchActionPoolKeys(

View File

@@ -1,4 +1,5 @@
import client from '../client'
import { dedupedRequest } from '@/utils/cache'
import type {
ClaudeCodeAdvancedConfig,
FailoverRulesConfig,
@@ -11,16 +12,20 @@ import type {
* 获取 Providers 摘要(包含 Endpoints 统计)
*/
export async function getProvidersSummary(): Promise<ProviderWithEndpointsSummary[]> {
const response = await client.get('/api/admin/providers/summary')
return response.data
return dedupedRequest('providers:summary', async () => {
const response = await client.get<ProviderWithEndpointsSummary[]>('/api/admin/providers/summary')
return response.data
})
}
/**
* 获取单个 Provider 的详细信息
*/
export async function getProvider(providerId: string): Promise<ProviderWithEndpointsSummary> {
const response = await client.get(`/api/admin/providers/${providerId}/summary`)
return response.data
return dedupedRequest(`providers:detail:${providerId}`, async () => {
const response = await client.get<ProviderWithEndpointsSummary>(`/api/admin/providers/${providerId}/summary`)
return response.data
})
}
/**
@@ -214,6 +219,8 @@ export interface ProviderMappingPreviewResponse {
export async function getProviderMappingPreview(
providerId: string
): Promise<ProviderMappingPreviewResponse> {
const response = await client.get(`/api/admin/providers/${providerId}/mapping-preview`)
return response.data
return dedupedRequest(`providers:mapping-preview:${providerId}`, async () => {
const response = await client.get<ProviderMappingPreviewResponse>(`/api/admin/providers/${providerId}/mapping-preview`)
return response.data
})
}

View File

@@ -1,6 +1,7 @@
import apiClient from './client'
import type { ActivityHeatmap } from '@/types/activity'
import type { TieredPricingConfig } from './endpoints/types'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
export interface Profile {
id: string // UUID
@@ -328,8 +329,15 @@ export const meApi = {
points: Array<{ x: string; y: number; model?: string }>
models?: string[]
}> {
const response = await apiClient.get('/api/users/me/usage/interval-timeline', { params })
return response.data
const cacheKey = buildCacheKey('me:interval-timeline', params as Record<string, unknown> | undefined)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get('/api/users/me/usage/interval-timeline', { params })
return response.data
},
30000
)
},
/**
@@ -337,7 +345,13 @@ export const meApi = {
* 后端已缓存5分钟
*/
async getActivityHeatmap(): Promise<ActivityHeatmap> {
const response = await apiClient.get<ActivityHeatmap>('/api/users/me/usage/heatmap')
return response.data
return cachedRequest(
'me-activity-heatmap',
async () => {
const response = await apiClient.get<ActivityHeatmap>('/api/users/me/usage/heatmap')
return response.data
},
60000
)
}
}

View File

@@ -1,5 +1,5 @@
import apiClient from './client'
import { cachedRequest } from '@/utils/cache'
import { cachedRequest, dedupedRequest, buildCacheKey } from '@/utils/cache'
import type { ActivityHeatmap } from '@/types/activity'
export interface UsageRecord {
@@ -187,8 +187,11 @@ export const usageApi = {
limit: number
offset: number
}> {
const response = await apiClient.get('/api/admin/usage/records', { params })
return response.data
const key = buildCacheKey('usage:records', params as Record<string, unknown> | undefined)
return dedupedRequest(key, async () => {
const response = await apiClient.get('/api/admin/usage/records', { params })
return response.data
})
},
/**
@@ -226,7 +229,13 @@ export const usageApi = {
* 后端已缓存5分钟
*/
async getActivityHeatmap(): Promise<ActivityHeatmap> {
const response = await apiClient.get<ActivityHeatmap>('/api/admin/usage/heatmap')
return response.data
return cachedRequest(
'admin-usage-activity-heatmap',
async () => {
const response = await apiClient.get<ActivityHeatmap>('/api/admin/usage/heatmap')
return response.data
},
60000
)
}
}