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
+9 -4
View File
@@ -717,6 +717,7 @@ let modelSelectionRequestId = 0
let modelProvidersRequestId = 0
let batchManageModelsRequestId = 0
let providerOptionsRequest: Promise<void> | null = null
const GLOBAL_MODELS_LIST_CACHE_TTL_MS = 10 * 1000
// 模型目录分页
const catalogCurrentPage = ref(1)
@@ -1036,11 +1037,13 @@ const globalModelsQueryParams = computed(() => ({
let modelSearchDebounceTimer: ReturnType<typeof setTimeout> | null = null
async function loadGlobalModels() {
async function loadGlobalModels(options: { cacheTtlMs?: number } = {}) {
const requestId = ++globalModelsRequestId
loading.value = true
try {
const response = await listGlobalModels(globalModelsQueryParams.value)
const response = await listGlobalModels(globalModelsQueryParams.value, {
cacheTtlMs: options.cacheTtlMs ?? 0,
})
if (requestId !== globalModelsRequestId) return
const pageModels = response.models || []
@@ -1533,9 +1536,11 @@ watch(globalModelsQueryParams, (newParams, oldParams) => {
&& newParams.skip === oldParams?.skip
&& newParams.limit === oldParams?.limit
if (isSearchOnly) {
modelSearchDebounceTimer = setTimeout(loadGlobalModels, 300)
modelSearchDebounceTimer = setTimeout(() => {
void loadGlobalModels({ cacheTtlMs: GLOBAL_MODELS_LIST_CACHE_TTL_MS })
}, 300)
} else {
loadGlobalModels()
void loadGlobalModels({ cacheTtlMs: GLOBAL_MODELS_LIST_CACHE_TTL_MS })
}
}, { immediate: true })
+21 -12
View File
@@ -1258,12 +1258,15 @@ let keysRequestId = 0
let keysSearchDebounceTimer: number | null = null
let suppressFiltersWatch = false
let hasHydratedInitialProviderSelection = false
const POOL_OVERVIEW_CACHE_TTL_MS = 10 * 1000
const POOL_KEYS_CACHE_TTL_MS = 10 * 1000
const POOL_SCHEDULING_PRESETS_CACHE_TTL_MS = 5 * 60 * 1000
async function loadOverview() {
async function loadOverview(options: { cacheTtlMs?: number } = {}) {
const requestId = ++overviewRequestId
overviewLoading.value = true
try {
const res = await getPoolOverview()
const res = await getPoolOverview({ cacheTtlMs: options.cacheTtlMs ?? 0 })
if (requestId !== overviewRequestId) return
const allProviders = Array.isArray(res.items) ? res.items : []
const enabledProviders = allProviders.filter(item => item.pool_enabled)
@@ -1283,6 +1286,7 @@ async function loadOverview() {
preserveSearch: true,
preserveStatus: true,
preservePagination: true,
cacheTtlMs: options.cacheTtlMs ? POOL_KEYS_CACHE_TTL_MS : 0,
})
}
return
@@ -1296,6 +1300,7 @@ async function loadOverview() {
preserveSearch: shouldPreserveViewState,
preserveStatus: shouldPreserveViewState,
preservePagination: shouldPreserveViewState,
cacheTtlMs: options.cacheTtlMs ? POOL_KEYS_CACHE_TTL_MS : 0,
})
} else {
selectedProviderId.value = null
@@ -1333,7 +1338,7 @@ const selectedProviderIdProxy = computed({
get: () => selectedProviderId.value ?? '',
set: (val: string) => {
if (val && val !== selectedProviderId.value) {
selectProvider(val)
void selectProvider(val, { cacheTtlMs: POOL_KEYS_CACHE_TTL_MS })
}
},
})
@@ -1363,9 +1368,9 @@ function normalizePresetName(value: unknown): string {
return String(value ?? '').trim().toLowerCase()
}
async function loadSchedulingPresetMetas(): Promise<void> {
async function loadSchedulingPresetMetas(options: { cacheTtlMs?: number } = {}): Promise<void> {
try {
const metas = await getPoolSchedulingPresets()
const metas = await getPoolSchedulingPresets({ cacheTtlMs: options.cacheTtlMs ?? 0 })
const next: Record<string, string> = {}
for (const meta of metas as PoolPresetMeta[]) {
const name = normalizePresetName(meta.name)
@@ -1492,6 +1497,7 @@ async function selectProvider(
preserveSearch?: boolean
preserveStatus?: boolean
preservePagination?: boolean
cacheTtlMs?: number
} = {},
) {
const requestId = ++selectProviderRequestId
@@ -1522,7 +1528,7 @@ async function selectProvider(
keysSearchDebounceTimer = null
}
resetKeyPage(currentPage.value, pageSize.value)
const keysTask = loadKeys()
const keysTask = loadKeys({ cacheTtlMs: options.cacheTtlMs ?? 0 })
// Provider summary is non-blocking for key list rendering.
void loadProviderData(id)
await keysTask
@@ -1620,6 +1626,7 @@ watch(
preserveSearch: true,
preserveStatus: true,
preservePagination: true,
cacheTtlMs: POOL_KEYS_CACHE_TTL_MS,
})
},
{ immediate: true },
@@ -1790,7 +1797,7 @@ async function refreshCurrentPage() {
}
}
async function loadKeys() {
async function loadKeys(options: { cacheTtlMs?: number } = {}) {
if (!selectedProviderId.value) return
const requestId = ++keysRequestId
const providerId = selectedProviderId.value
@@ -1805,6 +1812,8 @@ async function loadKeys() {
page_size: pageSizeValue,
search,
status,
}, {
cacheTtlMs: options.cacheTtlMs ?? 0,
})
if (requestId !== keysRequestId || selectedProviderId.value !== providerId) return
const resolvedPage = resolvePoolManagementPageAfterLoad({
@@ -1829,13 +1838,13 @@ async function loadKeys() {
}
watch([currentPage, pageSize], () => {
void loadKeys()
void loadKeys({ cacheTtlMs: POOL_KEYS_CACHE_TTL_MS })
})
watch(statusFilter, () => {
if (suppressFiltersWatch) return
currentPage.value = 1
void loadKeys()
void loadKeys({ cacheTtlMs: POOL_KEYS_CACHE_TTL_MS })
})
watch(searchQuery, () => {
@@ -1846,7 +1855,7 @@ watch(searchQuery, () => {
}
keysSearchDebounceTimer = window.setTimeout(() => {
keysSearchDebounceTimer = null
void loadKeys()
void loadKeys({ cacheTtlMs: POOL_KEYS_CACHE_TTL_MS })
}, 300)
})
@@ -3035,8 +3044,8 @@ function formatRelativeTime(isoStr: string): string {
// --- Init ---
onMounted(() => {
startCountdownTimer()
void loadSchedulingPresetMetas()
void loadOverview()
void loadSchedulingPresetMetas({ cacheTtlMs: POOL_SCHEDULING_PRESETS_CACHE_TTL_MS })
void loadOverview({ cacheTtlMs: POOL_OVERVIEW_CACHE_TTL_MS })
})
onBeforeUnmount(() => {
+27 -15
View File
@@ -324,6 +324,9 @@ let deletePollAbort: AbortController | null = null
const DELETE_POLL_INTERVAL_MS = 2000
const DELETE_POLL_MAX_MS = 30 * 60 * 1000
const DELETE_POLL_MAX_FAILURES = 3
const PROVIDER_SUMMARY_CACHE_TTL_MS = 10 * 1000
const PROVIDER_PRIORITY_MODE_CACHE_TTL_MS = 30 * 1000
const PROVIDER_MODEL_FILTER_CACHE_TTL_MS = 10 * 1000
async function pollProviderDeleteTask(providerId: string, taskId: string) {
deletePollAbort?.abort()
@@ -533,9 +536,11 @@ const maxProviderPriority = computed(() => {
})
// 加载优先级模式
async function loadPriorityMode() {
async function loadPriorityMode(options: { cacheTtlMs?: number } = {}) {
try {
const response = await adminApi.getSystemConfig('provider_priority_mode')
const response = await adminApi.getSystemConfig('provider_priority_mode', {
cacheTtlMs: options.cacheTtlMs ?? 0,
})
if (response.value) {
priorityMode.value = response.value as 'provider' | 'global_key'
}
@@ -545,9 +550,12 @@ async function loadPriorityMode() {
}
// 加载全局模型列表(用于模型筛选下拉)
async function loadGlobalModelList() {
async function loadGlobalModelList(options: { cacheTtlMs?: number } = {}) {
try {
const response = await getGlobalModels({ is_active: true, limit: 1000 })
const response = await getGlobalModels(
{ is_active: true, limit: 1000 },
{ cacheTtlMs: options.cacheTtlMs ?? 0 },
)
globalModels.value = response.models.map(m => ({ id: m.id, name: m.name }))
} catch {
globalModels.value = []
@@ -555,11 +563,13 @@ async function loadGlobalModelList() {
}
// 加载提供商列表(服务端分页)
async function loadProviders() {
async function loadProviders(options: { cacheTtlMs?: number } = {}) {
const requestId = ++providersRequestId
loading.value = true
try {
const response = await getProvidersSummary(queryParams.value)
const response = await getProvidersSummary(queryParams.value, {
cacheTtlMs: options.cacheTtlMs ?? 0,
})
if (requestId !== providersRequestId) return
providers.value = response.items
total.value = response.total
@@ -587,9 +597,11 @@ watch(queryParams, (newParams, oldParams) => {
newParams.api_format === oldParams?.api_format &&
newParams.model_id === oldParams?.model_id
if (isSearchOnly) {
debounceTimer = setTimeout(loadProviders, 300)
debounceTimer = setTimeout(() => {
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
}, 300)
} else {
loadProviders()
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
}
}, { deep: true })
@@ -658,7 +670,7 @@ function openOpsConfigDialog(provider: ProviderWithEndpointsSummary) {
// 扩展操作配置保存回调
function handleOpsConfigSaved() {
opsConfigDialogOpen.value = false
loadProviders()
void loadProviders()
}
// 处理提供商编辑完成
@@ -680,7 +692,7 @@ async function handlePrioritySaved() {
// 处理提供商添加
function handleProviderAdded() {
loadProviders()
void loadProviders()
}
// 删除提供商
@@ -716,7 +728,7 @@ async function handleDeleteProvider(provider: ProviderWithEndpointsSummary) {
showSuccess('提供商已删除')
providerDeleteProgress.value = null
loadProviders()
void loadProviders()
} catch (err: unknown) {
providerDeleteProgress.value = null
showError(parseApiError(err, '删除提供商失败'), '错误')
@@ -753,10 +765,10 @@ function handleGlobalClick(event: MouseEvent) {
}
onMounted(() => {
loadProviders()
loadPriorityMode()
loadGlobalModelList()
loadArchitectureSchemas()
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
void loadPriorityMode({ cacheTtlMs: PROVIDER_PRIORITY_MODE_CACHE_TTL_MS })
void loadGlobalModelList({ cacheTtlMs: PROVIDER_MODEL_FILTER_CACHE_TTL_MS })
void loadArchitectureSchemas()
document.addEventListener('click', handleGlobalClick, true)
// 每秒更新一次倒计时
startTick()
+20 -10
View File
@@ -1128,6 +1128,9 @@ const filterStatus = ref('all')
const currentPage = ref(1)
const pageSize = ref(20)
const USERS_PAGE_CACHE_TTL_MS = 10 * 1000
const USER_WALLETS_CACHE_TTL_MS = 10 * 1000
let userWalletsRequestId = 0
const filteredUsers = computed(() => {
let filtered = [...usersStore.users]
@@ -1173,31 +1176,38 @@ watch([searchQuery, filterRole, filterStatus], () => {
currentPage.value = 1
})
onMounted(async () => {
await refreshUsers()
onMounted(() => {
void refreshUsers({ preferCache: true })
})
async function refreshUsers() {
await Promise.all([
usersStore.fetchUsers(),
loadUserWallets()
])
async function refreshUsers(options: { preferCache?: boolean } = {}) {
const cacheTtlMs = options.preferCache ? USERS_PAGE_CACHE_TTL_MS : 0
await usersStore.fetchUsers({ cacheTtlMs })
void loadUserWallets({
cacheTtlMs: options.preferCache ? USER_WALLETS_CACHE_TTL_MS : 0,
})
}
function formatDate(dateString: string) {
return new Date(dateString).toLocaleDateString('zh-CN')
}
async function loadUserWallets() {
async function loadUserWallets(options: { cacheTtlMs?: number } = {}) {
const requestId = ++userWalletsRequestId
try {
const wallets = await adminWalletApi.listAllWallets()
const wallets = await adminWalletApi.listAllWallets(
{ owner_type: 'user' },
{ cacheTtlMs: options.cacheTtlMs ?? 0 },
)
if (requestId !== userWalletsRequestId) return
userWalletMap.value = wallets
.filter((wallet) => wallet.owner_type === 'user' && !!wallet.user_id)
.filter((wallet) => !!wallet.user_id)
.reduce<Record<string, AdminWallet>>((acc, wallet) => {
acc[wallet.user_id as string] = wallet
return acc
}, {})
} catch (err) {
if (requestId !== userWalletsRequestId) return
log.error('加载用户钱包失败:', err)
}
}