DaydreamCoding bf24b61139 perf(usage): 优化 /admin/usage 打开速度与刷新响应
根因:页面 mount 并发 6 个请求,其中 5 个在原始 usage_logs 上 live 聚合,
且有 1 个重复 getModelStats。优化(不引入预聚合):

前端
- UsageView 经 :model-options 下传 model 列表,移除 UsageFilters 重复的
  getModelStats(mount 请求 6→5,少一次 usage_logs 全表 GROUP BY model)
- 刷新/换筛选保留旧模型数据(invalidateModelStatsCache 只失效标记不清空数据),
  图表不再闪空,刷新期间页面保持可交互

后端
- GetStatsWithFilters 4 条聚合 errgroup 并行(仅 *sql.DB 连接池路径,ent.Tx
  顺序回退以保事务内不并发),endpoint 明细 best-effort;抑制取消级联噪声日志
- /admin/usage/stats 复用 dashboard 的 newSnapshotCache 30s 处理器层缓存,按
  filters+窗口为 key;前端手动刷新带 nocache=1 强制回源(刷新=最新)

注:自合并提交 4c8396c 迁移而来,仅取"列表打开速度与刷新响应"部分;原提交的
"审计查看弹窗大 body 渲染"改动(AuditLogModal / audit-log-format / 审计 i18n)
依赖尚未迁移的审计功能(d8389ade),本次已排除。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:56:08 +08:00

212 lines
5.3 KiB
TypeScript

/**
* Admin Usage API endpoints
* Handles admin-level usage logs and statistics retrieval
*/
import { apiClient } from '../client'
import type { AdminUsageLog, UsageQueryParams, PaginatedResponse, UsageRequestType } from '@/types'
import type { EndpointStat } from '@/types'
// ==================== Types ====================
export interface AdminUsageStatsResponse {
total_requests: number
total_input_tokens: number
total_output_tokens: number
total_cache_tokens: number
total_tokens: number
total_cost: number
total_actual_cost: number
total_account_cost: number
average_duration_ms: number
endpoints?: EndpointStat[]
upstream_endpoints?: EndpointStat[]
endpoint_paths?: EndpointStat[]
}
export interface SimpleUser {
id: number
email: string
deleted: boolean
}
export interface SimpleApiKey {
id: number
name: string
user_id: number
}
export interface UsageCleanupFilters {
start_time: string
end_time: string
user_id?: number
api_key_id?: number
account_id?: number
group_id?: number
model?: string | null
request_type?: UsageRequestType | null
stream?: boolean | null
billing_type?: number | null
}
export interface UsageCleanupTask {
id: number
status: string
filters: UsageCleanupFilters
created_by: number
deleted_rows: number
error_message?: string | null
canceled_by?: number | null
canceled_at?: string | null
started_at?: string | null
finished_at?: string | null
created_at: string
updated_at: string
}
export interface CreateUsageCleanupTaskRequest {
start_date: string
end_date: string
user_id?: number
api_key_id?: number
account_id?: number
group_id?: number
model?: string | null
request_type?: UsageRequestType | null
stream?: boolean | null
billing_type?: number | null
timezone?: string
}
export interface AdminUsageQueryParams extends UsageQueryParams {
user_id?: number
exact_total?: boolean
billing_mode?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
}
// ==================== API Functions ====================
/**
* List all usage logs with optional filters (admin only)
* @param params - Query parameters for filtering and pagination
* @returns Paginated list of usage logs
*/
export async function list(
params: AdminUsageQueryParams,
options?: { signal?: AbortSignal }
): Promise<PaginatedResponse<AdminUsageLog>> {
const { data } = await apiClient.get<PaginatedResponse<AdminUsageLog>>('/admin/usage', {
params,
signal: options?.signal
})
return data
}
/**
* Get usage statistics with optional filters (admin only)
* @param params - Query parameters for filtering
* @returns Usage statistics
*/
export async function getStats(params: {
user_id?: number
api_key_id?: number
account_id?: number
group_id?: number
model?: string
request_type?: UsageRequestType
stream?: boolean
period?: string
start_date?: string
end_date?: string
timezone?: string
nocache?: number
}): Promise<AdminUsageStatsResponse> {
const { data } = await apiClient.get<AdminUsageStatsResponse>('/admin/usage/stats', {
params
})
return data
}
/**
* Search users by email keyword (admin only)
* @param keyword - Email keyword to search
* @returns List of matching users (max 30)
*/
export async function searchUsers(keyword: string): Promise<SimpleUser[]> {
const { data } = await apiClient.get<SimpleUser[]>('/admin/usage/search-users', {
params: { q: keyword }
})
return data
}
/**
* Search API keys by user ID and/or keyword (admin only)
* @param userId - Optional user ID to filter by
* @param keyword - Optional keyword to search in key name
* @returns List of matching API keys (max 30)
*/
export async function searchApiKeys(userId?: number, keyword?: string): Promise<SimpleApiKey[]> {
const params: Record<string, unknown> = {}
if (userId !== undefined) {
params.user_id = userId
}
if (keyword) {
params.q = keyword
}
const { data } = await apiClient.get<SimpleApiKey[]>('/admin/usage/search-api-keys', {
params
})
return data
}
/**
* List usage cleanup tasks (admin only)
* @param params - Query parameters for pagination
* @returns Paginated list of cleanup tasks
*/
export async function listCleanupTasks(
params: { page?: number; page_size?: number },
options?: { signal?: AbortSignal }
): Promise<PaginatedResponse<UsageCleanupTask>> {
const { data } = await apiClient.get<PaginatedResponse<UsageCleanupTask>>('/admin/usage/cleanup-tasks', {
params,
signal: options?.signal
})
return data
}
/**
* Create a usage cleanup task (admin only)
* @param payload - Cleanup task parameters
* @returns Created cleanup task
*/
export async function createCleanupTask(payload: CreateUsageCleanupTaskRequest): Promise<UsageCleanupTask> {
const { data } = await apiClient.post<UsageCleanupTask>('/admin/usage/cleanup-tasks', payload)
return data
}
/**
* Cancel a usage cleanup task (admin only)
* @param taskId - Task ID to cancel
*/
export async function cancelCleanupTask(taskId: number): Promise<{ id: number; status: string }> {
const { data } = await apiClient.post<{ id: number; status: string }>(
`/admin/usage/cleanup-tasks/${taskId}/cancel`
)
return data
}
export const adminUsageAPI = {
list,
getStats,
searchUsers,
searchApiKeys,
listCleanupTasks,
createCleanupTask,
cancelCleanupTask
}
export default adminUsageAPI