feat(admin): 将用户管理的用量统计改为全量累计口径 (#314)

- 新增按用户 ID 批量汇总 usage 的后端查询
- 在 /api/admin/users 中返回 request_count 和 total_tokens
- 移除用户管理页面额外的 usage 聚合请求
- 为管理端用户列表补充累计统计回归测试
This commit is contained in:
AAEE86
2026-04-19 01:26:22 +08:00
committed by GitHub
parent ab54881eb0
commit 6666992d01
12 changed files with 273 additions and 71 deletions

View File

@@ -15,6 +15,8 @@ export interface User {
created_at: string
updated_at?: string
last_login_at?: string | null
request_count?: number
total_tokens?: number
}
export interface CreateUserRequest {

View File

@@ -26,7 +26,7 @@
</Button>
<!-- 刷新按钮 -->
<RefreshButton
:loading="usersStore.loading || loadingStats"
:loading="usersStore.loading"
@click="refreshUsers"
/>
</div>
@@ -161,7 +161,7 @@
<!-- 刷新按钮 -->
<RefreshButton
:loading="usersStore.loading || loadingStats"
:loading="usersStore.loading"
@click="refreshUsers"
/>
</div>
@@ -259,23 +259,13 @@
</TableCell>
<TableCell class="py-4">
<div class="space-y-1 text-xs">
<template v-if="userStats[user.id]">
<div class="flex items-center text-muted-foreground">
<span class="w-14">请求:</span>
<span class="font-medium text-foreground">{{ formatNumber(userStats[user.id]?.request_count) }}</span>
</div>
<div class="flex items-center text-muted-foreground">
<span class="w-14">Tokens:</span>
<span class="font-medium text-foreground">{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}</span>
</div>
</template>
<div
v-else
class="flex items-center text-muted-foreground"
>
<span class="w-14">统计:</span>
<span v-if="loadingStats">加载中...</span>
<span v-else>无数据</span>
<div class="flex items-center text-muted-foreground">
<span class="w-14">请求:</span>
<span class="font-medium text-foreground">{{ formatNumber(user.request_count) }}</span>
</div>
<div class="flex items-center text-muted-foreground">
<span class="w-14">Tokens:</span>
<span class="font-medium text-foreground">{{ formatTokens(user.total_tokens ?? 0) }}</span>
</div>
<div class="flex items-center text-muted-foreground">
<span class="w-14">限速:</span>
@@ -507,7 +497,7 @@
请求次数
</div>
<div class="font-semibold text-foreground">
{{ formatNumber(userStats[user.id]?.request_count) }}
{{ formatNumber(user.request_count) }}
</div>
</div>
<div class="rounded-lg border border-border/50 bg-background/70 p-2.5">
@@ -515,7 +505,7 @@
Tokens
</div>
<div class="font-semibold text-foreground">
{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}
{{ formatTokens(user.total_tokens ?? 0) }}
</div>
</div>
</div>
@@ -1044,7 +1034,6 @@ import { adminWalletApi, type AdminWallet } from '@/api/admin-wallets'
import { useToast } from '@/composables/useToast'
import { useConfirm } from '@/composables/useConfirm'
import { useClipboard } from '@/composables/useClipboard'
import { usageApi, type UsageByUser } from '@/api/usage'
import { adminApi } from '@/api/admin'
import { walletStatusBadge, walletStatusLabel } from '@/utils/walletDisplay'
@@ -1128,9 +1117,6 @@ const userApiKeyForm = ref({
})
// 用户统计
const userStats = ref<Record<string, UsageByUser>>({})
const loadingStats = ref(false)
let userStatsRequestId = 0
const userWalletMap = ref<Record<string, AdminWallet>>({})
const showWalletActionDialogState = ref(false)
@@ -1194,7 +1180,6 @@ onMounted(async () => {
async function refreshUsers() {
await Promise.all([
usersStore.fetchUsers(),
loadUserStats(),
loadUserWallets()
])
}
@@ -1203,25 +1188,6 @@ function formatDate(dateString: string) {
return new Date(dateString).toLocaleDateString('zh-CN')
}
async function loadUserStats() {
const requestId = ++userStatsRequestId
loadingStats.value = true
try {
const data = await usageApi.getUsageByUser()
if (requestId !== userStatsRequestId) return
userStats.value = data.reduce((acc: Record<string, UsageByUser>, stat: UsageByUser) => {
acc[stat.user_id] = stat
return acc
}, {})
} catch (err) {
log.error('加载用户统计失败:', err)
} finally {
if (requestId === userStatsRequestId) {
loadingStats.value = false
}
}
}
async function loadUserWallets() {
try {
const wallets = await adminWalletApi.listAllWallets()