From 2756671117499618d3ff89857c66a2093b5b9bb7 Mon Sep 17 00:00:00 2001 From: Feng Yue <2525275@gmail.com> Date: Wed, 13 Aug 2025 22:34:22 +0800 Subject: [PATCH] fix: user stats --- src/middleware/auth.js | 71 ++++++------- src/routes/userRoutes.js | 53 ++++------ src/services/apiKeyService.js | 8 +- src/services/ldapService.js | 83 ++++++++------- src/services/userService.js | 185 +++++++++++++++++++++++----------- 5 files changed, 235 insertions(+), 165 deletions(-) diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 1752b0da..2bfb5ae6 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -490,7 +490,9 @@ const authenticateUser = async (req, res, next) => { // 检查用户是否被禁用 if (!user.isActive) { - logger.security(`🔒 Disabled user login attempt: ${user.username} from ${req.ip || 'unknown'}`) + logger.security( + `🔒 Disabled user login attempt: ${user.username} from ${req.ip || 'unknown'}` + ) return res.status(403).json({ error: 'Account disabled', message: 'Your account has been disabled. Please contact administrator.' @@ -506,7 +508,7 @@ const authenticateUser = async (req, res, next) => { firstName: user.firstName, lastName: user.lastName, role: user.role, - sessionToken: sessionToken, + sessionToken, sessionCreatedAt: session.createdAt } @@ -559,7 +561,7 @@ const authenticateUserOrAdmin = async (req, res, next) => { loginTime: adminSession.loginTime } req.userType = 'admin' - + const authDuration = Date.now() - startTime logger.security(`🔐 Admin authenticated: ${adminSession.username} in ${authDuration}ms`) return next() @@ -575,7 +577,7 @@ const authenticateUserOrAdmin = async (req, res, next) => { const sessionValidation = await userService.validateUserSession(userToken) if (sessionValidation) { const { session, user } = sessionValidation - + if (user.isActive) { req.user = { id: user.id, @@ -589,7 +591,7 @@ const authenticateUserOrAdmin = async (req, res, next) => { sessionCreatedAt: session.createdAt } req.userType = 'user' - + const authDuration = Date.now() - startTime logger.info(`👤 User authenticated: ${user.username} (${user.id}) in ${authDuration}ms`) return next() @@ -606,7 +608,6 @@ const authenticateUserOrAdmin = async (req, res, next) => { error: 'Authentication required', message: 'Please login as user or admin to access this resource' }) - } catch (error) { const authDuration = Date.now() - startTime logger.error(`❌ User/Admin authentication error (${authDuration}ms):`, { @@ -624,34 +625,34 @@ const authenticateUserOrAdmin = async (req, res, next) => { } // 🛡️ 权限检查中间件 -const requireRole = (allowedRoles) => { - return (req, res, next) => { - // 管理员始终有权限 - if (req.admin) { - return next() - } - - // 检查用户角色 - if (req.user) { - const userRole = req.user.role - const allowed = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles] - - if (allowed.includes(userRole)) { - return next() - } else { - logger.security(`🚫 Access denied for user ${req.user.username} (role: ${userRole}) to ${req.originalUrl}`) - return res.status(403).json({ - error: 'Insufficient permissions', - message: `This resource requires one of the following roles: ${allowed.join(', ')}` - }) - } - } - - return res.status(401).json({ - error: 'Authentication required', - message: 'Please login to access this resource' - }) +const requireRole = (allowedRoles) => (req, res, next) => { + // 管理员始终有权限 + if (req.admin) { + return next() } + + // 检查用户角色 + if (req.user) { + const userRole = req.user.role + const allowed = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles] + + if (allowed.includes(userRole)) { + return next() + } else { + logger.security( + `🚫 Access denied for user ${req.user.username} (role: ${userRole}) to ${req.originalUrl}` + ) + return res.status(403).json({ + error: 'Insufficient permissions', + message: `This resource requires one of the following roles: ${allowed.join(', ')}` + }) + } + } + + return res.status(401).json({ + error: 'Authentication required', + message: 'Please login to access this resource' + }) } // 🔒 管理员权限检查中间件 @@ -665,7 +666,9 @@ const requireAdmin = (req, res, next) => { return next() } - logger.security(`🚫 Admin access denied for ${req.user?.username || 'unknown'} from ${req.ip || 'unknown'}`) + logger.security( + `🚫 Admin access denied for ${req.user?.username || 'unknown'} from ${req.ip || 'unknown'}` + ) return res.status(403).json({ error: 'Admin access required', message: 'This resource requires administrator privileges' diff --git a/src/routes/userRoutes.js b/src/routes/userRoutes.js index c6a698fc..5ec1fdbd 100644 --- a/src/routes/userRoutes.js +++ b/src/routes/userRoutes.js @@ -66,7 +66,6 @@ router.post('/login', async (req, res) => { }, sessionToken: authResult.sessionToken }) - } catch (error) { logger.error('❌ User login error:', error) res.status(500).json({ @@ -80,14 +79,13 @@ router.post('/login', async (req, res) => { router.post('/logout', authenticateUser, async (req, res) => { try { await userService.invalidateUserSession(req.user.sessionToken) - + logger.info(`👋 User logout: ${req.user.username}`) res.json({ success: true, message: 'Logout successful' }) - } catch (error) { logger.error('❌ User logout error:', error) res.status(500).json({ @@ -125,7 +123,6 @@ router.get('/profile', authenticateUser, async (req, res) => { totalUsage: user.totalUsage } }) - } catch (error) { logger.error('❌ Get user profile error:', error) res.status(500).json({ @@ -139,9 +136,9 @@ router.get('/profile', authenticateUser, async (req, res) => { router.get('/api-keys', authenticateUser, async (req, res) => { try { const apiKeys = await apiKeyService.getUserApiKeys(req.user.id) - + // 移除敏感信息 - const safeApiKeys = apiKeys.map(key => ({ + const safeApiKeys = apiKeys.map((key) => ({ id: key.id, name: key.name, description: key.description, @@ -154,7 +151,9 @@ router.get('/api-keys', authenticateUser, async (req, res) => { dailyCost: key.dailyCost, dailyCostLimit: key.dailyCostLimit, // 不返回实际的key值,只返回前缀和后几位 - keyPreview: key.key ? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}` : null + keyPreview: key.key + ? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}` + : null })) res.json({ @@ -162,7 +161,6 @@ router.get('/api-keys', authenticateUser, async (req, res) => { apiKeys: safeApiKeys, total: safeApiKeys.length }) - } catch (error) { logger.error('❌ Get user API keys error:', error) res.status(500).json({ @@ -207,7 +205,7 @@ router.post('/api-keys', authenticateUser, async (req, res) => { } const newApiKey = await apiKeyService.createApiKey(apiKeyData) - + // 更新用户API Key数量 await userService.updateUserApiKeyCount(req.user.id, userApiKeys.length + 1) @@ -227,7 +225,6 @@ router.post('/api-keys', authenticateUser, async (req, res) => { createdAt: newApiKey.createdAt } }) - } catch (error) { logger.error('❌ Create user API key error:', error) res.status(500).json({ @@ -265,7 +262,6 @@ router.post('/api-keys/:keyId/regenerate', authenticateUser, async (req, res) => updatedAt: newKey.updatedAt } }) - } catch (error) { logger.error('❌ Regenerate user API key error:', error) res.status(500).json({ @@ -301,7 +297,6 @@ router.delete('/api-keys/:keyId', authenticateUser, async (req, res) => { success: true, message: 'API key deleted successfully' }) - } catch (error) { logger.error('❌ Delete user API key error:', error) res.status(500).json({ @@ -318,7 +313,7 @@ router.get('/usage-stats', authenticateUser, async (req, res) => { // 获取用户的API Keys const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id) - const apiKeyIds = userApiKeys.map(key => key.id) + const apiKeyIds = userApiKeys.map((key) => key.id) if (apiKeyIds.length === 0) { return res.json({ @@ -341,7 +336,6 @@ router.get('/usage-stats', authenticateUser, async (req, res) => { success: true, stats }) - } catch (error) { logger.error('❌ Get user usage stats error:', error) res.status(500).json({ @@ -371,10 +365,11 @@ router.get('/', authenticateUserOrAdmin, requireAdmin, async (req, res) => { let filteredUsers = result.users if (search) { const searchLower = search.toLowerCase() - filteredUsers = result.users.filter(user => - user.username.toLowerCase().includes(searchLower) || - user.displayName.toLowerCase().includes(searchLower) || - user.email.toLowerCase().includes(searchLower) + filteredUsers = result.users.filter( + (user) => + user.username.toLowerCase().includes(searchLower) || + user.displayName.toLowerCase().includes(searchLower) || + user.email.toLowerCase().includes(searchLower) ) } @@ -388,7 +383,6 @@ router.get('/', authenticateUserOrAdmin, requireAdmin, async (req, res) => { totalPages: result.totalPages } }) - } catch (error) { logger.error('❌ Get users list error:', error) res.status(500).json({ @@ -418,7 +412,7 @@ router.get('/:userId', authenticateUserOrAdmin, requireAdmin, async (req, res) = success: true, user: { ...user, - apiKeys: apiKeys.map(key => ({ + apiKeys: apiKeys.map((key) => ({ id: key.id, name: key.name, description: key.description, @@ -426,11 +420,12 @@ router.get('/:userId', authenticateUserOrAdmin, requireAdmin, async (req, res) = createdAt: key.createdAt, lastUsedAt: key.lastUsedAt, usage: key.usage, - keyPreview: key.key ? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}` : null + keyPreview: key.key + ? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}` + : null })) } }) - } catch (error) { logger.error('❌ Get user details error:', error) res.status(500).json({ @@ -456,7 +451,9 @@ router.patch('/:userId/status', authenticateUserOrAdmin, requireAdmin, async (re const updatedUser = await userService.updateUserStatus(userId, isActive) const adminUser = req.admin?.username || req.user?.username - logger.info(`🔄 Admin ${adminUser} ${isActive ? 'enabled' : 'disabled'} user: ${updatedUser.username}`) + logger.info( + `🔄 Admin ${adminUser} ${isActive ? 'enabled' : 'disabled'} user: ${updatedUser.username}` + ) res.json({ success: true, @@ -468,7 +465,6 @@ router.patch('/:userId/status', authenticateUserOrAdmin, requireAdmin, async (re updatedAt: updatedUser.updatedAt } }) - } catch (error) { logger.error('❌ Update user status error:', error) res.status(500).json({ @@ -507,7 +503,6 @@ router.patch('/:userId/role', authenticateUserOrAdmin, requireAdmin, async (req, updatedAt: updatedUser.updatedAt } }) - } catch (error) { logger.error('❌ Update user role error:', error) res.status(500).json({ @@ -540,7 +535,6 @@ router.post('/:userId/disable-keys', authenticateUserOrAdmin, requireAdmin, asyn message: `Disabled ${result.count} API keys for user ${user.username}`, disabledCount: result.count }) - } catch (error) { logger.error('❌ Disable user API keys error:', error) res.status(500).json({ @@ -566,7 +560,7 @@ router.get('/:userId/usage-stats', authenticateUserOrAdmin, requireAdmin, async // 获取用户的API Keys const userApiKeys = await apiKeyService.getUserApiKeys(userId) - const apiKeyIds = userApiKeys.map(key => key.id) + const apiKeyIds = userApiKeys.map((key) => key.id) if (apiKeyIds.length === 0) { return res.json({ @@ -599,7 +593,6 @@ router.get('/:userId/usage-stats', authenticateUserOrAdmin, requireAdmin, async }, stats }) - } catch (error) { logger.error('❌ Get user usage stats (admin) error:', error) res.status(500).json({ @@ -618,7 +611,6 @@ router.get('/stats/overview', authenticateUserOrAdmin, requireAdmin, async (req, success: true, stats }) - } catch (error) { logger.error('❌ Get user stats overview error:', error) res.status(500).json({ @@ -638,7 +630,6 @@ router.get('/admin/ldap-test', authenticateUserOrAdmin, requireAdmin, async (req ldapTest: testResult, config: ldapService.getConfigInfo() }) - } catch (error) { logger.error('❌ LDAP test error:', error) res.status(500).json({ @@ -648,4 +639,4 @@ router.get('/admin/ldap-test', authenticateUserOrAdmin, requireAdmin, async (req } }) -module.exports = router \ No newline at end of file +module.exports = router diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 082e992e..e46b674c 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -492,8 +492,8 @@ class ApiKeyService { try { const allKeys = await redis.getAllApiKeys() return allKeys - .filter(key => key.userId === userId) - .map(key => ({ + .filter((key) => key.userId === userId) + .map((key) => ({ id: key.id, name: key.name, description: key.description, @@ -520,7 +520,9 @@ class ApiKeyService { async getApiKeyById(keyId, userId = null) { try { const keyData = await redis.getApiKey(keyId) - if (!keyData) return null + if (!keyData) { + return null + } // 如果指定了用户ID,检查权限 if (userId && keyData.userId !== userId) { diff --git a/src/services/ldapService.js b/src/services/ldapService.js index 6264e81d..ddbeb692 100644 --- a/src/services/ldapService.js +++ b/src/services/ldapService.js @@ -7,7 +7,7 @@ class LdapService { constructor() { this.config = config.ldap this.client = null - + // 验证配置 if (this.config.enabled) { this.validateConfiguration() @@ -17,31 +17,34 @@ class LdapService { // 🔍 验证LDAP配置 validateConfiguration() { const errors = [] - + if (!this.config.server) { errors.push('LDAP server configuration is missing') } else { if (!this.config.server.url || typeof this.config.server.url !== 'string') { errors.push('LDAP server URL is not configured or invalid') } - + if (!this.config.server.bindDN || typeof this.config.server.bindDN !== 'string') { errors.push('LDAP bind DN is not configured or invalid') } - - if (!this.config.server.bindCredentials || typeof this.config.server.bindCredentials !== 'string') { + + if ( + !this.config.server.bindCredentials || + typeof this.config.server.bindCredentials !== 'string' + ) { errors.push('LDAP bind credentials are not configured or invalid') } - + if (!this.config.server.searchBase || typeof this.config.server.searchBase !== 'string') { errors.push('LDAP search base is not configured or invalid') } - + if (!this.config.server.searchFilter || typeof this.config.server.searchFilter !== 'string') { errors.push('LDAP search filter is not configured or invalid') } } - + if (errors.length > 0) { logger.error('❌ LDAP configuration validation failed:', errors) // Don't throw error during initialization, just log warnings @@ -59,7 +62,7 @@ class LdapService { // Try different ways to get the DN let dn = null - + // Method 1: Direct dn property if (ldapEntry.dn) { dn = ldapEntry.dn @@ -107,35 +110,35 @@ class LdapService { // 如果使用 LDAPS (SSL/TLS),添加 TLS 选项 if (this.config.server.url.toLowerCase().startsWith('ldaps://')) { const tlsOptions = {} - + // 证书验证设置 if (this.config.server.tls) { if (typeof this.config.server.tls.rejectUnauthorized === 'boolean') { tlsOptions.rejectUnauthorized = this.config.server.tls.rejectUnauthorized } - + // CA 证书 if (this.config.server.tls.ca) { tlsOptions.ca = this.config.server.tls.ca } - + // 客户端证书和私钥 (双向认证) if (this.config.server.tls.cert) { tlsOptions.cert = this.config.server.tls.cert } - + if (this.config.server.tls.key) { tlsOptions.key = this.config.server.tls.key } - + // 服务器名称 (SNI) if (this.config.server.tls.servername) { tlsOptions.servername = this.config.server.tls.servername } } - + clientOptions.tlsOptions = tlsOptions - + logger.debug('🔒 Creating LDAPS client with TLS options:', { url: this.config.server.url, rejectUnauthorized: tlsOptions.rejectUnauthorized, @@ -184,23 +187,23 @@ class LdapService { async bindClient(client) { return new Promise((resolve, reject) => { // 验证绑定凭据 - const bindDN = this.config.server.bindDN - const bindCredentials = this.config.server.bindCredentials - + const { bindDN } = this.config.server + const { bindCredentials } = this.config.server + if (!bindDN || typeof bindDN !== 'string') { const error = new Error('LDAP bind DN is not configured or invalid') logger.error('❌ LDAP configuration error:', error.message) reject(error) return } - + if (!bindCredentials || typeof bindCredentials !== 'string') { const error = new Error('LDAP bind credentials are not configured or invalid') logger.error('❌ LDAP configuration error:', error.message) reject(error) return } - + client.bind(bindDN, bindCredentials, (err) => { if (err) { logger.error('❌ LDAP bind failed:', err) @@ -226,7 +229,7 @@ class LdapService { logger.debug(`🔍 Searching for user: ${username} with filter: ${searchFilter}`) const entries = [] - + client.search(this.config.server.searchBase, searchOptions, (err, res) => { if (err) { logger.error('❌ LDAP search error:', err) @@ -256,8 +259,10 @@ class LdapService { }) res.on('end', (result) => { - logger.debug(`✅ LDAP search completed. Status: ${result.status}, Found ${entries.length} entries`) - + logger.debug( + `✅ LDAP search completed. Status: ${result.status}, Found ${entries.length} entries` + ) + if (entries.length === 0) { resolve(null) } else { @@ -270,7 +275,7 @@ class LdapService { entryStringified: JSON.stringify(entries[0], null, 2).substring(0, 500) }) } - + if (entries.length === 1) { resolve(entries[0]) } else { @@ -293,18 +298,18 @@ class LdapService { reject(error) return } - + if (!password || typeof password !== 'string') { logger.debug(`🚫 Invalid or empty password for DN: ${userDN}`) resolve(false) return } - + const authClient = this.createClient() - + authClient.bind(userDN, password, (err) => { authClient.unbind() // 立即关闭认证客户端 - + if (err) { if (err.name === 'InvalidCredentialsError') { logger.debug(`🚫 Invalid credentials for DN: ${userDN}`) @@ -329,7 +334,7 @@ class LdapService { // 创建属性映射 const attrMap = {} - attributes.forEach(attr => { + attributes.forEach((attr) => { const name = attr.type || attr.name const values = Array.isArray(attr.values) ? attr.values : [attr.values] attrMap[name] = values.length === 1 ? values[0] : values @@ -337,7 +342,7 @@ class LdapService { // 根据配置映射用户属性 const mapping = this.config.userMapping - + userInfo.displayName = attrMap[mapping.displayName] || username userInfo.email = attrMap[mapping.email] || '' userInfo.firstName = attrMap[mapping.firstName] || '' @@ -386,7 +391,10 @@ class LdapService { throw new Error('LDAP bind DN is not configured') } - if (!this.config.server.bindCredentials || typeof this.config.server.bindCredentials !== 'string') { + if ( + !this.config.server.bindCredentials || + typeof this.config.server.bindCredentials !== 'string' + ) { throw new Error('LDAP bind credentials are not configured') } @@ -450,9 +458,9 @@ class LdapService { // 7. 检查用户是否被禁用 if (!user.isActive) { logger.security(`🔒 Disabled user LDAP login attempt: ${username} from LDAP authentication`) - return { - success: false, - message: 'Your account has been disabled. Please contact administrator.' + return { + success: false, + message: 'Your account has been disabled. Please contact administrator.' } } @@ -470,7 +478,6 @@ class LdapService { sessionToken, message: 'Authentication successful' } - } catch (error) { logger.error('❌ LDAP authentication error:', error) return { @@ -499,7 +506,7 @@ class LdapService { try { await this.bindClient(client) - + return { success: true, message: 'LDAP connection successful', @@ -553,4 +560,4 @@ class LdapService { } } -module.exports = new LdapService() \ No newline at end of file +module.exports = new LdapService() diff --git a/src/services/userService.js b/src/services/userService.js index bf0d1ff8..0d460459 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -1,6 +1,5 @@ const redis = require('../models/redis') const crypto = require('crypto') -const bcrypt = require('bcryptjs') const logger = require('../utils/logger') const config = require('../../config/config') @@ -88,7 +87,9 @@ class UserService { async getUserByUsername(username) { try { const userId = await redis.get(`${this.usernamePrefix}${username}`) - if (!userId) return null + if (!userId) { + return null + } const userData = await redis.get(`${this.userPrefix}${userId}`) return userData ? JSON.parse(userData) : null @@ -99,16 +100,100 @@ class UserService { } // 👤 通过ID获取用户 - async getUserById(userId) { + async getUserById(userId, calculateUsage = true) { try { const userData = await redis.get(`${this.userPrefix}${userId}`) - return userData ? JSON.parse(userData) : null + if (!userData) { + return null + } + + const user = JSON.parse(userData) + + // Calculate totalUsage by aggregating user's API keys usage (if requested) + if (calculateUsage) { + try { + const usageStats = await this.calculateUserUsageStats(userId) + user.totalUsage = usageStats.totalUsage + user.apiKeyCount = usageStats.apiKeyCount + } catch (error) { + logger.error('❌ Error calculating user usage stats:', error) + // Fallback to stored values if calculation fails + user.totalUsage = user.totalUsage || { + requests: 0, + inputTokens: 0, + outputTokens: 0, + totalCost: 0 + } + user.apiKeyCount = user.apiKeyCount || 0 + } + } + + return user } catch (error) { logger.error('❌ Error getting user by ID:', error) throw error } } + // 📊 计算用户使用统计(通过聚合API Keys) + async calculateUserUsageStats(userId) { + try { + // Use redis directly to avoid circular dependency + const client = redis.getClientSafe() + const pattern = 'api_key:*' + const keys = await client.keys(pattern) + + const userApiKeys = [] + for (const key of keys) { + const keyData = await client.get(key) + if (keyData) { + const apiKey = JSON.parse(keyData) + if (apiKey.userId === userId) { + // Get usage stats for this API key + const usage = await redis.getUsageStats(apiKey.id) + userApiKeys.push({ + id: apiKey.id, + name: apiKey.name, + usage + }) + } + } + } + + const totalUsage = { + requests: 0, + inputTokens: 0, + outputTokens: 0, + totalCost: 0 + } + + for (const apiKey of userApiKeys) { + if (apiKey.usage) { + totalUsage.requests += apiKey.usage.requests || 0 + totalUsage.inputTokens += apiKey.usage.inputTokens || 0 + totalUsage.outputTokens += apiKey.usage.outputTokens || 0 + totalUsage.totalCost += apiKey.usage.totalCost || 0 + } + } + + return { + totalUsage, + apiKeyCount: userApiKeys.length + } + } catch (error) { + logger.error('❌ Error calculating user usage stats:', error) + return { + totalUsage: { + requests: 0, + inputTokens: 0, + outputTokens: 0, + totalCost: 0 + }, + apiKeyCount: 0 + } + } + } + // 📋 获取所有用户列表(管理员功能) async getAllUsers(options = {}) { try { @@ -116,17 +201,21 @@ class UserService { const { page = 1, limit = 20, role, isActive } = options const pattern = `${this.userPrefix}*` const keys = await client.keys(pattern) - + const users = [] for (const key of keys) { const userData = await client.get(key) if (userData) { const user = JSON.parse(userData) - + // 应用过滤条件 - if (role && user.role !== role) continue - if (typeof isActive === 'boolean' && user.isActive !== isActive) continue - + if (role && user.role !== role) { + continue + } + if (typeof isActive === 'boolean' && user.isActive !== isActive) { + continue + } + users.push(user) } } @@ -153,7 +242,7 @@ class UserService { // 🔄 更新用户状态 async updateUserStatus(userId, isActive) { try { - const user = await this.getUserById(userId) + const user = await this.getUserById(userId, false) // Skip usage calculation if (!user) { throw new Error('User not found') } @@ -179,7 +268,7 @@ class UserService { // 🔄 更新用户角色 async updateUserRole(userId, role) { try { - const user = await this.getUserById(userId) + const user = await this.getUserById(userId, false) // Skip usage calculation if (!user) { throw new Error('User not found') } @@ -197,46 +286,22 @@ class UserService { } } - // 📊 更新用户使用统计 - async updateUserUsage(userId, usage) { - try { - const user = await this.getUserById(userId) - if (!user) return - - const { requests = 0, inputTokens = 0, outputTokens = 0, cost = 0 } = usage - - user.totalUsage.requests += requests - user.totalUsage.inputTokens += inputTokens - user.totalUsage.outputTokens += outputTokens - user.totalUsage.totalCost += cost - user.updatedAt = new Date().toISOString() - - await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user)) - } catch (error) { - logger.error('❌ Error updating user usage:', error) - } - } - - // 📊 更新用户API Key数量 - async updateUserApiKeyCount(userId, count) { - try { - const user = await this.getUserById(userId) - if (!user) return - - user.apiKeyCount = count - user.updatedAt = new Date().toISOString() - - await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user)) - } catch (error) { - logger.error('❌ Error updating user API key count:', error) - } + // 📊 更新用户API Key数量 (已废弃,现在通过聚合计算) + async updateUserApiKeyCount(userId, _count) { + // This method is deprecated since apiKeyCount is now calculated dynamically + // in getUserById by aggregating the user's API keys + logger.debug( + `📊 updateUserApiKeyCount called for ${userId} but is now deprecated (count auto-calculated)` + ) } // 📝 记录用户登录 async recordUserLogin(userId) { try { - const user = await this.getUserById(userId) - if (!user) return + const user = await this.getUserById(userId, false) // Skip usage calculation + if (!user) { + return + } user.lastLoginAt = new Date().toISOString() await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user)) @@ -272,10 +337,12 @@ class UserService { async validateUserSession(sessionToken) { try { const sessionData = await redis.get(`${this.userSessionPrefix}${sessionToken}`) - if (!sessionData) return null + if (!sessionData) { + return null + } const session = JSON.parse(sessionData) - + // 检查会话是否过期 if (new Date() > new Date(session.expiresAt)) { await this.invalidateUserSession(sessionToken) @@ -283,7 +350,7 @@ class UserService { } // 获取用户信息 - const user = await this.getUserById(session.userId) + const user = await this.getUserById(session.userId, false) // Skip usage calculation for validation if (!user || !user.isActive) { await this.invalidateUserSession(sessionToken) return null @@ -312,7 +379,7 @@ class UserService { const client = redis.getClientSafe() const pattern = `${this.userSessionPrefix}*` const keys = await client.keys(pattern) - + for (const key of keys) { const sessionData = await client.get(key) if (sessionData) { @@ -322,7 +389,7 @@ class UserService { } } } - + logger.info(`🚫 Invalidated all sessions for user: ${userId}`) } catch (error) { logger.error('❌ Error invalidating user sessions:', error) @@ -332,7 +399,7 @@ class UserService { // 🗑️ 删除用户(软删除,标记为不活跃) async deleteUser(userId) { try { - const user = await this.getUserById(userId) + const user = await this.getUserById(userId, false) // Skip usage calculation if (!user) { throw new Error('User not found') } @@ -343,10 +410,10 @@ class UserService { user.updatedAt = new Date().toISOString() await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user)) - + // 删除所有会话 await this.invalidateUserSessions(userId) - + logger.info(`🗑️ Soft deleted user: ${user.username} (${userId})`) return user } catch (error) { @@ -361,7 +428,7 @@ class UserService { const client = redis.getClientSafe() const pattern = `${this.userPrefix}*` const keys = await client.keys(pattern) - + const stats = { totalUsers: 0, activeUsers: 0, @@ -381,17 +448,17 @@ class UserService { if (userData) { const user = JSON.parse(userData) stats.totalUsers++ - + if (user.isActive) { stats.activeUsers++ } - + if (user.role === 'admin') { stats.adminUsers++ } else { stats.regularUsers++ } - + stats.totalApiKeys += user.apiKeyCount || 0 stats.totalUsage.requests += user.totalUsage?.requests || 0 stats.totalUsage.inputTokens += user.totalUsage?.inputTokens || 0 @@ -408,4 +475,4 @@ class UserService { } } -module.exports = new UserService() \ No newline at end of file +module.exports = new UserService()