feat: 增加每日费用限制

This commit is contained in:
KevinLiao
2025-07-27 14:47:59 +08:00
parent bf9ffa831e
commit ac1e367a69
10 changed files with 471 additions and 20 deletions

View File

@@ -18,6 +18,37 @@ const router = express.Router();
// 🔑 API Keys 管理
// 调试获取API Key费用详情
router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params;
const costStats = await redis.getCostStats(keyId);
const dailyCost = await redis.getDailyCost(keyId);
const today = redis.getDateStringInTimezone();
const client = redis.getClientSafe();
// 获取所有相关的Redis键
const costKeys = await client.keys(`usage:cost:*:${keyId}:*`);
const keyValues = {};
for (const key of costKeys) {
keyValues[key] = await client.get(key);
}
res.json({
keyId,
today,
dailyCost,
costStats,
redisKeys: keyValues,
timezone: config.system.timezoneOffset || 8
});
} catch (error) {
logger.error('❌ Failed to get cost debug info:', error);
res.status(500).json({ error: 'Failed to get cost debug info', message: error.message });
}
});
// 获取所有API Keys
router.get('/api-keys', authenticateAdmin, async (req, res) => {
try {
@@ -29,20 +60,26 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
let searchPatterns = [];
if (timeRange === 'today') {
// 今日
const dateStr = now.toISOString().split('T')[0];
// 今日 - 使用时区日期
const redis = require('../models/redis');
const tzDate = redis.getDateInTimezone(now);
const dateStr = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}-${String(tzDate.getDate()).padStart(2, '0')}`;
searchPatterns.push(`usage:daily:*:${dateStr}`);
} else if (timeRange === '7days') {
// 最近7天
const redis = require('../models/redis');
for (let i = 0; i < 7; i++) {
const date = new Date(now);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const tzDate = redis.getDateInTimezone(date);
const dateStr = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}-${String(tzDate.getDate()).padStart(2, '0')}`;
searchPatterns.push(`usage:daily:*:${dateStr}`);
}
} else if (timeRange === 'monthly') {
// 本月
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
const redis = require('../models/redis');
const tzDate = redis.getDateInTimezone(now);
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`;
searchPatterns.push(`usage:monthly:*:${currentMonth}`);
}
@@ -149,11 +186,16 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
// 计算指定时间范围的费用
let totalCost = 0;
const redis = require('../models/redis');
const tzToday = redis.getDateStringInTimezone(now);
const tzDate = redis.getDateInTimezone(now);
const tzMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`;
const modelKeys = timeRange === 'today'
? await client.keys(`usage:${apiKey.id}:model:daily:*:${now.toISOString().split('T')[0]}`)
? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`)
: timeRange === '7days'
? await client.keys(`usage:${apiKey.id}:model:daily:*:*`)
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`);
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`);
const modelStatsMap = new Map();
@@ -277,7 +319,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
enableModelRestriction,
restrictedModels,
enableClientRestriction,
allowedClients
allowedClients,
dailyCostLimit
} = req.body;
// 输入验证
@@ -342,7 +385,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
enableModelRestriction,
restrictedModels,
enableClientRestriction,
allowedClients
allowedClients,
dailyCostLimit
});
logger.success(`🔑 Admin created new API key: ${name}`);
@@ -357,7 +401,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params;
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt } = req.body;
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt, dailyCostLimit } = req.body;
// 只允许更新指定字段
const updates = {};
@@ -453,6 +497,15 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
}
}
// 处理每日费用限制
if (dailyCostLimit !== undefined && dailyCostLimit !== null && dailyCostLimit !== '') {
const costLimit = Number(dailyCostLimit);
if (isNaN(costLimit) || costLimit < 0) {
return res.status(400).json({ error: 'Daily cost limit must be a non-negative number' });
}
updates.dailyCostLimit = costLimit;
}
await apiKeyService.updateApiKey(keyId, updates);
logger.success(`📝 Admin updated API key: ${keyId}`);