361 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'
const RATE_LIMIT_WINDOW_MS = 60 * 1000
const RATE_LIMIT_MAX_REQUESTS = 40
const MAX_VISIBLE_UPLOAD_SESSIONS = 100
const requestCounts = new Map<string, { count: number; resetAt: number }>()
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<number> {
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) } },
)
}
// 1. 系统性能(先用 Node.js os 模块,后续可接 TrueNAS)
const loadAvg = os.loadavg() // [1min, 5min, 15min]
const totalMem = os.totalmem()
const freeMem = os.freemem()
const usedMem = totalMem - freeMem
const memPercentage = totalMem > 0 ? Math.round((usedMem / totalMem) * 100) : 0
// 获取更准确的CPU使用率
const cpuUsage = await getCpuUsage()
const cpu = {
usage: cpuUsage,
loadAverage: loadAvg,
}
const memory = {
used: Math.round(usedMem / (1024 * 1024)), // MB
total: Math.round(totalMem / (1024 * 1024)), // MB
percentage: memPercentage,
}
// 获取网络带宽使用情况
const networkSpeed = await getNetworkSpeed()
const network = {
uploadSpeed: networkSpeed?.uploadSpeed || 0, // 字节/秒
downloadSpeed: networkSpeed?.downloadSpeed || 0, // 字节/秒
interface: networkSpeed?.interface || 'unknown', // 使用的网卡
}
// 2. 上传队列(从 Redis 获取,只显示真正活跃的上传)
const uploadProcessing: any[] = []
const uploadWaiting: any[] = []
const uploadUserIds = new Set<string>()
const uploadIds = new Set<string>()
// 从 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 isTooOld = inactiveMs >= MAX_SHOW_THRESHOLD
// 只展示:未完成 + 未超过“最大无活动时间”
// 注意:即使不是“活跃”,也会展示(并返回 inactiveMs),用于解释“队列里有任务但网速很低”的情况
if (progress < 100) {
if (!isTooOld) {
uploadUserIds.add(session.userId)
uploadIds.add(uploadId)
uploadProcessing.push({
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,
})
}
}
})
// 批量查询用户信息
const userInfoMap = new Map<string, { username: string; realName?: string }>()
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<string, { materialType: string }>()
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 uploadProcessingWithUsers = uploadProcessing.map((task) => {
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 uploadQueue = {
processing: uploadProcessingWithUsers,
waiting: uploadWaiting,
total: uploadProcessingWithUsers.length + uploadWaiting.length,
}
// 3. 转码队列(基于 transcodeQueue)
const status = transcodeQueue.getStatus()
const progressObj: Record<string, number> = {}
const stageObj: Record<string, string> = {}
// Map 无法直接序列化,转成普通对象
status.progress.forEach((value, key) => {
progressObj[key] = value
})
status.stages.forEach((value, key) => {
stageObj[key] = value
})
// 查询转码队列中的素材信息(文件名、类型、上传人等)
const processingMaterialIds = status.processingIds
const waitingMaterialIds = status.waiting
const allMaterialIds = [...processingMaterialIds, ...waitingMaterialIds]
const materialInfoMap = new Map<string, any>()
if (allMaterialIds.length > 0) {
try {
const materials = await prisma.material.findMany({
where: {
id: { in: allMaterialIds },
deletedAt: null,
} as any,
select: {
id: true,
zipFileName: true,
materialType: true,
authorName: true,
user: {
select: {
username: true,
},
},
} as any,
})
materials.forEach((m: any) => {
materialInfoMap.set(m.id, {
fileName: m.zipFileName || '未知文件',
materialType: m.materialType || '未知类型',
authorName: m.authorName || m.user?.username || '未知用户',
})
})
} catch (error) {
console.error('[PerformanceQuick] Failed to fetch material info:', error)
}
}
const transcodeQueueData = {
processing: processingMaterialIds.map((id) => {
const info = materialInfoMap.get(id) || {
fileName: '未知文件',
materialType: '未知类型',
authorName: '未知用户',
}
return {
materialId: id,
fileName: info.fileName,
materialType: info.materialType,
authorName: info.authorName,
progress: progressObj[id] ?? null,
stage: stageObj[id] ?? null,
}
}),
waiting: waitingMaterialIds.map((id, index) => {
const info = materialInfoMap.get(id) || {
fileName: '未知文件',
materialType: '未知类型',
authorName: '未知用户',
}
return {
materialId: id,
fileName: info.fileName,
materialType: info.materialType,
authorName: info.authorName,
queuePosition: index + 1,
}
}),
total: status.queueLength,
maxConcurrent: status.maxConcurrent,
}
return NextResponse.json({
cpu,
memory,
network,
uploadQueue,
transcodeQueue: transcodeQueueData,
})
} catch (error) {
console.error('[PerformanceQuick] 获取性能数据失败:', error)
return NextResponse.json(
{ error: '获取性能数据失败' },
{ status: 500 }
)
}
}