mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
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:
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
// 更新系统配置
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
// 获取每日统计数据
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user