210 lines
7.6 KiB
TypeScript
210 lines
7.6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
// 调用日志接口
|
|
interface CallLog {
|
|
id: string
|
|
keyId: string
|
|
keyName: string
|
|
endpoint: string
|
|
method: string
|
|
status: number
|
|
credits: number
|
|
responseTime: number
|
|
timestamp: string
|
|
ip: string
|
|
requestFields?: string[]
|
|
}
|
|
|
|
// 计费明细接口
|
|
interface BillingDetail {
|
|
category: string
|
|
callCount: number
|
|
avgPrice: number
|
|
totalCredits: number
|
|
percentage: number
|
|
}
|
|
|
|
// 内存存储调用日志
|
|
const callLogs: CallLog[] = [
|
|
{ id: 'log_1', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/user', method: 'GET', status: 200, credits: 5, responseTime: 123, timestamp: '2026-01-31 14:32:15', ip: '123.45.67.89' },
|
|
{ id: 'log_2', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/users/batch', method: 'POST', status: 200, credits: 40, responseTime: 856, timestamp: '2026-01-31 14:30:02', ip: '123.45.67.89' },
|
|
{ id: 'log_3', keyId: 'key_2', keyName: '点了码-测试环境', endpoint: '/api/shensheshou/ai/chat', method: 'POST', status: 200, credits: 5, responseTime: 2341, timestamp: '2026-01-31 14:28:45', ip: '98.76.54.32' },
|
|
{ id: 'log_4', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/tags', method: 'GET', status: 200, credits: 0.5, responseTime: 45, timestamp: '2026-01-31 14:25:18', ip: '123.45.67.89' },
|
|
{ id: 'log_5', keyId: 'key_2', keyName: '点了码-测试环境', endpoint: '/api/shensheshou/user', method: 'GET', status: 403, credits: 0, responseTime: 12, timestamp: '2026-01-31 14:20:33', ip: '98.76.54.32' },
|
|
{ id: 'log_6', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/ai/analyze', method: 'POST', status: 200, credits: 10, responseTime: 5623, timestamp: '2026-01-31 14:15:00', ip: '123.45.67.89' },
|
|
{ id: 'log_7', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/packages/create', method: 'POST', status: 200, credits: 5, responseTime: 1234, timestamp: '2026-01-31 14:10:22', ip: '123.45.67.89' },
|
|
{ id: 'log_8', keyId: 'key_2', keyName: '点了码-测试环境', endpoint: '/api/shensheshou/ingest', method: 'POST', status: 429, credits: 0, responseTime: 8, timestamp: '2026-01-31 14:05:11', ip: '98.76.54.32' },
|
|
]
|
|
|
|
// API分类映射
|
|
const ENDPOINT_CATEGORIES: Record<string, string> = {
|
|
'/api/shensheshou/user': '用户查询',
|
|
'/api/shensheshou/users/batch': '用户查询',
|
|
'/api/shensheshou/tags': '标签服务',
|
|
'/api/shensheshou/tags/apply': '标签服务',
|
|
'/api/shensheshou/ai/chat': 'AI服务',
|
|
'/api/shensheshou/ai/analyze': 'AI服务',
|
|
'/api/shensheshou/ai/tag': 'AI服务',
|
|
'/api/shensheshou/sources': '数据服务',
|
|
'/api/shensheshou/ingest': '数据服务',
|
|
'/api/shensheshou/report/generate': '报告服务',
|
|
'/api/shensheshou/packages': '流量包',
|
|
'/api/shensheshou/packages/create': '流量包',
|
|
'/api/shensheshou/packages/export': '流量包',
|
|
}
|
|
|
|
// GET: 获取调用日志和计费明细
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const { searchParams } = new URL(request.url)
|
|
const action = searchParams.get('action')
|
|
const keyId = searchParams.get('keyId')
|
|
const startDate = searchParams.get('startDate')
|
|
const endDate = searchParams.get('endDate')
|
|
const page = parseInt(searchParams.get('page') || '1')
|
|
const pageSize = parseInt(searchParams.get('pageSize') || '20')
|
|
|
|
// 筛选日志
|
|
let filteredLogs = [...callLogs]
|
|
|
|
if (keyId) {
|
|
filteredLogs = filteredLogs.filter(log => log.keyId === keyId)
|
|
}
|
|
|
|
if (startDate) {
|
|
filteredLogs = filteredLogs.filter(log => log.timestamp >= startDate)
|
|
}
|
|
|
|
if (endDate) {
|
|
filteredLogs = filteredLogs.filter(log => log.timestamp <= endDate)
|
|
}
|
|
|
|
// 获取计费明细
|
|
if (action === 'billing') {
|
|
const categoryStats: Record<string, { count: number; credits: number }> = {}
|
|
let totalCredits = 0
|
|
|
|
filteredLogs.forEach(log => {
|
|
const category = ENDPOINT_CATEGORIES[log.endpoint] || '其他'
|
|
if (!categoryStats[category]) {
|
|
categoryStats[category] = { count: 0, credits: 0 }
|
|
}
|
|
categoryStats[category].count++
|
|
categoryStats[category].credits += log.credits
|
|
totalCredits += log.credits
|
|
})
|
|
|
|
const billingDetails: BillingDetail[] = Object.entries(categoryStats).map(([category, stats]) => ({
|
|
category,
|
|
callCount: stats.count,
|
|
avgPrice: stats.count > 0 ? Number((stats.credits / stats.count).toFixed(2)) : 0,
|
|
totalCredits: stats.credits,
|
|
percentage: totalCredits > 0 ? Number(((stats.credits / totalCredits) * 100).toFixed(1)) : 0
|
|
}))
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: {
|
|
details: billingDetails.sort((a, b) => b.totalCredits - a.totalCredits),
|
|
totalCredits,
|
|
totalCalls: filteredLogs.length,
|
|
period: {
|
|
start: startDate || filteredLogs[filteredLogs.length - 1]?.timestamp?.split(' ')[0],
|
|
end: endDate || filteredLogs[0]?.timestamp?.split(' ')[0]
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 获取统计概览
|
|
if (action === 'stats') {
|
|
const today = new Date().toISOString().split('T')[0]
|
|
const todayLogs = filteredLogs.filter(log => log.timestamp.startsWith(today))
|
|
const successLogs = filteredLogs.filter(log => log.status === 200)
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: {
|
|
today: {
|
|
calls: todayLogs.length,
|
|
credits: todayLogs.reduce((sum, log) => sum + log.credits, 0),
|
|
avgResponseTime: todayLogs.length > 0
|
|
? Math.round(todayLogs.reduce((sum, log) => sum + log.responseTime, 0) / todayLogs.length)
|
|
: 0
|
|
},
|
|
total: {
|
|
calls: filteredLogs.length,
|
|
credits: filteredLogs.reduce((sum, log) => sum + log.credits, 0),
|
|
successRate: filteredLogs.length > 0
|
|
? Number(((successLogs.length / filteredLogs.length) * 100).toFixed(1))
|
|
: 0
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 分页返回日志
|
|
const startIndex = (page - 1) * pageSize
|
|
const paginatedLogs = filteredLogs.slice(startIndex, startIndex + pageSize)
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: paginatedLogs,
|
|
pagination: {
|
|
page,
|
|
pageSize,
|
|
total: filteredLogs.length,
|
|
totalPages: Math.ceil(filteredLogs.length / pageSize)
|
|
}
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error('获取计费信息失败:', error)
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: '获取计费信息失败'
|
|
}, { status: 500 })
|
|
}
|
|
}
|
|
|
|
// POST: 记录API调用
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json()
|
|
const { keyId, keyName, endpoint, method, status, credits, responseTime, ip, requestFields } = body
|
|
|
|
const newLog: CallLog = {
|
|
id: `log_${Date.now()}`,
|
|
keyId,
|
|
keyName,
|
|
endpoint,
|
|
method,
|
|
status,
|
|
credits: credits || 0,
|
|
responseTime: responseTime || 0,
|
|
timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
|
ip: ip || 'unknown',
|
|
requestFields
|
|
}
|
|
|
|
callLogs.unshift(newLog) // 添加到开头
|
|
|
|
// 保持日志数量在合理范围
|
|
if (callLogs.length > 10000) {
|
|
callLogs.splice(10000)
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: newLog
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error('记录调用日志失败:', error)
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: '记录调用日志失败'
|
|
}, { status: 500 })
|
|
}
|
|
}
|