mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat: 优化APIKey查询
1. 查询相关接口修改为APIKey的UUID 2. 输入APIKey查询后,自动查询API的UUID并添加浏览器地址参数,后续可以直接复制链接进行查询。同时保证了APIKey的安全性
This commit is contained in:
@@ -49,13 +49,12 @@ router.get('/style.css', (req, res) => {
|
|||||||
serveStaticFile(req, res, 'style.css', 'text/css; charset=utf-8');
|
serveStaticFile(req, res, 'style.css', 'text/css; charset=utf-8');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 📊 用户API Key统计查询接口 - 安全的自查询接口
|
// 🔑 获取 API Key 对应的 ID
|
||||||
router.post('/api/user-stats', async (req, res) => {
|
router.post('/api/get-key-id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { apiKey } = req.body;
|
const { apiKey } = req.body;
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
logger.security(`🔒 Missing API key in user stats query from ${req.ip || 'unknown'}`);
|
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'API Key is required',
|
error: 'API Key is required',
|
||||||
message: 'Please provide your API Key'
|
message: 'Please provide your API Key'
|
||||||
@@ -63,6 +62,131 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 基本API Key格式验证
|
// 基本API Key格式验证
|
||||||
|
if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid API key format',
|
||||||
|
message: 'API key format is invalid'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证API Key
|
||||||
|
const validation = await apiKeyService.validateApiKey(apiKey);
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
const clientIP = req.ip || req.connection?.remoteAddress || 'unknown';
|
||||||
|
logger.security(`🔒 Invalid API key in get-key-id: ${validation.error} from ${clientIP}`);
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Invalid API key',
|
||||||
|
message: validation.error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyData = validation.keyData;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: keyData.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to get API key ID:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal server error',
|
||||||
|
message: 'Failed to retrieve API key ID'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 📊 用户API Key统计查询接口 - 安全的自查询接口
|
||||||
|
router.post('/api/user-stats', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { apiKey, apiId } = req.body;
|
||||||
|
|
||||||
|
let keyData;
|
||||||
|
let keyId;
|
||||||
|
|
||||||
|
if (apiId) {
|
||||||
|
// 通过 apiId 查询
|
||||||
|
if (typeof apiId !== 'string' || !apiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid API ID format',
|
||||||
|
message: 'API ID must be a valid UUID'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接通过 ID 获取 API Key 数据
|
||||||
|
keyData = await redis.getApiKey(apiId);
|
||||||
|
|
||||||
|
if (!keyData || Object.keys(keyData).length === 0) {
|
||||||
|
logger.security(`🔒 API key not found for ID: ${apiId} from ${req.ip || 'unknown'}`);
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'API key not found',
|
||||||
|
message: 'The specified API key does not exist'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否激活
|
||||||
|
if (keyData.isActive !== 'true') {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'API key is disabled',
|
||||||
|
message: 'This API key has been disabled'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (keyData.expiresAt && new Date() > new Date(keyData.expiresAt)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'API key has expired',
|
||||||
|
message: 'This API key has expired'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
keyId = apiId;
|
||||||
|
|
||||||
|
// 获取使用统计
|
||||||
|
const usage = await redis.getUsageStats(keyId);
|
||||||
|
|
||||||
|
// 获取当日费用统计
|
||||||
|
const dailyCost = await redis.getDailyCost(keyId);
|
||||||
|
|
||||||
|
// 处理数据格式,与 validateApiKey 返回的格式保持一致
|
||||||
|
// 解析限制模型数据
|
||||||
|
let restrictedModels = [];
|
||||||
|
try {
|
||||||
|
restrictedModels = keyData.restrictedModels ? JSON.parse(keyData.restrictedModels) : [];
|
||||||
|
} catch (e) {
|
||||||
|
restrictedModels = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析允许的客户端数据
|
||||||
|
let allowedClients = [];
|
||||||
|
try {
|
||||||
|
allowedClients = keyData.allowedClients ? JSON.parse(keyData.allowedClients) : [];
|
||||||
|
} catch (e) {
|
||||||
|
allowedClients = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化 keyData
|
||||||
|
keyData = {
|
||||||
|
...keyData,
|
||||||
|
tokenLimit: parseInt(keyData.tokenLimit) || 0,
|
||||||
|
concurrencyLimit: parseInt(keyData.concurrencyLimit) || 0,
|
||||||
|
rateLimitWindow: parseInt(keyData.rateLimitWindow) || 0,
|
||||||
|
rateLimitRequests: parseInt(keyData.rateLimitRequests) || 0,
|
||||||
|
dailyCostLimit: parseFloat(keyData.dailyCostLimit) || 0,
|
||||||
|
dailyCost: dailyCost || 0,
|
||||||
|
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||||
|
restrictedModels: restrictedModels,
|
||||||
|
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||||
|
allowedClients: allowedClients,
|
||||||
|
permissions: keyData.permissions || 'all',
|
||||||
|
usage: usage // 使用完整的 usage 数据,而不是只有 total
|
||||||
|
};
|
||||||
|
|
||||||
|
} else if (apiKey) {
|
||||||
|
// 通过 apiKey 查询(保持向后兼容)
|
||||||
if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) {
|
if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) {
|
||||||
logger.security(`🔒 Invalid API key format in user stats query from ${req.ip || 'unknown'}`);
|
logger.security(`🔒 Invalid API key format in user stats query from ${req.ip || 'unknown'}`);
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -83,13 +207,22 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyData = validation.keyData;
|
keyData = validation.keyData;
|
||||||
|
keyId = keyData.id;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
logger.security(`🔒 Missing API key or ID in user stats query from ${req.ip || 'unknown'}`);
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'API Key or ID is required',
|
||||||
|
message: 'Please provide your API Key or API ID'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 记录合法查询
|
// 记录合法查询
|
||||||
logger.api(`📊 User stats query from key: ${keyData.name} (${keyData.id}) from ${req.ip || 'unknown'}`);
|
logger.api(`📊 User stats query from key: ${keyData.name} (${keyId}) from ${req.ip || 'unknown'}`);
|
||||||
|
|
||||||
// 获取验证结果中的完整keyData(包含isActive状态和cost信息)
|
// 获取验证结果中的完整keyData(包含isActive状态和cost信息)
|
||||||
const fullKeyData = validation.keyData;
|
const fullKeyData = keyData;
|
||||||
|
|
||||||
// 计算总费用 - 使用与模型统计相同的逻辑(按模型分别计算)
|
// 计算总费用 - 使用与模型统计相同的逻辑(按模型分别计算)
|
||||||
let totalCost = 0;
|
let totalCost = 0;
|
||||||
@@ -99,7 +232,7 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
const client = redis.getClientSafe();
|
const client = redis.getClientSafe();
|
||||||
|
|
||||||
// 获取所有月度模型统计(与model-stats接口相同的逻辑)
|
// 获取所有月度模型统计(与model-stats接口相同的逻辑)
|
||||||
const allModelKeys = await client.keys(`usage:${fullKeyData.id}:model:monthly:*:*`);
|
const allModelKeys = await client.keys(`usage:${keyId}:model:monthly:*:*`);
|
||||||
const modelUsageMap = new Map();
|
const modelUsageMap = new Map();
|
||||||
|
|
||||||
for (const key of allModelKeys) {
|
for (const key of allModelKeys) {
|
||||||
@@ -157,7 +290,7 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
formattedCost = CostCalculator.formatCost(totalCost);
|
formattedCost = CostCalculator.formatCost(totalCost);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Failed to calculate detailed cost for key ${fullKeyData.id}:`, error);
|
logger.warn(`Failed to calculate detailed cost for key ${keyId}:`, error);
|
||||||
// 回退到简单计算
|
// 回退到简单计算
|
||||||
if (fullKeyData.usage?.total?.allTokens > 0) {
|
if (fullKeyData.usage?.total?.allTokens > 0) {
|
||||||
const usage = fullKeyData.usage.total;
|
const usage = fullKeyData.usage.total;
|
||||||
@@ -176,7 +309,7 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
|
|
||||||
// 构建响应数据(只返回该API Key自己的信息,确保不泄露其他信息)
|
// 构建响应数据(只返回该API Key自己的信息,确保不泄露其他信息)
|
||||||
const responseData = {
|
const responseData = {
|
||||||
id: fullKeyData.id,
|
id: keyId,
|
||||||
name: fullKeyData.name,
|
name: fullKeyData.name,
|
||||||
description: keyData.description || '',
|
description: keyData.description || '',
|
||||||
isActive: true, // 如果能通过validateApiKey验证,说明一定是激活的
|
isActive: true, // 如果能通过validateApiKey验证,说明一定是激活的
|
||||||
@@ -242,16 +375,47 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
// 📊 用户模型统计查询接口 - 安全的自查询接口
|
// 📊 用户模型统计查询接口 - 安全的自查询接口
|
||||||
router.post('/api/user-model-stats', async (req, res) => {
|
router.post('/api/user-model-stats', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { apiKey, period = 'monthly' } = req.body;
|
const { apiKey, apiId, period = 'monthly' } = req.body;
|
||||||
|
|
||||||
if (!apiKey) {
|
let keyData;
|
||||||
logger.security(`🔒 Missing API key in user model stats query from ${req.ip || 'unknown'}`);
|
let keyId;
|
||||||
|
|
||||||
|
if (apiId) {
|
||||||
|
// 通过 apiId 查询
|
||||||
|
if (typeof apiId !== 'string' || !apiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'API Key is required',
|
error: 'Invalid API ID format',
|
||||||
message: 'Please provide your API Key'
|
message: 'API ID must be a valid UUID'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 直接通过 ID 获取 API Key 数据
|
||||||
|
keyData = await redis.getApiKey(apiId);
|
||||||
|
|
||||||
|
if (!keyData || Object.keys(keyData).length === 0) {
|
||||||
|
logger.security(`🔒 API key not found for ID: ${apiId} from ${req.ip || 'unknown'}`);
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'API key not found',
|
||||||
|
message: 'The specified API key does not exist'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否激活
|
||||||
|
if (keyData.isActive !== 'true') {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'API key is disabled',
|
||||||
|
message: 'This API key has been disabled'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
keyId = apiId;
|
||||||
|
|
||||||
|
// 获取使用统计
|
||||||
|
const usage = await redis.getUsageStats(keyId);
|
||||||
|
keyData.usage = { total: usage.total };
|
||||||
|
|
||||||
|
} else if (apiKey) {
|
||||||
|
// 通过 apiKey 查询(保持向后兼容)
|
||||||
// 验证API Key
|
// 验证API Key
|
||||||
const validation = await apiKeyService.validateApiKey(apiKey);
|
const validation = await apiKeyService.validateApiKey(apiKey);
|
||||||
|
|
||||||
@@ -264,8 +428,18 @@ router.post('/api/user-model-stats', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyData = validation.keyData;
|
keyData = validation.keyData;
|
||||||
logger.api(`📊 User model stats query from key: ${keyData.name} (${keyData.id}) for period: ${period}`);
|
keyId = keyData.id;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
logger.security(`🔒 Missing API key or ID in user model stats query from ${req.ip || 'unknown'}`);
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'API Key or ID is required',
|
||||||
|
message: 'Please provide your API Key or API ID'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.api(`📊 User model stats query from key: ${keyData.name} (${keyId}) for period: ${period}`);
|
||||||
|
|
||||||
// 重用管理后台的模型统计逻辑,但只返回该API Key的数据
|
// 重用管理后台的模型统计逻辑,但只返回该API Key的数据
|
||||||
const client = redis.getClientSafe();
|
const client = redis.getClientSafe();
|
||||||
@@ -273,8 +447,8 @@ router.post('/api/user-model-stats', async (req, res) => {
|
|||||||
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
|
||||||
const pattern = period === 'daily' ?
|
const pattern = period === 'daily' ?
|
||||||
`usage:${keyData.id}:model:daily:*:${today}` :
|
`usage:${keyId}:model:daily:*:${today}` :
|
||||||
`usage:${keyData.id}:model:monthly:*:${currentMonth}`;
|
`usage:${keyId}:model:monthly:*:${currentMonth}`;
|
||||||
|
|
||||||
const keys = await client.keys(pattern);
|
const keys = await client.keys(pattern);
|
||||||
const modelStats = [];
|
const modelStats = [];
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ const app = createApp({
|
|||||||
return {
|
return {
|
||||||
// 用户输入
|
// 用户输入
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
|
apiId: null, // 存储 API Key 对应的 ID
|
||||||
|
|
||||||
// 状态控制
|
// 状态控制
|
||||||
loading: false,
|
loading: false,
|
||||||
modelStatsLoading: false,
|
modelStatsLoading: false,
|
||||||
error: '',
|
error: '',
|
||||||
|
showAdminButton: true, // 控制管理后端按钮显示
|
||||||
|
|
||||||
// 时间范围控制
|
// 时间范围控制
|
||||||
statsPeriod: 'daily', // 默认今日
|
statsPeriod: 'daily', // 默认今日
|
||||||
@@ -41,9 +43,11 @@ const app = createApp({
|
|||||||
this.error = '';
|
this.error = '';
|
||||||
this.statsData = null;
|
this.statsData = null;
|
||||||
this.modelStats = [];
|
this.modelStats = [];
|
||||||
|
this.apiId = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/apiStats/api/user-stats', {
|
// 首先获取 API Key 对应的 ID
|
||||||
|
const idResponse = await fetch('/apiStats/api/get-key-id', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -53,6 +57,26 @@ const app = createApp({
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const idResult = await idResponse.json();
|
||||||
|
|
||||||
|
if (!idResponse.ok) {
|
||||||
|
throw new Error(idResult.message || '获取 API Key ID 失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idResult.success) {
|
||||||
|
this.apiId = idResult.data.id;
|
||||||
|
|
||||||
|
// 使用 apiId 查询统计数据
|
||||||
|
const response = await fetch('/apiStats/api/user-stats', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
apiId: this.apiId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -67,15 +91,22 @@ const app = createApp({
|
|||||||
|
|
||||||
// 清除错误信息
|
// 清除错误信息
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
|
||||||
|
// 更新 URL
|
||||||
|
this.updateURL();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || '查询失败');
|
throw new Error(result.message || '查询失败');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(idResult.message || '获取 API Key ID 失败');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Query stats error:', error);
|
console.error('Query stats error:', error);
|
||||||
this.error = error.message || '查询统计数据失败,请检查您的 API Key 是否正确';
|
this.error = error.message || '查询统计数据失败,请检查您的 API Key 是否正确';
|
||||||
this.statsData = null;
|
this.statsData = null;
|
||||||
this.modelStats = [];
|
this.modelStats = [];
|
||||||
|
this.apiId = null;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -83,7 +114,7 @@ const app = createApp({
|
|||||||
|
|
||||||
// 📊 加载所有时间段的统计数据
|
// 📊 加载所有时间段的统计数据
|
||||||
async loadAllPeriodStats() {
|
async loadAllPeriodStats() {
|
||||||
if (!this.apiKey.trim()) {
|
if (!this.apiId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +137,7 @@ const app = createApp({
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
apiKey: this.apiKey,
|
apiId: this.apiId,
|
||||||
period: period
|
period: period
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -156,7 +187,7 @@ const app = createApp({
|
|||||||
|
|
||||||
// 📊 加载模型统计数据
|
// 📊 加载模型统计数据
|
||||||
async loadModelStats(period = 'daily') {
|
async loadModelStats(period = 'daily') {
|
||||||
if (!this.apiKey.trim()) {
|
if (!this.apiId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +200,7 @@ const app = createApp({
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
apiKey: this.apiKey,
|
apiId: this.apiId,
|
||||||
period: period
|
period: period
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -373,6 +404,7 @@ const app = createApp({
|
|||||||
this.monthlyStats = null;
|
this.monthlyStats = null;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
this.statsPeriod = 'daily'; // 重置为默认值
|
this.statsPeriod = 'daily'; // 重置为默认值
|
||||||
|
this.apiId = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 🔄 刷新数据
|
// 🔄 刷新数据
|
||||||
@@ -384,10 +416,70 @@ const app = createApp({
|
|||||||
|
|
||||||
// 📊 刷新当前时间段数据
|
// 📊 刷新当前时间段数据
|
||||||
async refreshCurrentPeriod() {
|
async refreshCurrentPeriod() {
|
||||||
if (this.apiKey) {
|
if (this.apiId) {
|
||||||
await this.loadPeriodStats(this.statsPeriod);
|
await this.loadPeriodStats(this.statsPeriod);
|
||||||
await this.loadModelStats(this.statsPeriod);
|
await this.loadModelStats(this.statsPeriod);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 🔄 更新 URL
|
||||||
|
updateURL() {
|
||||||
|
if (this.apiId) {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set('apiId', this.apiId);
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 📊 使用 apiId 直接加载数据
|
||||||
|
async loadStatsWithApiId() {
|
||||||
|
if (!this.apiId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.statsData = null;
|
||||||
|
this.modelStats = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 apiId 查询统计数据
|
||||||
|
const response = await fetch('/apiStats/api/user-stats', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
apiId: this.apiId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || '查询失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.statsData = result.data;
|
||||||
|
|
||||||
|
// 同时加载今日和本月的统计数据
|
||||||
|
await this.loadAllPeriodStats();
|
||||||
|
|
||||||
|
// 清除错误信息
|
||||||
|
this.error = '';
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '查询失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load stats with apiId error:', error);
|
||||||
|
this.error = error.message || '查询统计数据失败';
|
||||||
|
this.statsData = null;
|
||||||
|
this.modelStats = [];
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -475,10 +567,18 @@ const app = createApp({
|
|||||||
// 页面加载完成后的初始化
|
// 页面加载完成后的初始化
|
||||||
console.log('User Stats Page loaded');
|
console.log('User Stats Page loaded');
|
||||||
|
|
||||||
// 检查 URL 参数是否有预填的 API Key(用于开发测试)
|
// 检查 URL 参数
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const presetApiId = urlParams.get('apiId');
|
||||||
const presetApiKey = urlParams.get('apiKey');
|
const presetApiKey = urlParams.get('apiKey');
|
||||||
if (presetApiKey && presetApiKey.length > 10) {
|
|
||||||
|
if (presetApiId && presetApiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)) {
|
||||||
|
// 如果 URL 中有 apiId,直接使用 apiId 加载数据
|
||||||
|
this.apiId = presetApiId;
|
||||||
|
this.showAdminButton = false; // 隐藏管理后端按钮
|
||||||
|
this.loadStatsWithApiId();
|
||||||
|
} else if (presetApiKey && presetApiKey.length > 10) {
|
||||||
|
// 向后兼容,支持 apiKey 参数
|
||||||
this.apiKey = presetApiKey;
|
this.apiKey = presetApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<p class="text-gray-600 text-sm leading-tight font-medium">使用统计查询</p>
|
<p class="text-gray-600 text-sm leading-tight font-medium">使用统计查询</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div v-if="showAdminButton" class="flex items-center gap-3">
|
||||||
<a href="/web" class="glass-button rounded-xl px-4 py-2 text-white hover:bg-white/20 transition-colors flex items-center gap-2">
|
<a href="/web" class="glass-button rounded-xl px-4 py-2 text-white hover:bg-white/20 transition-colors flex items-center gap-2">
|
||||||
<i class="fas fa-cog text-sm"></i>
|
<i class="fas fa-cog text-sm"></i>
|
||||||
<span class="text-sm font-medium">管理后台</span>
|
<span class="text-sm font-medium">管理后台</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user