diff --git a/src/routes/admin/dashboard.js b/src/routes/admin/dashboard.js index fe2cb440..56a4718a 100644 --- a/src/routes/admin/dashboard.js +++ b/src/routes/admin/dashboard.js @@ -704,4 +704,106 @@ router.post('/cleanup', authenticateAdmin, async (req, res) => { } }) +// 📊 获取最近的使用记录 +router.get('/usage-records', authenticateAdmin, async (req, res) => { + try { + const { limit = 100, offset = 0 } = req.query + const limitNum = Math.min(parseInt(limit) || 100, 500) // 最多500条 + const offsetNum = Math.max(parseInt(offset) || 0, 0) + + // 获取所有API Keys + const apiKeys = await apiKeyService.getAllApiKeys() + if (!apiKeys || apiKeys.length === 0) { + return res.json({ success: true, data: { records: [], total: 0 } }) + } + + // 收集所有API Key的使用记录 + const allRecords = [] + for (const key of apiKeys) { + try { + const records = await redis.getUsageRecords(key.id, 100) // 每个key最多取100条 + if (records && records.length > 0) { + // 为每条记录添加API Key信息 + const enrichedRecords = records.map((record) => ({ + ...record, + apiKeyId: key.id, + apiKeyName: key.name || 'Unnamed Key' + })) + allRecords.push(...enrichedRecords) + } + } catch (error) { + logger.error(`Failed to get usage records for key ${key.id}:`, error) + continue + } + } + + // 按时间戳倒序排序(最新的在前) + allRecords.sort((a, b) => { + const timeA = new Date(a.timestamp).getTime() + const timeB = new Date(b.timestamp).getTime() + return timeB - timeA + }) + + // 分页 + const paginatedRecords = allRecords.slice(offsetNum, offsetNum + limitNum) + + // 获取账户名称映射 + const accountIds = [...new Set(paginatedRecords.map((r) => r.accountId).filter(Boolean))] + const accountNameMap = {} + + // 并发获取所有账户名称 + await Promise.all( + accountIds.map(async (accountId) => { + try { + // 尝试从不同类型的账户中获取 + const claudeAcc = await redis.getAccount(accountId) + if (claudeAcc && claudeAcc.name) { + accountNameMap[accountId] = claudeAcc.name + return + } + + const consoleAcc = await redis.getClaudeConsoleAccount(accountId) + if (consoleAcc && consoleAcc.name) { + accountNameMap[accountId] = consoleAcc.name + return + } + + const geminiAcc = await redis.getGeminiAccount(accountId) + if (geminiAcc && geminiAcc.name) { + accountNameMap[accountId] = geminiAcc.name + return + } + + // 其他平台账户... + accountNameMap[accountId] = accountId // 降级显示ID + } catch (error) { + accountNameMap[accountId] = accountId + } + }) + ) + + // 为记录添加账户名称 + const enrichedRecords = paginatedRecords.map((record) => ({ + ...record, + accountName: record.accountId ? accountNameMap[record.accountId] || record.accountId : '-' + })) + + return res.json({ + success: true, + data: { + records: enrichedRecords, + total: allRecords.length, + limit: limitNum, + offset: offsetNum + } + }) + } catch (error) { + logger.error('❌ Failed to get usage records:', error) + return res.status(500).json({ + error: 'Failed to get usage records', + message: error.message + }) + } +}) + module.exports = router diff --git a/web/admin-spa/src/views/DashboardView.vue b/web/admin-spa/src/views/DashboardView.vue index 61ac8124..8666b600 100644 --- a/web/admin-spa/src/views/DashboardView.vue +++ b/web/admin-spa/src/views/DashboardView.vue @@ -673,6 +673,158 @@ + + +
+
+
+

+ 最近使用记录 +

+ +
+ +
+
+

正在加载使用记录...

+
+ +
+

暂无使用记录

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 时间 + + API Key + + 账户 + + 模型 + + 输入 + + 输出 + + 缓存创建 + + 缓存读取 + + 成本 +
+ {{ formatRecordTime(record.timestamp) }} + +
+ {{ record.apiKeyName }} +
+
+
+ {{ record.accountName }} +
+
+
+ {{ record.model }} +
+
+ {{ formatNumber(record.inputTokens) }} + + {{ formatNumber(record.outputTokens) }} + + {{ formatNumber(record.cacheCreateTokens) }} + + {{ formatNumber(record.cacheReadTokens) }} + + ${{ formatCost(record.cost) }} +
+ + +
+ +
+
+
+
@@ -681,12 +833,21 @@ import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue' import { storeToRefs } from 'pinia' import { useDashboardStore } from '@/stores/dashboard' import { useThemeStore } from '@/stores/theme' +import { apiClient } from '@/config/api' +import { showToast } from '@/utils/toast' import Chart from 'chart.js/auto' const dashboardStore = useDashboardStore() const themeStore = useThemeStore() const { isDarkMode } = storeToRefs(themeStore) +// 使用记录相关 +const usageRecords = ref([]) +const usageRecordsLoading = ref(false) +const usageRecordsTotal = ref(0) +const usageRecordsOffset = ref(0) +const usageRecordsLimit = ref(50) + const { dashboardData, costsData, @@ -1477,13 +1638,95 @@ watch(accountUsageTrendData, () => { nextTick(() => createAccountUsageTrendChart()) }) +// 加载使用记录 +async function loadUsageRecords(reset = true) { + if (usageRecordsLoading.value) return + + try { + usageRecordsLoading.value = true + if (reset) { + usageRecordsOffset.value = 0 + usageRecords.value = [] + } + + const response = await apiClient.get('/admin/dashboard/usage-records', { + params: { + limit: usageRecordsLimit.value, + offset: usageRecordsOffset.value + } + }) + + if (response.success && response.data) { + if (reset) { + usageRecords.value = response.data.records || [] + } else { + usageRecords.value = [...usageRecords.value, ...(response.data.records || [])] + } + usageRecordsTotal.value = response.data.total || 0 + } + } catch (error) { + console.error('Failed to load usage records:', error) + showToast('加载使用记录失败', 'error') + } finally { + usageRecordsLoading.value = false + } +} + +// 加载更多使用记录 +async function loadMoreUsageRecords() { + usageRecordsOffset.value += usageRecordsLimit.value + await loadUsageRecords(false) +} + +// 格式化记录时间 +function formatRecordTime(timestamp) { + if (!timestamp) return '-' + const date = new Date(timestamp) + const now = new Date() + const diff = now - date + + // 如果是今天 + if (diff < 86400000 && date.getDate() === now.getDate()) { + return date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + } + + // 如果是昨天 + if (diff < 172800000 && date.getDate() === now.getDate() - 1) { + return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + } + + // 其他日期 + return date.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) +} + +// 格式化数字(添加千分位) +function formatNumber(num) { + if (!num || num === 0) return '0' + return num.toLocaleString('en-US') +} + +// 格式化成本 +function formatCost(cost) { + if (!cost || cost === 0) return '0.000000' + return cost.toFixed(6) +} + // 刷新所有数据 async function refreshAllData() { if (isRefreshing.value) return isRefreshing.value = true try { - await Promise.all([loadDashboardData(), refreshChartsData()]) + await Promise.all([loadDashboardData(), refreshChartsData(), loadUsageRecords()]) } finally { isRefreshing.value = false }