fix: correct API key cost calculation and UI display issues

- Fix admin panel cost display for "all time" period using permanent Redis key
- Fix user statistics total cost limit to show complete history
- Fix restricted models list overflow with scrollable container

Backend changes:
- src/routes/admin/apiKeys.js: Use allTimeCost for timeRange='all' instead of scanning TTL keys
- src/routes/apiStats.js: Prioritize permanent usage:cost:total key over monthly keys

Frontend changes:
- web/admin-spa/src/components/apistats/LimitConfig.vue: Add overflow-visible and scrolling to model list

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John Doe
2025-12-12 18:11:02 +03:00
parent 87426133a2
commit baafebbf7b
3 changed files with 87 additions and 52 deletions

View File

@@ -945,6 +945,30 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) {
allTimeCost = parseFloat((await client.get(totalCostKey)) || '0') allTimeCost = parseFloat((await client.get(totalCostKey)) || '0')
} }
// 🔧 FIX: 对于 "全部时间" 时间范围,直接使用 allTimeCost
// 因为 usage:*:model:daily:* 键有 30 天 TTL旧数据已经过期
if (timeRange === 'all' && allTimeCost > 0) {
logger.debug(`📊 使用 allTimeCost 计算 timeRange='all': ${allTimeCost}`)
return {
requests: 0, // 旧数据详情不可用
tokens: 0,
inputTokens: 0,
outputTokens: 0,
cacheCreateTokens: 0,
cacheReadTokens: 0,
cost: allTimeCost,
formattedCost: CostCalculator.formatCost(allTimeCost),
// 实时限制数据(始终返回,不受时间范围影响)
dailyCost,
currentWindowCost,
windowRemainingSeconds,
windowStartTime,
windowEndTime,
allTimeCost
}
}
// 只在启用了窗口限制时查询窗口数据 // 只在启用了窗口限制时查询窗口数据
if (rateLimitWindow > 0) { if (rateLimitWindow > 0) {
const costCountKey = `rate_limit:cost:${keyId}` const costCountKey = `rate_limit:cost:${keyId}`

View File

@@ -206,14 +206,24 @@ router.post('/api/user-stats', async (req, res) => {
// 获取验证结果中的完整keyData包含isActive状态和cost信息 // 获取验证结果中的完整keyData包含isActive状态和cost信息
const fullKeyData = keyData const fullKeyData = keyData
// 计算总费用 - 使用与模型统计相同的逻辑(按模型分别计算) // 🔧 FIX: 使用 allTimeCost 而不是扫描月度键
// 计算总费用 - 优先使用持久化的总费用计数器
let totalCost = 0 let totalCost = 0
let formattedCost = '$0.000000' let formattedCost = '$0.000000'
try { try {
const client = redis.getClientSafe() const client = redis.getClientSafe()
// 获取所有月度模型统计与model-stats接口相同的逻辑 // 读取累积的总费用(没有 TTL 的持久键
const totalCostKey = `usage:cost:total:${keyId}`
const allTimeCost = parseFloat((await client.get(totalCostKey)) || '0')
if (allTimeCost > 0) {
totalCost = allTimeCost
formattedCost = CostCalculator.formatCost(allTimeCost)
logger.debug(`📊 使用 allTimeCost 计算用户统计: ${allTimeCost}`)
} else {
// Fallback: 如果 allTimeCost 为空(旧键),尝试月度键
const allModelKeys = await client.keys(`usage:${keyId}:model:monthly:*:*`) const allModelKeys = await client.keys(`usage:${keyId}:model:monthly:*:*`)
const modelUsageMap = new Map() const modelUsageMap = new Map()
@@ -272,8 +282,9 @@ router.post('/api/user-stats', async (req, res) => {
} }
formattedCost = CostCalculator.formatCost(totalCost) formattedCost = CostCalculator.formatCost(totalCost)
}
} catch (error) { } catch (error) {
logger.warn(`Failed to calculate detailed cost for key ${keyId}:`, error) logger.warn(`Failed to calculate cost for key ${keyId}:`, error)
// 回退到简单计算 // 回退到简单计算
if (fullKeyData.usage?.total?.allTokens > 0) { if (fullKeyData.usage?.total?.allTokens > 0) {
const usage = fullKeyData.usage.total const usage = fullKeyData.usage.total

View File

@@ -284,7 +284,7 @@
</div> </div>
<!-- 详细限制信息 --> <!-- 详细限制信息 -->
<div v-if="hasModelRestrictions" class="card p-4 md:p-6"> <div v-if="hasModelRestrictions" class="card !overflow-visible p-4 md:p-6">
<h3 <h3
class="mb-3 flex items-center text-lg font-bold text-gray-900 dark:text-gray-100 md:mb-4 md:text-xl" class="mb-3 flex items-center text-lg font-bold text-gray-900 dark:text-gray-100 md:mb-4 md:text-xl"
> >
@@ -301,7 +301,7 @@
<i class="fas fa-robot mr-1 text-xs md:mr-2 md:text-sm" /> <i class="fas fa-robot mr-1 text-xs md:mr-2 md:text-sm" />
受限模型列表 受限模型列表
</h4> </h4>
<div class="space-y-1 md:space-y-2"> <div class="max-h-64 space-y-1 overflow-y-auto pr-1 md:max-h-80 md:space-y-2">
<div <div
v-for="model in statsData.restrictions.restrictedModels" v-for="model in statsData.restrictions.restrictedModels"
:key="model" :key="model"