190 lines
5.6 KiB
TypeScript
190 lines
5.6 KiB
TypeScript
/**
|
||
* 视频转码统计 API
|
||
*
|
||
* GET: 获取视频转码队列状况、失败任务等统计数据
|
||
*
|
||
* 参考:flnxi_sucai/app/api/admin/video-transcode-stats/route.ts
|
||
* 适配当前系统使用 materialType 字段
|
||
*/
|
||
|
||
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getServerSession } from 'next-auth'
|
||
import { authOptions } from '@/lib/auth'
|
||
import { prisma } from '@/lib/prisma'
|
||
import { LEGACY_COMPATIBLE_VIDEO_MATERIAL_TYPES } from '@/lib/video-material-types'
|
||
|
||
export async function GET(req: NextRequest) {
|
||
try {
|
||
const session = await getServerSession(authOptions)
|
||
|
||
// 检查管理员权限
|
||
if (!session || (session.user as any).role !== 'ADMIN') {
|
||
return NextResponse.json(
|
||
{ error: '无权限' },
|
||
{ status: 403 }
|
||
)
|
||
}
|
||
|
||
// 识别视频素材类型
|
||
const videoMaterialTypes = [...LEGACY_COMPATIBLE_VIDEO_MATERIAL_TYPES]
|
||
|
||
// 获取视频素材统计(兼容旧数据库)
|
||
let videoStats: any = {}
|
||
try {
|
||
// 查询所有视频素材
|
||
const videoMaterials = await prisma.material.findMany({
|
||
where: {
|
||
AND: [
|
||
{
|
||
materialType: {
|
||
in: videoMaterialTypes,
|
||
},
|
||
},
|
||
{
|
||
deletedAt: null, // 只统计未删除的素材
|
||
} as any,
|
||
],
|
||
} as any,
|
||
select: {
|
||
id: true,
|
||
materialType: true,
|
||
videoProcessingStatus: true,
|
||
transcodeLastError: true,
|
||
transcodeAttempts: true,
|
||
createdAt: true,
|
||
} as any, // 使用 as any 兼容旧数据库
|
||
})
|
||
|
||
// 按转码状态分组统计
|
||
const statusCounts = {
|
||
pending: 0, // 待转码
|
||
processing: 0, // 转码中
|
||
completed: 0, // 转码完成
|
||
failed: 0, // 转码失败
|
||
unknown: 0, // 状态未知
|
||
}
|
||
|
||
const failedTasks: any[] = []
|
||
const recentTasks: any[] = []
|
||
|
||
videoMaterials.forEach((material: any) => {
|
||
const status = material.videoProcessingStatus || 'unknown'
|
||
|
||
switch (status) {
|
||
case 'pending':
|
||
case 'queued':
|
||
statusCounts.pending++
|
||
break
|
||
case 'processing':
|
||
case 'transcoding':
|
||
statusCounts.processing++
|
||
break
|
||
case 'completed':
|
||
case 'success':
|
||
statusCounts.completed++
|
||
break
|
||
case 'failed':
|
||
case 'error':
|
||
statusCounts.failed++
|
||
failedTasks.push({
|
||
id: material.id,
|
||
materialType: material.materialType,
|
||
error: material.transcodeLastError,
|
||
attempts: material.transcodeAttempts || 0,
|
||
createdAt: material.createdAt,
|
||
})
|
||
break
|
||
default:
|
||
statusCounts.unknown++
|
||
}
|
||
|
||
// 收集最近的任务(最近24小时)
|
||
const oneDayAgo = new Date()
|
||
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
|
||
|
||
if (material.createdAt && new Date(material.createdAt) > oneDayAgo) {
|
||
recentTasks.push({
|
||
id: material.id,
|
||
materialType: material.materialType,
|
||
status: material.videoProcessingStatus || 'unknown',
|
||
createdAt: material.createdAt,
|
||
})
|
||
}
|
||
})
|
||
|
||
videoStats = {
|
||
total: videoMaterials.length,
|
||
byStatus: statusCounts,
|
||
byType: videoMaterialTypes.reduce((acc, type) => {
|
||
acc[type] = videoMaterials.filter((m: any) => m.materialType === type).length
|
||
return acc
|
||
}, {} as Record<string, number>),
|
||
failedTasks: failedTasks.slice(0, 10), // 最多返回10个失败任务
|
||
recentTasks: recentTasks.slice(0, 20), // 最多返回20个最近任务
|
||
}
|
||
} catch (error: any) {
|
||
// 兼容旧数据库:如果视频相关字段不存在
|
||
if (error?.meta?.column?.includes('videoProcessingStatus') ||
|
||
error?.meta?.column?.includes('deletedAt')) {
|
||
console.warn('[Admin] Video fields not found, using fallback query')
|
||
|
||
// 降级查询:只统计视频素材数量
|
||
const videoMaterials = await prisma.material.findMany({
|
||
where: {
|
||
materialType: {
|
||
in: videoMaterialTypes,
|
||
},
|
||
},
|
||
select: {
|
||
id: true,
|
||
materialType: true,
|
||
createdAt: true,
|
||
},
|
||
})
|
||
|
||
videoStats = {
|
||
total: videoMaterials.length,
|
||
byStatus: {
|
||
pending: 0,
|
||
processing: 0,
|
||
completed: videoMaterials.length, // 假设都已完成
|
||
failed: 0,
|
||
unknown: 0,
|
||
},
|
||
byType: videoMaterialTypes.reduce((acc, type) => {
|
||
acc[type] = videoMaterials.filter(m => m.materialType === type).length
|
||
return acc
|
||
}, {} as Record<string, number>),
|
||
failedTasks: [],
|
||
recentTasks: videoMaterials.slice(0, 20).map(m => ({
|
||
id: m.id,
|
||
materialType: m.materialType,
|
||
status: 'completed',
|
||
createdAt: m.createdAt,
|
||
})),
|
||
}
|
||
} else {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 系统资源使用情况
|
||
const systemResources = {
|
||
memory: process.memoryUsage(),
|
||
uptime: process.uptime(),
|
||
timestamp: new Date().toISOString(),
|
||
}
|
||
|
||
return NextResponse.json({
|
||
video: videoStats,
|
||
system: systemResources,
|
||
})
|
||
} catch (error) {
|
||
console.error('[Admin] Video transcode stats error:', error)
|
||
return NextResponse.json(
|
||
{ error: '获取转码统计失败' },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|