mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
fix: 修复PR #458中的totalCostLimit功能问题
主要修复: - 移除重复的totalUsageLimit字段,统一使用totalCostLimit - 删除auth.js中重复的总费用限制检查逻辑 - 删除admin.js中重复的totalCostLimit验证代码 - 更新所有前端组件,移除totalUsageLimit引用 功能改进: - 确保totalCostLimit作为永久累计费用限制正常工作 - 与dailyCostLimit(每日重置)功能互补 - 适用于预付费、一次性API Key场景 测试: - 删除有逻辑错误的test-total-usage-limit.js - 创建新的test-total-cost-limit.js验证功能正确性 - 所有测试通过,功能正常工作 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -312,33 +312,6 @@ const authenticateApiKey = async (req, res, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查总额度限制(基于累计费用)
|
||||
const totalUsageLimit = Number(validation.keyData.totalUsageLimit || 0)
|
||||
if (totalUsageLimit > 0) {
|
||||
const totalCost = Number(validation.keyData.totalCost || 0)
|
||||
|
||||
if (totalCost >= totalUsageLimit) {
|
||||
logger.security(
|
||||
`📉 Total usage limit exceeded for key: ${validation.keyData.id} (${
|
||||
validation.keyData.name
|
||||
}), cost: $${totalCost.toFixed(2)}/$${totalUsageLimit.toFixed(2)}`
|
||||
)
|
||||
|
||||
return res.status(429).json({
|
||||
error: 'Total usage limit exceeded',
|
||||
message: `已达到总额度限制 ($${totalUsageLimit.toFixed(2)})`,
|
||||
currentCost: totalCost,
|
||||
costLimit: totalUsageLimit
|
||||
})
|
||||
}
|
||||
|
||||
logger.api(
|
||||
`📉 Total usage for key: ${validation.keyData.id} (${
|
||||
validation.keyData.name
|
||||
}), cost: $${totalCost.toFixed(2)}/$${totalUsageLimit.toFixed(2)}`
|
||||
)
|
||||
}
|
||||
|
||||
// 检查每日费用限制
|
||||
const dailyCostLimit = validation.keyData.dailyCostLimit || 0
|
||||
if (dailyCostLimit > 0) {
|
||||
@@ -460,7 +433,6 @@ const authenticateApiKey = async (req, res, next) => {
|
||||
allowedClients: validation.keyData.allowedClients,
|
||||
dailyCostLimit: validation.keyData.dailyCostLimit,
|
||||
dailyCost: validation.keyData.dailyCost,
|
||||
totalUsageLimit: validation.keyData.totalUsageLimit,
|
||||
totalCostLimit: validation.keyData.totalCostLimit,
|
||||
totalCost: validation.keyData.totalCost,
|
||||
usage: validation.keyData.usage
|
||||
|
||||
@@ -550,7 +550,6 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
enableClientRestriction,
|
||||
allowedClients,
|
||||
dailyCostLimit,
|
||||
totalUsageLimit,
|
||||
totalCostLimit,
|
||||
weeklyOpusCostLimit,
|
||||
tags,
|
||||
@@ -634,22 +633,6 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
return res.status(400).json({ error: 'All tags must be non-empty strings' })
|
||||
}
|
||||
|
||||
if (totalUsageLimit !== undefined && totalUsageLimit !== null && totalUsageLimit !== '') {
|
||||
const usageLimit = Number(totalUsageLimit)
|
||||
if (Number.isNaN(usageLimit) || usageLimit < 0) {
|
||||
return res.status(400).json({ error: 'Total usage limit must be a non-negative number' })
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
totalCostLimit !== undefined &&
|
||||
totalCostLimit !== null &&
|
||||
totalCostLimit !== '' &&
|
||||
(Number.isNaN(Number(totalCostLimit)) || Number(totalCostLimit) < 0)
|
||||
) {
|
||||
return res.status(400).json({ error: 'Total cost limit must be a non-negative number' })
|
||||
}
|
||||
|
||||
if (
|
||||
totalCostLimit !== undefined &&
|
||||
totalCostLimit !== null &&
|
||||
@@ -704,7 +687,6 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
enableClientRestriction,
|
||||
allowedClients,
|
||||
dailyCostLimit,
|
||||
totalUsageLimit,
|
||||
totalCostLimit,
|
||||
weeklyOpusCostLimit,
|
||||
tags,
|
||||
@@ -745,7 +727,6 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
||||
enableClientRestriction,
|
||||
allowedClients,
|
||||
dailyCostLimit,
|
||||
totalUsageLimit,
|
||||
totalCostLimit,
|
||||
weeklyOpusCostLimit,
|
||||
tags,
|
||||
@@ -796,7 +777,6 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
||||
enableClientRestriction,
|
||||
allowedClients,
|
||||
dailyCostLimit,
|
||||
totalUsageLimit,
|
||||
totalCostLimit,
|
||||
weeklyOpusCostLimit,
|
||||
tags,
|
||||
@@ -915,9 +895,6 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
||||
if (updates.dailyCostLimit !== undefined) {
|
||||
finalUpdates.dailyCostLimit = updates.dailyCostLimit
|
||||
}
|
||||
if (updates.totalUsageLimit !== undefined) {
|
||||
finalUpdates.totalUsageLimit = updates.totalUsageLimit
|
||||
}
|
||||
if (updates.totalCostLimit !== undefined) {
|
||||
finalUpdates.totalCostLimit = updates.totalCostLimit
|
||||
}
|
||||
@@ -1049,7 +1026,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
allowedClients,
|
||||
expiresAt,
|
||||
dailyCostLimit,
|
||||
totalUsageLimit,
|
||||
totalCostLimit,
|
||||
weeklyOpusCostLimit,
|
||||
tags,
|
||||
@@ -1208,14 +1184,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
updates.totalCostLimit = costLimit
|
||||
}
|
||||
|
||||
if (totalUsageLimit !== undefined && totalUsageLimit !== null && totalUsageLimit !== '') {
|
||||
const usageLimit = Number(totalUsageLimit)
|
||||
if (Number.isNaN(usageLimit) || usageLimit < 0) {
|
||||
return res.status(400).json({ error: 'Total usage limit must be a non-negative number' })
|
||||
}
|
||||
updates.totalUsageLimit = usageLimit
|
||||
}
|
||||
|
||||
// 处理 Opus 周费用限制
|
||||
if (
|
||||
weeklyOpusCostLimit !== undefined &&
|
||||
|
||||
@@ -141,7 +141,6 @@ router.post('/api/user-stats', async (req, res) => {
|
||||
rateLimitWindow: parseInt(keyData.rateLimitWindow) || 0,
|
||||
rateLimitRequests: parseInt(keyData.rateLimitRequests) || 0,
|
||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit) || 0,
|
||||
totalUsageLimit: parseFloat(keyData.totalUsageLimit) || 0,
|
||||
totalCostLimit: parseFloat(keyData.totalCostLimit) || 0,
|
||||
dailyCost: dailyCost || 0,
|
||||
totalCost: costStats.total || 0,
|
||||
@@ -376,7 +375,6 @@ router.post('/api/user-stats', async (req, res) => {
|
||||
rateLimitRequests: fullKeyData.rateLimitRequests || 0,
|
||||
rateLimitCost: parseFloat(fullKeyData.rateLimitCost) || 0, // 新增:费用限制
|
||||
dailyCostLimit: fullKeyData.dailyCostLimit || 0,
|
||||
totalUsageLimit: fullKeyData.totalUsageLimit || 0,
|
||||
totalCostLimit: fullKeyData.totalCostLimit || 0,
|
||||
// 当前使用量
|
||||
currentWindowRequests,
|
||||
|
||||
@@ -258,7 +258,6 @@ router.get('/api-keys', authenticateUser, async (req, res) => {
|
||||
usage: flatUsage,
|
||||
dailyCost: key.dailyCost,
|
||||
dailyCostLimit: key.dailyCostLimit,
|
||||
totalUsageLimit: key.totalUsageLimit,
|
||||
totalCost: key.totalCost,
|
||||
totalCostLimit: key.totalCostLimit,
|
||||
// 不返回实际的key值,只返回前缀和后几位
|
||||
@@ -290,15 +289,7 @@ router.get('/api-keys', authenticateUser, async (req, res) => {
|
||||
// 🔑 创建新的API Key
|
||||
router.post('/api-keys', authenticateUser, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
tokenLimit,
|
||||
expiresAt,
|
||||
dailyCostLimit,
|
||||
totalUsageLimit,
|
||||
totalCostLimit
|
||||
} = req.body
|
||||
const { name, description, tokenLimit, expiresAt, dailyCostLimit, totalCostLimit } = req.body
|
||||
|
||||
if (!name || !name.trim()) {
|
||||
return res.status(400).json({
|
||||
@@ -319,16 +310,6 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (totalUsageLimit !== undefined && totalUsageLimit !== null && totalUsageLimit !== '') {
|
||||
const usageLimit = Number(totalUsageLimit)
|
||||
if (Number.isNaN(usageLimit) || usageLimit < 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid total usage limit',
|
||||
message: 'Total usage limit must be a non-negative number'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户API Key数量限制
|
||||
const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id)
|
||||
if (userApiKeys.length >= config.userManagement.maxApiKeysPerUser) {
|
||||
@@ -347,7 +328,6 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
||||
tokenLimit: tokenLimit || null,
|
||||
expiresAt: expiresAt || null,
|
||||
dailyCostLimit: dailyCostLimit || null,
|
||||
totalUsageLimit: totalUsageLimit || null,
|
||||
totalCostLimit: totalCostLimit || null,
|
||||
createdBy: 'user',
|
||||
// 设置服务权限为全部服务,确保前端显示“服务权限”为“全部服务”且具备完整访问权限
|
||||
@@ -372,7 +352,6 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
||||
tokenLimit: newApiKey.tokenLimit,
|
||||
expiresAt: newApiKey.expiresAt,
|
||||
dailyCostLimit: newApiKey.dailyCostLimit,
|
||||
totalUsageLimit: newApiKey.totalUsageLimit,
|
||||
totalCostLimit: newApiKey.totalCostLimit,
|
||||
createdAt: newApiKey.createdAt
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ class ApiKeyService {
|
||||
enableClientRestriction = false,
|
||||
allowedClients = [],
|
||||
dailyCostLimit = 0,
|
||||
totalUsageLimit = 0,
|
||||
totalCostLimit = 0,
|
||||
weeklyOpusCostLimit = 0,
|
||||
tags = [],
|
||||
@@ -70,7 +69,6 @@ class ApiKeyService {
|
||||
enableClientRestriction: String(enableClientRestriction || false),
|
||||
allowedClients: JSON.stringify(allowedClients || []),
|
||||
dailyCostLimit: String(dailyCostLimit || 0),
|
||||
totalUsageLimit: String(totalUsageLimit || 0),
|
||||
totalCostLimit: String(totalCostLimit || 0),
|
||||
weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0),
|
||||
tags: JSON.stringify(tags || []),
|
||||
@@ -115,7 +113,6 @@ class ApiKeyService {
|
||||
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||
allowedClients: JSON.parse(keyData.allowedClients || '[]'),
|
||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||
totalUsageLimit: parseFloat(keyData.totalUsageLimit || 0),
|
||||
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
|
||||
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
||||
tags: JSON.parse(keyData.tags || '[]'),
|
||||
@@ -257,7 +254,6 @@ class ApiKeyService {
|
||||
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||
allowedClients,
|
||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||
totalUsageLimit: parseFloat(keyData.totalUsageLimit || 0),
|
||||
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
|
||||
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
||||
dailyCost: dailyCost || 0,
|
||||
@@ -383,7 +379,6 @@ class ApiKeyService {
|
||||
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||
allowedClients,
|
||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||
totalUsageLimit: parseFloat(keyData.totalUsageLimit || 0),
|
||||
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
|
||||
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
||||
dailyCost: dailyCost || 0,
|
||||
@@ -426,10 +421,6 @@ class ApiKeyService {
|
||||
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0)
|
||||
key.rateLimitRequests = parseInt(key.rateLimitRequests || 0)
|
||||
key.rateLimitCost = parseFloat(key.rateLimitCost || 0) // 新增:速率限制费用字段
|
||||
key.totalUsageLimit = parseFloat(key.totalUsageLimit || 0)
|
||||
if (Number.isNaN(key.totalUsageLimit)) {
|
||||
key.totalUsageLimit = 0
|
||||
}
|
||||
key.currentConcurrency = await redis.getConcurrency(key.id)
|
||||
key.isActive = key.isActive === 'true'
|
||||
key.enableModelRestriction = key.enableModelRestriction === 'true'
|
||||
@@ -558,7 +549,6 @@ class ApiKeyService {
|
||||
'enableClientRestriction',
|
||||
'allowedClients',
|
||||
'dailyCostLimit',
|
||||
'totalUsageLimit',
|
||||
'totalCostLimit',
|
||||
'weeklyOpusCostLimit',
|
||||
'tags',
|
||||
@@ -1156,7 +1146,6 @@ class ApiKeyService {
|
||||
dailyCost,
|
||||
totalCost: costStats.total,
|
||||
dailyCostLimit: parseFloat(key.dailyCostLimit || 0),
|
||||
totalUsageLimit: parseFloat(key.totalUsageLimit || 0),
|
||||
totalCostLimit: parseFloat(key.totalCostLimit || 0),
|
||||
userId: key.userId,
|
||||
userUsername: key.userUsername,
|
||||
@@ -1204,7 +1193,6 @@ class ApiKeyService {
|
||||
createdBy: keyData.createdBy,
|
||||
permissions: keyData.permissions,
|
||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||
totalUsageLimit: parseFloat(keyData.totalUsageLimit || 0),
|
||||
totalCostLimit: parseFloat(keyData.totalCostLimit || 0)
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user