chenpei 0ce59be34e 备份:组织架构设计方案讨论前的代码快照
- 包含所有当前修改和新功能
- 准备开始组织架构优化实施方案
2025-12-30 13:20:26 +08:00

242 lines
7.9 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 { NextResponse } from 'next/server'
import os from 'os'
import { exec } from 'child_process'
import { promisify } from 'util'
import { uploadSessions } from '@/lib/upload-sessions'
import { transcodeQueue } from '@/lib/video-transcode-queue'
import { prisma } from '@/lib/prisma'
const execAsync = promisify(exec)
// 性能监控 - 快速统计接口(用于悬停卡片)
// GET /api/performance/quick
export const dynamic = 'force-dynamic'
export const revalidate = 0
// 获取CPU使用率(正确的方法:需要两次采样)
async function getCpuUsage(): Promise<number> {
try {
// 方法1:使用 /proc/stat 两次采样计算CPU使用率(最准确)
const getCpuStats = async () => {
const { stdout } = await execAsync("cat /proc/stat | grep '^cpu '")
const parts = stdout.trim().split(/\s+/)
return {
user: parseInt(parts[1]) || 0,
nice: parseInt(parts[2]) || 0,
system: parseInt(parts[3]) || 0,
idle: parseInt(parts[4]) || 0,
iowait: parseInt(parts[5]) || 0,
irq: parseInt(parts[6]) || 0,
softirq: parseInt(parts[7]) || 0,
}
}
const stats1 = await getCpuStats()
await new Promise(resolve => setTimeout(resolve, 1000)) // 等待1秒
const stats2 = await getCpuStats()
const total1 = stats1.user + stats1.nice + stats1.system + stats1.idle + stats1.iowait + stats1.irq + stats1.softirq
const total2 = stats2.user + stats2.nice + stats2.system + stats2.idle + stats2.iowait + stats2.irq + stats2.softirq
const totalDiff = total2 - total1
if (totalDiff > 0) {
const idleDiff = stats2.idle - stats1.idle
const usedDiff = totalDiff - idleDiff
const cpuUsage = (usedDiff / totalDiff) * 100
return Math.min(Math.max(Math.round(cpuUsage), 0), 100)
}
} catch (error) {
console.warn('[PerformanceQuick] Failed to get CPU usage from /proc/stat:', error)
}
try {
// 方法2:尝试使用 top 命令(批处理模式,更准确)
const { stdout } = await execAsync("top -bn2 -d1 | grep 'Cpu(s)' | tail -1 | awk '{print $2}' | sed 's/%us,//'", { timeout: 3000 })
const cpuUsage = parseFloat(stdout.trim())
if (!isNaN(cpuUsage) && cpuUsage >= 0 && cpuUsage <= 100) {
return Math.round(cpuUsage)
}
} catch (error) {
console.warn('[PerformanceQuick] Failed to get CPU usage from top:', error)
}
try {
// 方法3:使用 vmstat(如果可用)
const { stdout } = await execAsync("vmstat 1 2 | tail -1 | awk '{print 100-$15}'", { timeout: 3000 })
const cpuUsage = parseFloat(stdout.trim())
if (!isNaN(cpuUsage) && cpuUsage >= 0 && cpuUsage <= 100) {
return Math.round(cpuUsage)
}
} catch (error) {
console.warn('[PerformanceQuick] Failed to get CPU usage from vmstat:', 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() {
try {
// 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,
}
// 2. 上传队列(基于 uploadSessions)
const uploadProcessing: any[] = []
const uploadWaiting: any[] = []
// 只显示真正进行中的上传(进度 < 100%)
uploadSessions.forEach((session, uploadId) => {
const completedChunks = session.chunks.size
const totalChunks = session.totalChunks || 1
const progress = Math.min(
100,
Math.round((completedChunks / totalChunks) * 100)
)
// 只显示未完成的上传(进度 < 100%)
if (progress < 100) {
uploadProcessing.push({
uploadId,
fileName: session.fileName,
materialType: '未知', // 目前 UploadSession 未区分素材类型,后续可扩展
progress,
userId: session.userId,
})
}
})
const uploadQueue = {
processing: uploadProcessing,
waiting: uploadWaiting,
total: uploadProcessing.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,
uploadQueue,
transcodeQueue: transcodeQueueData,
})
} catch (error) {
console.error('[PerformanceQuick] 获取性能数据失败:', error)
return NextResponse.json(
{ error: '获取性能数据失败' },
{ status: 500 }
)
}
}