perf: 并行化 admin 聚合路由并完善前端缓存预取

- gateway: usage detail / provider summary / pool overview / users list 改为 tokio join 并行拉取依赖数据
- usage: interval timeline 支持自动刷新并按查询区间动态展示,取消服务端 120 分钟过滤并在 ScatterChart 统一封顶
- frontend: 新增管理端导航预取工具及 SidebarNav/MainLayout 触发,admin 读接口统一走 cachedRequest 的短期缓存
- dashboard: request detail 支持短 TTL 缓存并在 UsageRecordsTable mousedown 时预取
- data: migrate 测试在 wait_for_postgres 失败时清理子进程,避免遗留
This commit is contained in:
fawney19
2026-04-19 15:17:25 +08:00
parent 97cd877ce5
commit 41b51f10a9
30 changed files with 619 additions and 243 deletions

View File

@@ -1,4 +1,5 @@
import apiClient from './client'
import { buildCacheKey, cachedRequest } from '@/utils/cache'
import type { RefundRequest, WalletSummary, WalletTransaction } from './wallet'
export interface AdminWallet extends WalletSummary {
@@ -100,41 +101,51 @@ export const adminWalletApi = {
async listAllWallets(params?: {
status?: string
owner_type?: 'user' | 'api_key'
}): Promise<AdminWallet[]> {
const items: AdminWallet[] = []
const limit = 200
const maxPages = 200
let offset = 0
let page = 0
}, options: { cacheTtlMs?: number } = {}): Promise<AdminWallet[]> {
const cacheKey = buildCacheKey(
'admin:wallets:list-all',
params as Record<string, unknown> | undefined,
)
return cachedRequest(
cacheKey,
async () => {
const items: AdminWallet[] = []
const limit = 200
const maxPages = 200
let offset = 0
let page = 0
while (page < maxPages) {
const response = await apiClient.get<AdminWalletListResponse>('/api/admin/wallets', {
params: {
...params,
limit,
offset,
},
})
const data = response.data
items.push(...data.items)
while (page < maxPages) {
const response = await apiClient.get<AdminWalletListResponse>('/api/admin/wallets', {
params: {
...params,
limit,
offset,
},
})
const data = response.data
items.push(...data.items)
if (items.length >= data.total || data.items.length < limit) {
break
}
if (items.length >= data.total || data.items.length < limit) {
break
}
const nextOffset = offset + data.items.length
if (nextOffset <= offset) {
throw new Error('分页游标未前进,终止全量钱包拉取以避免死循环')
}
offset = nextOffset
page += 1
}
const nextOffset = offset + data.items.length
if (nextOffset <= offset) {
throw new Error('分页游标未前进,终止全量钱包拉取以避免死循环')
}
offset = nextOffset
page += 1
}
if (page >= maxPages) {
throw new Error(`钱包列表分页超过最大页数 ${maxPages},已中止请求`)
}
if (page >= maxPages) {
throw new Error(`钱包列表分页超过最大页数 ${maxPages},已中止请求`)
}
return items
return items
},
options.cacheTtlMs ?? 0,
)
},
async getWalletDetail(walletId: string): Promise<AdminWalletDetailResponse> {

View File

@@ -583,11 +583,22 @@ export const adminApi = {
},
// 获取特定系统配置
async getSystemConfig(key: string): Promise<{ key: string; value: unknown }> {
const response = await apiClient.get<{ key: string; value: unknown }>(
`/api/admin/system/configs/${key}`
async getSystemConfig(
key: string,
options: { cacheTtlMs?: number } = {},
): Promise<{ key: string; value: unknown }> {
const cacheTtlMs = options.cacheTtlMs ?? 0
const cacheKey = buildCacheKey('admin:system:config', { key })
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<{ key: string; value: unknown }>(
`/api/admin/system/configs/${key}`
)
return response.data
},
cacheTtlMs,
)
return response.data
},
// 更新系统配置

View File

@@ -1,6 +1,8 @@
import apiClient from './client'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
const REQUEST_DETAIL_PREFETCH_TTL_MS = 5_000
export interface DashboardStat {
name: string
value: string
@@ -360,11 +362,30 @@ export const dashboardApi = {
// 获取请求详情
// NOTE: This method now calls the new RESTful API at /api/admin/usage/{id}
async getRequestDetail(requestId: string, options: { includeBodies?: boolean } = {}): Promise<RequestDetail> {
const response = await apiClient.get<RequestDetail>(`/api/admin/usage/${requestId}`, {
params: { include_bodies: options.includeBodies ?? true },
async getRequestDetail(
requestId: string,
options: { includeBodies?: boolean, cacheTtlMs?: number } = {}
): Promise<RequestDetail> {
const includeBodies = options.includeBodies ?? true
const cacheTtlMs = options.cacheTtlMs ?? 0
const cacheKey = buildCacheKey('dashboard:request-detail', { requestId, includeBodies })
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<RequestDetail>(`/api/admin/usage/${requestId}`, {
params: { include_bodies: includeBodies },
})
return response.data
},
cacheTtlMs
)
},
async prefetchRequestDetail(requestId: string): Promise<void> {
await dashboardApi.getRequestDetail(requestId, {
includeBodies: false,
cacheTtlMs: REQUEST_DETAIL_PREFETCH_TTL_MS
})
return response.data
},
// 获取每日统计数据

View File

@@ -1,5 +1,5 @@
import client from '../client'
import { dedupedRequest, buildCacheKey } from '@/utils/cache'
import { buildCacheKey, cachedRequest, dedupedRequest } from '@/utils/cache'
import type {
GlobalModelCreate,
GlobalModelUpdate,
@@ -22,17 +22,26 @@ export type {
/**
* 获取 GlobalModel 列表
*/
interface GlobalModelListOptions {
cacheTtlMs?: number
}
export async function getGlobalModels(params?: {
skip?: number
limit?: number
is_active?: boolean
search?: string
}): Promise<GlobalModelListResponse> {
}, options: GlobalModelListOptions = {}): Promise<GlobalModelListResponse> {
const cacheTtlMs = options.cacheTtlMs ?? 0
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
})
return cachedRequest(
key,
async () => {
const response = await client.get('/api/admin/models/global', { params })
return response.data
},
cacheTtlMs,
)
}
/**

View File

@@ -1,5 +1,5 @@
import client from '../client'
import { dedupedRequest } from '@/utils/cache'
import { buildCacheKey, cachedRequest } from '@/utils/cache'
import type {
AllowedModels,
OAuthOrganizationInfo,
@@ -230,33 +230,59 @@ export interface PoolBatchAction {
payload?: Record<string, unknown> | null
}
export async function getPoolOverview(): Promise<PoolOverviewResponse> {
return dedupedRequest('pool:overview', async () => {
const response = await client.get<PoolOverviewResponse>('/api/admin/pool/overview')
return response.data
})
interface PoolReadOptions {
cacheTtlMs?: number
}
export async function getPoolSchedulingPresets(): Promise<PoolPresetMeta[]> {
return dedupedRequest('pool:scheduling-presets', async () => {
const response = await client.get<PoolPresetMeta[]>('/api/admin/pool/scheduling-presets')
return response.data
})
export async function getPoolOverview(
options: PoolReadOptions = {},
): Promise<PoolOverviewResponse> {
const cacheTtlMs = options.cacheTtlMs ?? 0
return cachedRequest(
'pool:overview',
async () => {
const response = await client.get<PoolOverviewResponse>('/api/admin/pool/overview')
return response.data
},
cacheTtlMs,
)
}
export async function getPoolSchedulingPresets(
options: PoolReadOptions = {},
): Promise<PoolPresetMeta[]> {
const cacheTtlMs = options.cacheTtlMs ?? 0
return cachedRequest(
'pool:scheduling-presets',
async () => {
const response = await client.get<PoolPresetMeta[]>('/api/admin/pool/scheduling-presets')
return response.data
},
cacheTtlMs,
)
}
export async function listPoolKeys(
providerId: string,
params: PoolKeysQuery = {},
options: PoolReadOptions = {},
): Promise<PoolKeysPageResponse> {
const normalizedParams = {
...params,
quick_selectors: params.quick_selectors?.length ? params.quick_selectors.join(',') : undefined,
}
const key = `pool:keys:${providerId}|${normalizedParams.page ?? ''}|${normalizedParams.page_size ?? ''}|${normalizedParams.search ?? ''}|${normalizedParams.status ?? ''}|${normalizedParams.quick_selectors ?? ''}|${normalizedParams.search_scope ?? ''}`
return dedupedRequest(key, async () => {
const response = await client.get<PoolKeysPageResponse>(`/api/admin/pool/${providerId}/keys`, { params: normalizedParams })
return response.data
})
const cacheKey = buildCacheKey(
`pool:keys:${providerId}`,
normalizedParams as Record<string, unknown>,
)
return cachedRequest(
cacheKey,
async () => {
const response = await client.get<PoolKeysPageResponse>(`/api/admin/pool/${providerId}/keys`, { params: normalizedParams })
return response.data
},
options.cacheTtlMs ?? 0,
)
}
export async function resolvePoolKeySelection(

View File

@@ -1,5 +1,5 @@
import client from '../client'
import { dedupedRequest } from '@/utils/cache'
import { buildCacheKey, cachedRequest, dedupedRequest } from '@/utils/cache'
import type {
ClaudeCodeAdvancedConfig,
FailoverRulesConfig,
@@ -13,6 +13,11 @@ interface ProviderRequestOptions {
timeout?: number
}
interface ProviderReadOptions {
timeout?: number
cacheTtlMs?: number
}
/**
* 获取 Providers 摘要(分页)
*/
@@ -43,15 +48,27 @@ function normalizeProviderSummary(
export async function getProvidersSummary(
params: ProviderSummaryQuery = {},
options: ProviderReadOptions = {},
): Promise<ProviderSummaryPageResponse> {
const response = await client.get<ProviderSummaryPageResponse>(
'/api/admin/providers/summary',
{ params },
const cacheTtlMs = options.cacheTtlMs ?? 0
const cacheKey = buildCacheKey('providers:summary', params as Record<string, unknown>)
return cachedRequest(
cacheKey,
async () => {
const response = await client.get<ProviderSummaryPageResponse>(
'/api/admin/providers/summary',
{
params,
timeout: options.timeout,
},
)
return {
...response.data,
items: response.data.items.map(normalizeProviderSummary),
}
},
cacheTtlMs,
)
return {
...response.data,
items: response.data.items.map(normalizeProviderSummary),
}
}
/**

View File

@@ -5,6 +5,8 @@ import { cachedRequest, buildCacheKey } from '@/utils/cache'
import type { BillingSummary } from './auth'
import type { UserSession } from '@/types/session'
const ACTIVITY_HEATMAP_CACHE_TTL_MS = 30 * 60 * 1000
export type { UserSession }
export interface Profile {
@@ -414,7 +416,7 @@ export const meApi = {
/**
* 获取活跃度热力图数据(用户)
* 后端已缓存5分钟
* 历史热力图变化很慢,前端做长缓存,避免短时间重复请求。
*/
async getActivityHeatmap(): Promise<ActivityHeatmap> {
return cachedRequest(
@@ -423,7 +425,7 @@ export const meApi = {
const response = await apiClient.get<ActivityHeatmap>('/api/users/me/usage/heatmap')
return response.data
},
60000
ACTIVITY_HEATMAP_CACHE_TTL_MS
)
}
}

View File

@@ -2,6 +2,8 @@ import apiClient from './client'
import { cachedRequest, dedupedRequest, buildCacheKey } from '@/utils/cache'
import type { ActivityHeatmap } from '@/types/activity'
const ACTIVITY_HEATMAP_CACHE_TTL_MS = 30 * 60 * 1000
export interface UsageRecord {
id: string // UUID
user_id: string // UUID
@@ -340,7 +342,7 @@ export const usageApi = {
/**
* 获取活跃度热力图数据(管理员)
* 后端已缓存5分钟
* 历史热力图变化很慢,前端做长缓存,避免自动刷新链路重复请求。
*/
async getActivityHeatmap(): Promise<ActivityHeatmap> {
return cachedRequest(
@@ -349,7 +351,7 @@ export const usageApi = {
const response = await apiClient.get<ActivityHeatmap | unknown[]>('/api/admin/usage/heatmap')
return normalizeActivityHeatmapResponse(response.data)
},
60000
ACTIVITY_HEATMAP_CACHE_TTL_MS
)
}
}

View File

@@ -1,4 +1,5 @@
import apiClient from './client'
import { cachedRequest } from '@/utils/cache'
import type { UserSession as SessionRecord } from '@/types/session'
export interface User {
@@ -70,9 +71,16 @@ export interface UpsertUserApiKeyRequest {
export type UserSession = SessionRecord
export const usersApi = {
async getAllUsers(): Promise<User[]> {
const response = await apiClient.get<User[]>('/api/admin/users')
return response.data
async getAllUsers(options: { cacheTtlMs?: number } = {}): Promise<User[]> {
const cacheTtlMs = options.cacheTtlMs ?? 0
return cachedRequest(
'admin:users:list',
async () => {
const response = await apiClient.get<User[]>('/api/admin/users')
return response.data
},
cacheTtlMs,
)
},
async getUser(userId: string): Promise<User> {