feat: 给key增加总用量限制

This commit is contained in:
itzhan
2025-09-19 21:57:24 +08:00
parent 4d78471891
commit ec28b66e7f
11 changed files with 604 additions and 8 deletions

View File

@@ -533,6 +533,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
enableClientRestriction,
allowedClients,
dailyCostLimit,
totalUsageLimit,
totalCostLimit,
weeklyOpusCostLimit,
tags,
activationDays, // 新增:激活后有效天数
@@ -615,6 +617,35 @@ 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 &&
totalCostLimit !== '' &&
(Number.isNaN(Number(totalCostLimit)) || Number(totalCostLimit) < 0)
) {
return res.status(400).json({ error: 'Total cost limit must be a non-negative number' })
}
// 验证激活相关字段
if (expirationMode && !['fixed', 'activation'].includes(expirationMode)) {
return res
@@ -660,6 +691,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
enableClientRestriction,
allowedClients,
dailyCostLimit,
totalUsageLimit,
totalCostLimit,
weeklyOpusCostLimit,
tags,
activationDays,
@@ -699,6 +732,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
enableClientRestriction,
allowedClients,
dailyCostLimit,
totalUsageLimit,
totalCostLimit,
weeklyOpusCostLimit,
tags,
activationDays,
@@ -748,6 +783,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
enableClientRestriction,
allowedClients,
dailyCostLimit,
totalUsageLimit,
totalCostLimit,
weeklyOpusCostLimit,
tags,
activationDays,
@@ -865,6 +902,12 @@ 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
}
if (updates.weeklyOpusCostLimit !== undefined) {
finalUpdates.weeklyOpusCostLimit = updates.weeklyOpusCostLimit
}
@@ -993,6 +1036,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
allowedClients,
expiresAt,
dailyCostLimit,
totalUsageLimit,
totalCostLimit,
weeklyOpusCostLimit,
tags,
ownerId // 新增所有者ID字段
@@ -1142,6 +1187,22 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
updates.dailyCostLimit = costLimit
}
if (totalCostLimit !== undefined && totalCostLimit !== null && totalCostLimit !== '') {
const costLimit = Number(totalCostLimit)
if (isNaN(costLimit) || costLimit < 0) {
return res.status(400).json({ error: 'Total cost limit must be a non-negative number' })
}
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 &&

View File

@@ -114,6 +114,7 @@ router.post('/api/user-stats', async (req, res) => {
// 获取当日费用统计
const dailyCost = await redis.getDailyCost(keyId)
const costStats = await redis.getCostStats(keyId)
// 处理数据格式,与 validateApiKey 返回的格式保持一致
// 解析限制模型数据
@@ -140,7 +141,10 @@ 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,
enableModelRestriction: keyData.enableModelRestriction === 'true',
restrictedModels,
enableClientRestriction: keyData.enableClientRestriction === 'true',
@@ -372,11 +376,14 @@ 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,
currentWindowTokens,
currentWindowCost, // 新增:当前窗口费用
currentDailyCost,
currentTotalCost: totalCost,
// 时间窗口信息
windowStartTime,
windowEndTime,

View File

@@ -258,6 +258,9 @@ 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值只返回前缀和后几位
keyPreview: key.key
? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}`
@@ -287,7 +290,15 @@ 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 } = req.body
const {
name,
description,
tokenLimit,
expiresAt,
dailyCostLimit,
totalUsageLimit,
totalCostLimit
} = req.body
if (!name || !name.trim()) {
return res.status(400).json({
@@ -296,6 +307,32 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
})
}
if (
totalCostLimit !== undefined &&
totalCostLimit !== null &&
totalCostLimit !== '' &&
(Number.isNaN(Number(totalCostLimit)) || Number(totalCostLimit) < 0)
) {
return res.status(400).json({
error: 'Invalid total cost limit',
message: 'Total cost limit must be a non-negative number'
})
}
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) {
@@ -314,6 +351,8 @@ 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',
// 设置服务权限为全部服务,确保前端显示“服务权限”为“全部服务”且具备完整访问权限
permissions: 'all'
@@ -337,6 +376,8 @@ 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
}
})