mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
Merge remote-tracking branch 'f3n9/dev' into dev-um-8
This commit is contained in:
@@ -13,7 +13,7 @@ class AccountGroupService {
|
||||
* 创建账户分组
|
||||
* @param {Object} groupData - 分组数据
|
||||
* @param {string} groupData.name - 分组名称
|
||||
* @param {string} groupData.platform - 平台类型 (claude/gemini)
|
||||
* @param {string} groupData.platform - 平台类型 (claude/gemini/openai)
|
||||
* @param {string} groupData.description - 分组描述
|
||||
* @returns {Object} 创建的分组
|
||||
*/
|
||||
@@ -327,12 +327,36 @@ class AccountGroupService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据账户ID获取其所属的分组(兼容性方法,返回单个分组)
|
||||
* @param {string} accountId - 账户ID
|
||||
* @returns {Object|null} 分组信息
|
||||
*/
|
||||
async getAccountGroup(accountId) {
|
||||
try {
|
||||
const client = redis.getClientSafe()
|
||||
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
||||
|
||||
for (const groupId of allGroupIds) {
|
||||
const isMember = await client.sismember(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId)
|
||||
if (isMember) {
|
||||
return await this.getGroup(groupId)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('❌ 获取账户所属分组失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据账户ID获取其所属的所有分组
|
||||
* @param {string} accountId - 账户ID
|
||||
* @returns {Array} 分组信息数组
|
||||
*/
|
||||
async getAccountGroup(accountId) {
|
||||
async getAccountGroups(accountId) {
|
||||
try {
|
||||
const client = redis.getClientSafe()
|
||||
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
||||
@@ -357,6 +381,49 @@ class AccountGroupService {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置账户的分组
|
||||
* @param {string} accountId - 账户ID
|
||||
* @param {Array} groupIds - 分组ID数组
|
||||
* @param {string} accountPlatform - 账户平台
|
||||
*/
|
||||
async setAccountGroups(accountId, groupIds, accountPlatform) {
|
||||
try {
|
||||
// 首先移除账户的所有现有分组
|
||||
await this.removeAccountFromAllGroups(accountId)
|
||||
|
||||
// 然后添加到新的分组中
|
||||
for (const groupId of groupIds) {
|
||||
await this.addAccountToGroup(accountId, groupId, accountPlatform)
|
||||
}
|
||||
|
||||
logger.success(`✅ 批量设置账户分组成功: ${accountId} -> [${groupIds.join(', ')}]`)
|
||||
} catch (error) {
|
||||
logger.error('❌ 批量设置账户分组失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从所有分组中移除账户
|
||||
* @param {string} accountId - 账户ID
|
||||
*/
|
||||
async removeAccountFromAllGroups(accountId) {
|
||||
try {
|
||||
const client = redis.getClientSafe()
|
||||
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
||||
|
||||
for (const groupId of allGroupIds) {
|
||||
await client.srem(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId)
|
||||
}
|
||||
|
||||
logger.success(`✅ 从所有分组移除账户成功: ${accountId}`)
|
||||
} catch (error) {
|
||||
logger.error('❌ 从所有分组移除账户失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AccountGroupService()
|
||||
|
||||
@@ -437,6 +437,139 @@ class ApiKeyService {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔄 恢复已删除的API Key
|
||||
async restoreApiKey(keyId, restoredBy = 'system', restoredByType = 'system') {
|
||||
try {
|
||||
const keyData = await redis.getApiKey(keyId)
|
||||
if (!keyData || Object.keys(keyData).length === 0) {
|
||||
throw new Error('API key not found')
|
||||
}
|
||||
|
||||
// 检查是否确实是已删除的key
|
||||
if (keyData.isDeleted !== 'true') {
|
||||
throw new Error('API key is not deleted')
|
||||
}
|
||||
|
||||
// 准备更新的数据
|
||||
const updatedData = { ...keyData }
|
||||
updatedData.isActive = 'true'
|
||||
updatedData.restoredAt = new Date().toISOString()
|
||||
updatedData.restoredBy = restoredBy
|
||||
updatedData.restoredByType = restoredByType
|
||||
|
||||
// 从更新的数据中移除删除相关的字段
|
||||
delete updatedData.isDeleted
|
||||
delete updatedData.deletedAt
|
||||
delete updatedData.deletedBy
|
||||
delete updatedData.deletedByType
|
||||
|
||||
// 保存更新后的数据
|
||||
await redis.setApiKey(keyId, updatedData)
|
||||
|
||||
// 使用Redis的hdel命令删除不需要的字段
|
||||
const keyName = `apikey:${keyId}`
|
||||
await redis.client.hdel(keyName, 'isDeleted', 'deletedAt', 'deletedBy', 'deletedByType')
|
||||
|
||||
// 重新建立哈希映射(恢复API Key的使用能力)
|
||||
if (keyData.apiKey) {
|
||||
await redis.setApiKeyHash(keyData.apiKey, {
|
||||
id: keyId,
|
||||
name: keyData.name,
|
||||
isActive: 'true'
|
||||
})
|
||||
}
|
||||
|
||||
logger.success(`✅ Restored API key: ${keyId} by ${restoredBy} (${restoredByType})`)
|
||||
|
||||
return { success: true, apiKey: updatedData }
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to restore API key:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 🗑️ 彻底删除API Key(物理删除)
|
||||
async permanentDeleteApiKey(keyId) {
|
||||
try {
|
||||
const keyData = await redis.getApiKey(keyId)
|
||||
if (!keyData || Object.keys(keyData).length === 0) {
|
||||
throw new Error('API key not found')
|
||||
}
|
||||
|
||||
// 确保只能彻底删除已经软删除的key
|
||||
if (keyData.isDeleted !== 'true') {
|
||||
throw new Error('只能彻底删除已经删除的API Key')
|
||||
}
|
||||
|
||||
// 删除所有相关的使用统计数据
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]
|
||||
|
||||
// 删除每日统计
|
||||
await redis.client.del(`usage:daily:${today}:${keyId}`)
|
||||
await redis.client.del(`usage:daily:${yesterday}:${keyId}`)
|
||||
|
||||
// 删除月度统计
|
||||
const currentMonth = today.substring(0, 7)
|
||||
await redis.client.del(`usage:monthly:${currentMonth}:${keyId}`)
|
||||
|
||||
// 删除所有相关的统计键(通过模式匹配)
|
||||
const usageKeys = await redis.client.keys(`usage:*:${keyId}*`)
|
||||
if (usageKeys.length > 0) {
|
||||
await redis.client.del(...usageKeys)
|
||||
}
|
||||
|
||||
// 删除API Key本身
|
||||
await redis.deleteApiKey(keyId)
|
||||
|
||||
logger.success(`🗑️ Permanently deleted API key: ${keyId}`)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to permanently delete API key:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 🧹 清空所有已删除的API Keys
|
||||
async clearAllDeletedApiKeys() {
|
||||
try {
|
||||
const allKeys = await this.getAllApiKeys(true)
|
||||
const deletedKeys = allKeys.filter((key) => key.isDeleted === 'true')
|
||||
|
||||
let successCount = 0
|
||||
let failedCount = 0
|
||||
const errors = []
|
||||
|
||||
for (const key of deletedKeys) {
|
||||
try {
|
||||
await this.permanentDeleteApiKey(key.id)
|
||||
successCount++
|
||||
} catch (error) {
|
||||
failedCount++
|
||||
errors.push({
|
||||
keyId: key.id,
|
||||
keyName: key.name,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logger.success(`🧹 Cleared deleted API keys: ${successCount} success, ${failedCount} failed`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
total: deletedKeys.length,
|
||||
successCount,
|
||||
failedCount,
|
||||
errors
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to clear all deleted API keys:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 📊 记录使用情况(支持缓存token和账户级别统计)
|
||||
async recordUsage(
|
||||
keyId,
|
||||
|
||||
@@ -249,6 +249,10 @@ async function updateAccount(accountId, updates) {
|
||||
|
||||
// 删除账户
|
||||
async function deleteAccount(accountId) {
|
||||
// 首先从所有分组中移除此账户
|
||||
const accountGroupService = require('./accountGroupService')
|
||||
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||
|
||||
const client = redisClient.getClientSafe()
|
||||
const accountKey = `${AZURE_OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`
|
||||
|
||||
|
||||
@@ -621,10 +621,7 @@ class ClaudeAccountService {
|
||||
try {
|
||||
// 首先从所有分组中移除此账户
|
||||
const accountGroupService = require('./accountGroupService')
|
||||
const groups = await accountGroupService.getAccountGroup(accountId)
|
||||
for (const group of groups) {
|
||||
await accountGroupService.removeAccountFromGroup(accountId, group.id)
|
||||
}
|
||||
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||
|
||||
const result = await redis.deleteClaudeAccount(accountId)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user