#!/usr/bin/env node /** * 数据迁移脚本:修复历史使用统计数据 * * 功能: * 1. 统一 totalTokens 和 allTokens 字段 * 2. 确保 allTokens 包含所有类型的 tokens * 3. 修复历史数据的不一致性 * * 使用方法: * node scripts/fix-usage-stats.js [--dry-run] */ require('dotenv').config() const redis = require('../src/models/redis') const logger = require('../src/utils/logger') // 解析命令行参数 const args = process.argv.slice(2) const isDryRun = args.includes('--dry-run') async function fixUsageStats() { try { logger.info('🔧 开始修复使用统计数据...') if (isDryRun) { logger.info('📝 DRY RUN 模式 - 不会实际修改数据') } // 连接到 Redis await redis.connect() logger.success('✅ 已连接到 Redis') const client = redis.getClientSafe() // 统计信息 const stats = { totalKeys: 0, fixedTotalKeys: 0, fixedDailyKeys: 0, fixedMonthlyKeys: 0, fixedModelKeys: 0, errors: 0 } // 1. 修复 API Key 级别的总统计 logger.info('\n📊 修复 API Key 总统计数据...') const apiKeyPattern = 'apikey:*' const apiKeys = await client.keys(apiKeyPattern) stats.totalKeys = apiKeys.length for (const apiKeyKey of apiKeys) { const keyId = apiKeyKey.replace('apikey:', '') const usageKey = `usage:${keyId}` try { const usageData = await client.hgetall(usageKey) if (usageData && Object.keys(usageData).length > 0) { const inputTokens = parseInt(usageData.totalInputTokens) || 0 const outputTokens = parseInt(usageData.totalOutputTokens) || 0 const cacheCreateTokens = parseInt(usageData.totalCacheCreateTokens) || 0 const cacheReadTokens = parseInt(usageData.totalCacheReadTokens) || 0 const currentAllTokens = parseInt(usageData.totalAllTokens) || 0 // 计算正确的 allTokens const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens if (currentAllTokens !== correctAllTokens && correctAllTokens > 0) { logger.info(` 修复 ${keyId}: ${currentAllTokens} -> ${correctAllTokens}`) if (!isDryRun) { await client.hset(usageKey, 'totalAllTokens', correctAllTokens) } stats.fixedTotalKeys++ } } } catch (error) { logger.error(` 错误处理 ${keyId}: ${error.message}`) stats.errors++ } } // 2. 修复每日统计数据 logger.info('\n📅 修复每日统计数据...') const dailyPattern = 'usage:daily:*' const dailyKeys = await client.keys(dailyPattern) for (const dailyKey of dailyKeys) { try { const data = await client.hgetall(dailyKey) if (data && Object.keys(data).length > 0) { const inputTokens = parseInt(data.inputTokens) || 0 const outputTokens = parseInt(data.outputTokens) || 0 const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0 const cacheReadTokens = parseInt(data.cacheReadTokens) || 0 const currentAllTokens = parseInt(data.allTokens) || 0 const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens if (currentAllTokens !== correctAllTokens && correctAllTokens > 0) { if (!isDryRun) { await client.hset(dailyKey, 'allTokens', correctAllTokens) } stats.fixedDailyKeys++ } } } catch (error) { logger.error(` 错误处理 ${dailyKey}: ${error.message}`) stats.errors++ } } // 3. 修复每月统计数据 logger.info('\n📆 修复每月统计数据...') const monthlyPattern = 'usage:monthly:*' const monthlyKeys = await client.keys(monthlyPattern) for (const monthlyKey of monthlyKeys) { try { const data = await client.hgetall(monthlyKey) if (data && Object.keys(data).length > 0) { const inputTokens = parseInt(data.inputTokens) || 0 const outputTokens = parseInt(data.outputTokens) || 0 const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0 const cacheReadTokens = parseInt(data.cacheReadTokens) || 0 const currentAllTokens = parseInt(data.allTokens) || 0 const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens if (currentAllTokens !== correctAllTokens && correctAllTokens > 0) { if (!isDryRun) { await client.hset(monthlyKey, 'allTokens', correctAllTokens) } stats.fixedMonthlyKeys++ } } } catch (error) { logger.error(` 错误处理 ${monthlyKey}: ${error.message}`) stats.errors++ } } // 4. 修复模型级别的统计数据 logger.info('\n🤖 修复模型级别统计数据...') const modelPatterns = [ 'usage:model:daily:*', 'usage:model:monthly:*', 'usage:*:model:daily:*', 'usage:*:model:monthly:*' ] for (const pattern of modelPatterns) { const modelKeys = await client.keys(pattern) for (const modelKey of modelKeys) { try { const data = await client.hgetall(modelKey) if (data && Object.keys(data).length > 0) { const inputTokens = parseInt(data.inputTokens) || 0 const outputTokens = parseInt(data.outputTokens) || 0 const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0 const cacheReadTokens = parseInt(data.cacheReadTokens) || 0 const currentAllTokens = parseInt(data.allTokens) || 0 const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens if (currentAllTokens !== correctAllTokens && correctAllTokens > 0) { if (!isDryRun) { await client.hset(modelKey, 'allTokens', correctAllTokens) } stats.fixedModelKeys++ } } } catch (error) { logger.error(` 错误处理 ${modelKey}: ${error.message}`) stats.errors++ } } } // 5. 验证修复结果 if (!isDryRun) { logger.info('\n✅ 验证修复结果...') // 随机抽样验证 const sampleSize = Math.min(5, apiKeys.length) for (let i = 0; i < sampleSize; i++) { const randomIndex = Math.floor(Math.random() * apiKeys.length) const keyId = apiKeys[randomIndex].replace('apikey:', '') const usage = await redis.getUsageStats(keyId) logger.info(` 样本 ${keyId}:`) logger.info(` Total tokens: ${usage.total.tokens}`) logger.info(` All tokens: ${usage.total.allTokens}`) logger.info(` 一致性: ${usage.total.tokens === usage.total.allTokens ? '✅' : '❌'}`) } } // 打印统计结果 logger.info('\n📊 修复统计:') logger.info(` 总 API Keys: ${stats.totalKeys}`) logger.info(` 修复的总统计: ${stats.fixedTotalKeys}`) logger.info(` 修复的日统计: ${stats.fixedDailyKeys}`) logger.info(` 修复的月统计: ${stats.fixedMonthlyKeys}`) logger.info(` 修复的模型统计: ${stats.fixedModelKeys}`) logger.info(` 错误数: ${stats.errors}`) if (isDryRun) { logger.info('\n💡 这是 DRY RUN - 没有实际修改数据') logger.info(' 运行不带 --dry-run 参数来实际执行修复') } else { logger.success('\n✅ 数据修复完成!') } } catch (error) { logger.error('❌ 修复过程出错:', error) process.exit(1) } finally { await redis.disconnect() } } // 执行修复 fixUsageStats().catch((error) => { logger.error('❌ 未处理的错误:', error) process.exit(1) })