feat(usage): 管理员用量统计支持筛选刷新与失败保留旧数据

- 用量统计/聚合接口新增 skipCache 选项与 120s 超时,便于强制绕过缓存
- loadStats 增加 force/preserveOnFailure 选项,背景刷新失败时保留旧数据
- 管理员页面在用户/模型/Provider 筛选变化时强制刷新统计,并将筛选条件传入统计接口
- 手动刷新与自动刷新分离,自动刷新不再重载长期热力图相关聚合
This commit is contained in:
fawney19
2026-05-20 16:51:52 +08:00
parent 70f2882a43
commit 2881ff097a
5 changed files with 180 additions and 36 deletions
@@ -219,4 +219,51 @@ describe('useUsageData', () => {
expect(providerStats.value.map(item => item.provider)).toEqual(['OpenAI'])
expect(availableProviders.value).toEqual(['OpenAI'])
})
it('keeps previous admin provider stats when a background refresh fails', async () => {
const isAdminPage = ref(true)
const { loadStats, providerStats, availableProviders } = useUsageData({ isAdminPage })
const dateRange = { preset: 'last7days', tz_offset_minutes: 0 }
getUsageStatsMock.mockResolvedValueOnce({
total_requests: 3,
total_tokens: 300,
total_cost: 1,
avg_response_time: 0,
})
getUsageByProviderMock.mockResolvedValueOnce([
{
provider: 'OpenAI',
request_count: 3,
total_tokens: 300,
total_cost: 1.23,
actual_cost: 1.5,
avg_response_time_ms: 1250,
success_rate: 100,
},
])
await loadStats(dateRange)
getUsageStatsMock.mockResolvedValueOnce({
total_requests: 4,
total_tokens: 400,
total_cost: 2,
avg_response_time: 0,
})
getUsageByProviderMock.mockRejectedValueOnce({
response: { status: 500 },
message: 'provider aggregation failed',
})
const hadFailure = await loadStats(dateRange, { preserveOnFailure: true })
expect(hadFailure).toBe(true)
expect(providerStats.value).toHaveLength(1)
expect(providerStats.value[0]).toMatchObject({
provider: 'OpenAI',
requests: 3,
})
expect(availableProviders.value).toEqual(['OpenAI'])
})
})
@@ -18,6 +18,11 @@ export interface UseUsageDataOptions {
isAdminPage: Ref<boolean>
}
export interface LoadStatsOptions {
force?: boolean
preserveOnFailure?: boolean
}
export interface PaginationParams {
page: number
pageSize: number
@@ -76,7 +81,7 @@ export function useUsageData(options: UseUsageDataOptions) {
})
// 加载统计数据(不加载记录)
async function loadStats(dateRange?: DateRangeParams): Promise<boolean> {
async function loadStats(dateRange?: DateRangeParams, options: LoadStatsOptions = {}): Promise<boolean> {
const requestId = ++loadStatsRequestId
isLoadingStats.value = true
currentDateRange.value = dateRange
@@ -84,14 +89,17 @@ export function useUsageData(options: UseUsageDataOptions) {
try {
if (isAdminPage.value) {
// 管理员页面顺序加载统计数据,避免刷新使用记录时瞬时打满后端 worker。
stats.value = createDefaultStats()
modelStats.value = []
providerStats.value = []
apiFormatStats.value = []
availableModels.value = []
availableProviders.value = []
if (!options.preserveOnFailure) {
stats.value = createDefaultStats()
modelStats.value = []
providerStats.value = []
apiFormatStats.value = []
availableModels.value = []
availableProviders.value = []
}
let hadFailure = false
const requestOptions = options.force ? { skipCache: true } : undefined
const markFailure = (error: unknown) => {
hadFailure = true
if (getErrorStatus(error) !== 403) {
@@ -100,7 +108,7 @@ export function useUsageData(options: UseUsageDataOptions) {
}
try {
const statsData = await usageApi.getUsageStats(dateRange)
const statsData = await usageApi.getUsageStats(dateRange, requestOptions)
if (requestId !== loadStatsRequestId) {
return true
}
@@ -127,7 +135,7 @@ export function useUsageData(options: UseUsageDataOptions) {
}
try {
const modelData = await usageApi.getUsageByModel(dateRange)
const modelData = await usageApi.getUsageByModel(dateRange, requestOptions)
if (requestId !== loadStatsRequestId) {
return true
}
@@ -158,7 +166,7 @@ export function useUsageData(options: UseUsageDataOptions) {
}
try {
const providerData = await usageApi.getUsageByProvider(dateRange)
const providerData = await usageApi.getUsageByProvider(dateRange, requestOptions)
if (requestId !== loadStatsRequestId) {
return true
}
@@ -191,7 +199,7 @@ export function useUsageData(options: UseUsageDataOptions) {
}
try {
const apiFormatData = await usageApi.getUsageByApiFormat(dateRange)
const apiFormatData = await usageApi.getUsageByApiFormat(dateRange, requestOptions)
if (requestId !== loadStatsRequestId) {
return true
}
@@ -312,9 +320,9 @@ export function useUsageData(options: UseUsageDataOptions) {
if (getErrorStatus(error) !== 403) {
log.error('加载统计数据失败:', error)
}
stats.value = createDefaultStats()
modelStats.value = []
if (!isAdminPage.value) {
stats.value = createDefaultStats()
modelStats.value = []
// 用户页的 records 依赖 stats 一起加载;管理员页的 records 是独立分页,不应被统计失败清空。
currentRecords.value = []
totalRecords.value = 0