import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import os from 'os' import { getAllSessions } from '@/lib/upload-sessions-redis' import { transcodeQueue } from '@/lib/video-transcode-queue' import { prisma } from '@/lib/prisma' import { getNetworkSpeed } from '@/lib/network-bandwidth-monitor' import { authOptions } from '@/lib/auth' import { hasSystemAdminPermission } from '@/lib/department-permission' import { classifyTranscodeMaterial, classifyUploadSession, readProcessMemory, } from '@/lib/system-performance' const RATE_LIMIT_WINDOW_MS = 60 * 1000 const RATE_LIMIT_MAX_REQUESTS = 40 const MAX_VISIBLE_UPLOAD_SESSIONS = 100 const requestCounts = new Map() function consumeRateLimit(userId: string): number | null { const now = Date.now() const current = requestCounts.get(userId) if (!current || current.resetAt <= now) { requestCounts.set(userId, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS }) return null } if (current.count >= RATE_LIMIT_MAX_REQUESTS) { return Math.max(1, Math.ceil((current.resetAt - now) / 1000)) } current.count += 1 return null } // 性能监控 - 快速统计接口(用于悬停卡片) // GET /api/performance/quick export const dynamic = 'force-dynamic' export const revalidate = 0 // 获取CPU使用率(正确的方法:需要两次采样) async function getCpuUsage(): Promise { try { const snapshot = () => os.cpus().map((cpu) => cpu.times) const before = snapshot() await new Promise((resolve) => setTimeout(resolve, 200)) const after = snapshot() let totalDiff = 0 let idleDiff = 0 for (let index = 0; index < Math.min(before.length, after.length); index += 1) { const previous = before[index] const current = after[index] if (!previous || !current) { continue } const previousTotal = previous.user + previous.nice + previous.sys + previous.idle + previous.irq const currentTotal = current.user + current.nice + current.sys + current.idle + current.irq totalDiff += Math.max(0, currentTotal - previousTotal) idleDiff += Math.max(0, current.idle - previous.idle) } if (totalDiff > 0) { const usedDiff = Math.max(0, totalDiff - idleDiff) const cpuUsage = (usedDiff / totalDiff) * 100 return Math.min(Math.max(Math.round(cpuUsage), 0), 100) } } catch (error) { console.warn('[PerformanceQuick] Failed to sample CPU usage:', error) } // 降级方案:使用负载平均值估算(不准确,但总比没有好) const loadAvg = os.loadavg()[0] const cpuCount = os.cpus().length // 负载平均值反映系统负载,但不等同于CPU使用率 // 负载平均值 = CPU核心数时,表示CPU满载 // 这里做一个粗略估算:负载平均值 / CPU核心数 * 100,但限制在100%以内 const estimatedUsage = Math.min(Math.round((loadAvg / cpuCount) * 100), 100) console.warn(`[PerformanceQuick] Using loadavg estimation: load=${loadAvg}, cores=${cpuCount}, estimated=${estimatedUsage}%`) return estimatedUsage } export async function GET(request: NextRequest) { try { const session = await getServerSession(authOptions) if (!session?.user) { return NextResponse.json({ error: '请先登录' }, { status: 401 }) } const user = session.user as any if (!hasSystemAdminPermission(user.role, Boolean(user.isSuperAdmin))) { return NextResponse.json({ error: '仅系统管理员可查看性能数据' }, { status: 403 }) } const rateLimitKey = `${String(user.id || 'unknown')}:${request.headers.get('x-forwarded-for') || ''}` const retryAfter = consumeRateLimit(rateLimitKey) if (retryAfter) { return NextResponse.json( { error: '请求过于频繁,请稍后再试' }, { status: 429, headers: { 'Retry-After': String(retryAfter) } }, ) } const loadAvg = os.loadavg() const cpuCores = Math.max(1, os.cpus().length) const [cpuUsage, memoryStats] = await Promise.all([getCpuUsage(), readProcessMemory()]) const cpu = { usage: cpuUsage, loadAverage: loadAvg, cores: cpuCores, } const memory = { used: Math.round(memoryStats.usedBytes / (1024 * 1024)), total: Math.round(memoryStats.totalBytes / (1024 * 1024)), percentage: memoryStats.percentage, usedBytes: memoryStats.usedBytes, totalBytes: memoryStats.totalBytes, source: memoryStats.source, hostUsedBytes: memoryStats.hostUsedBytes, hostTotalBytes: memoryStats.hostTotalBytes, hostPercentage: memoryStats.hostPercentage, } const networkSpeed = await getNetworkSpeed() const network = { uploadSpeed: networkSpeed?.uploadSpeed || 0, downloadSpeed: networkSpeed?.downloadSpeed || 0, interface: networkSpeed?.interface || 'unknown', scope: 'app-container' as const, } // 2. 上传队列(从 Redis 获取,只显示真正活跃的上传) const uploadProcessing: any[] = [] const uploadWaiting: any[] = [] const uploadUserIds = new Set() const uploadIds = new Set() // 从 Redis 获取所有上传会话 const uploadSessions = await getAllSessions() // 只显示真正进行中的上传(进度 < 100% 且最近有活动) const now = Date.now() // 说明: // - lastActivityAt:每个 chunk 上传完成后更新(更能代表“是否真的在上传”) // - createdAt:只能代表会话创建时间,不能代表持续活跃 const ACTIVE_THRESHOLD = 5 * 60 * 1000 // 5分钟内有活动 → 认为“活跃” const MAX_SHOW_THRESHOLD = 60 * 60 * 1000 // 超过1小时无活动 → 不再展示(避免长期误导) Array.from(uploadSessions.entries()) .sort(([, left], [, right]) => { const leftActivity = left.lastActivityAt || left.createdAt || 0 const rightActivity = right.lastActivityAt || right.createdAt || 0 return rightActivity - leftActivity }) .slice(0, MAX_VISIBLE_UPLOAD_SESSIONS) .forEach(([uploadId, session]) => { const completedChunks = session.chunks.size const totalChunks = session.totalChunks || 1 const progress = Math.min( 100, Math.round((completedChunks / totalChunks) * 100) ) const lastActivityAt = (session as any).lastActivityAt || session.createdAt || 0 const inactiveMs = lastActivityAt > 0 ? now - lastActivityAt : Number.POSITIVE_INFINITY const isActive = inactiveMs >= 0 && inactiveMs < ACTIVE_THRESHOLD const bucket = classifyUploadSession({ progress, inactiveMs, activeThresholdMs: ACTIVE_THRESHOLD, maxShowThresholdMs: MAX_SHOW_THRESHOLD, }) if (bucket === 'hidden') { return } uploadUserIds.add(session.userId) uploadIds.add(uploadId) const item = { uploadId, fileName: session.fileName, fileSize: session.fileSize || 0, progress, userId: session.userId, currentSpeed: (session as any).currentSpeed || 0, uploadedBytes: (session as any).uploadedBytes || 0, lastActivityAt, inactiveMs, isActive, } if (bucket === 'processing') { uploadProcessing.push(item) } else { uploadWaiting.push(item) } }) // 批量查询用户信息 const userInfoMap = new Map() if (uploadUserIds.size > 0) { try { const users = await prisma.user.findMany({ where: { id: { in: Array.from(uploadUserIds) }, } as any, select: { id: true, username: true, realName: true, } as any, }) users.forEach((user: any) => { userInfoMap.set(user.id, { username: user.username || '未知', realName: user.realName || undefined, }) }) } catch (error) { console.error('[PerformanceQuick] Failed to fetch user info:', error) } } // 尝试通过uploadId查询已创建的material记录(获取materialType) const uploadMaterialInfoMap = new Map() if (uploadIds.size > 0) { try { const materials = await prisma.material.findMany({ where: { uploadId: { in: Array.from(uploadIds) }, deletedAt: null, } as any, select: { uploadId: true, materialType: true, } as any, }) materials.forEach((m: any) => { if (m.uploadId) { uploadMaterialInfoMap.set(m.uploadId, { materialType: m.materialType || '未知类型', }) } }) } catch (error) { console.error('[PerformanceQuick] Failed to fetch upload material info:', error) } } const decorateUploadTask = (task: any) => { const userInfo = userInfoMap.get(task.userId) || { username: '未知用户' } const materialInfo = uploadMaterialInfoMap.get(task.uploadId) || { materialType: '未知类型' } return { ...task, username: userInfo.realName || userInfo.username || '未知用户', materialType: materialInfo.materialType, } } const uploadProcessingWithUsers = uploadProcessing.map(decorateUploadTask) const uploadWaitingWithUsers = uploadWaiting.map(decorateUploadTask) const uploadQueue = { processing: uploadProcessingWithUsers, waiting: uploadWaitingWithUsers, total: uploadProcessingWithUsers.length + uploadWaitingWithUsers.length, } const status = transcodeQueue.getStatus() const progressObj: Record = {} const stageObj: Record = {} status.progress.forEach((value, key) => { progressObj[key] = value }) status.stages.forEach((value, key) => { stageObj[key] = value }) const transcodeMaterials = await prisma.material.findMany({ where: { deletedAt: null, videoProcessingStatus: { in: ['pending', 'processing'] }, }, select: { id: true, zipFileName: true, materialType: true, authorName: true, videoProcessingStatus: true, transcodeLeaseExpiresAt: true, user: { select: { username: true, }, }, }, orderBy: { updatedAt: 'asc' }, take: 80, }) const transcodeNow = new Date() const transcodeProcessing: any[] = [] const transcodeWaiting: any[] = [] transcodeMaterials.forEach((material) => { const bucket = classifyTranscodeMaterial({ status: material.videoProcessingStatus, leaseExpiresAt: material.transcodeLeaseExpiresAt, now: transcodeNow, }) if (bucket === 'hidden') return const item = { materialId: material.id, fileName: material.zipFileName || '未知文件', materialType: material.materialType || '未知类型', authorName: material.authorName || material.user?.username || '未知用户', progress: progressObj[material.id] ?? null, stage: stageObj[material.id] ?? null, } if (bucket === 'processing') { transcodeProcessing.push(item) } else { transcodeWaiting.push({ ...item, queuePosition: transcodeWaiting.length + 1, }) } }) const transcodeQueueData = { processing: transcodeProcessing, waiting: transcodeWaiting, total: transcodeProcessing.length + transcodeWaiting.length, maxConcurrent: status.maxConcurrent, } return NextResponse.json({ cpu, memory, network, uploadQueue, transcodeQueue: transcodeQueueData, }) } catch (error) { console.error('[PerformanceQuick] 获取性能数据失败:', error) return NextResponse.json( { error: '获取性能数据失败' }, { status: 500 } ) } }