const redis = require('../models/redis') const apiKeyService = require('./apiKeyService') const CostCalculator = require('../utils/costCalculator') const logger = require('../utils/logger') class CostInitService { /** * 初始化所有API Key的费用数据 * 扫描历史使用记录并计算费用 */ async initializeAllCosts() { try { logger.info('💰 Starting cost initialization for all API Keys...') const apiKeys = await apiKeyService.getAllApiKeys() const client = redis.getClientSafe() let processedCount = 0 let errorCount = 0 for (const apiKey of apiKeys) { try { await this.initializeApiKeyCosts(apiKey.id, client) processedCount++ if (processedCount % 10 === 0) { logger.info(`💰 Processed ${processedCount} API Keys...`) } } catch (error) { errorCount++ logger.error(`❌ Failed to initialize costs for API Key ${apiKey.id}:`, error) } } logger.success( `💰 Cost initialization completed! Processed: ${processedCount}, Errors: ${errorCount}` ) return { processed: processedCount, errors: errorCount } } catch (error) { logger.error('❌ Failed to initialize costs:', error) throw error } } /** * 初始化单个API Key的费用数据 */ async initializeApiKeyCosts(apiKeyId, client) { // 获取所有时间的模型使用统计 const modelKeys = await client.keys(`usage:${apiKeyId}:model:*:*:*`) // 按日期分组统计 const dailyCosts = new Map() // date -> cost const monthlyCosts = new Map() // month -> cost const hourlyCosts = new Map() // hour -> cost for (const key of modelKeys) { // 解析key格式: usage:{keyId}:model:{period}:{model}:{date} const match = key.match( /usage:(.+):model:(daily|monthly|hourly):(.+):(\d{4}-\d{2}(?:-\d{2})?(?::\d{2})?)$/ ) if (!match) { continue } const [, , period, model, dateStr] = match // 获取使用数据 const data = await client.hgetall(key) if (!data || Object.keys(data).length === 0) { continue } // 计算费用 const usage = { input_tokens: parseInt(data.totalInputTokens) || parseInt(data.inputTokens) || 0, output_tokens: parseInt(data.totalOutputTokens) || parseInt(data.outputTokens) || 0, cache_creation_input_tokens: parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0, cache_read_input_tokens: parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0 } const costResult = CostCalculator.calculateCost(usage, model) const cost = costResult.costs.total // 根据period分组累加费用 if (period === 'daily') { const currentCost = dailyCosts.get(dateStr) || 0 dailyCosts.set(dateStr, currentCost + cost) } else if (period === 'monthly') { const currentCost = monthlyCosts.get(dateStr) || 0 monthlyCosts.set(dateStr, currentCost + cost) } else if (period === 'hourly') { const currentCost = hourlyCosts.get(dateStr) || 0 hourlyCosts.set(dateStr, currentCost + cost) } } // 将计算出的费用写入Redis const promises = [] // 写入每日费用 for (const [date, cost] of dailyCosts) { const key = `usage:cost:daily:${apiKeyId}:${date}` promises.push( client.set(key, cost.toString()), client.expire(key, 86400 * 30) // 30天过期 ) } // 写入每月费用 for (const [month, cost] of monthlyCosts) { const key = `usage:cost:monthly:${apiKeyId}:${month}` promises.push( client.set(key, cost.toString()), client.expire(key, 86400 * 90) // 90天过期 ) } // 写入每小时费用 for (const [hour, cost] of hourlyCosts) { const key = `usage:cost:hourly:${apiKeyId}:${hour}` promises.push( client.set(key, cost.toString()), client.expire(key, 86400 * 7) // 7天过期 ) } // 计算总费用 let totalCost = 0 for (const cost of dailyCosts.values()) { totalCost += cost } // 写入总费用 if (totalCost > 0) { const totalKey = `usage:cost:total:${apiKeyId}` promises.push(client.set(totalKey, totalCost.toString())) } await Promise.all(promises) logger.debug( `💰 Initialized costs for API Key ${apiKeyId}: Daily entries: ${dailyCosts.size}, Total cost: $${totalCost.toFixed(2)}` ) } /** * 检查是否需要初始化费用数据 */ async needsInitialization() { try { const client = redis.getClientSafe() // 检查是否有任何费用数据 const costKeys = await client.keys('usage:cost:*') // 如果没有费用数据,需要初始化 if (costKeys.length === 0) { logger.info('💰 No cost data found, initialization needed') return true } // 检查是否有使用数据但没有对应的费用数据 const sampleKeys = await client.keys('usage:*:model:daily:*:*') if (sampleKeys.length > 10) { // 抽样检查 const sampleSize = Math.min(10, sampleKeys.length) for (let i = 0; i < sampleSize; i++) { const usageKey = sampleKeys[Math.floor(Math.random() * sampleKeys.length)] const match = usageKey.match(/usage:(.+):model:daily:(.+):(\d{4}-\d{2}-\d{2})$/) if (match) { const [, keyId, , date] = match const costKey = `usage:cost:daily:${keyId}:${date}` const hasCost = await client.exists(costKey) if (!hasCost) { logger.info( `💰 Found usage without cost data for key ${keyId} on ${date}, initialization needed` ) return true } } } } logger.info('💰 Cost data appears to be up to date') return false } catch (error) { logger.error('❌ Failed to check initialization status:', error) return false } } } module.exports = new CostInitService()