chenpei 752b7bf999 fix: 修复批量上传和视频扩展名问题
主要修复:

- 修复批量上传闭包陷阱,防止文件丢失

- 添加 fileName 参数传递原始文件名,保留正确扩展名

- 添加取消检查回调,改进批量上传取消功能

其他改进:

- 优化搜索功能和管理后台

- 添加 FRP 故障诊断文档

- 更新环境验证和网络检测
2026-02-03 11:12:50 +08:00

165 lines
5.7 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 { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { isSystemAdmin } from '@/lib/department-permission'
import { deleteFiles } from '@/lib/file-cleanup'
import { collectMaterialFiles, deleteMaterialRelatedRecords } from '@/lib/material-cleanup'
// UUID 格式校验正则
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
/**
* 批量删除素材(软删除或永久删除)
*
* 改进点:
* 1. 使用 isSystemAdmin() 统一权限校验
* 2. 永久删除时会物理删除 S3/本地文件
* 3. 永久删除时会删除关联记录(MaterialAsset, MaterialSku)
* 4. 增加 ID 格式校验
*/
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
// 使用统一的权限检查函数(与单条删除保持一致)
if (!session || !isSystemAdmin((session.user as any)?.role)) {
return NextResponse.json({ error: '无权限,需要系统管理员权限' }, { status: 403 })
}
const { ids, permanent } = await request.json()
// 校验 ids 是数组且非空
if (!Array.isArray(ids) || ids.length === 0) {
return NextResponse.json({ error: '请提供要删除的素材ID列表' }, { status: 400 })
}
// 校验每个 ID 的格式(防止注入)
const invalidIds = ids.filter(id => typeof id !== 'string' || !UUID_REGEX.test(id))
if (invalidIds.length > 0) {
return NextResponse.json({
error: `存在无效的素材ID格式: ${invalidIds.slice(0, 3).join(', ')}${invalidIds.length > 3 ? '...' : ''}`
}, { status: 400 })
}
if (permanent) {
// ========== 永久删除:物理删除文件 + 删除数据库记录 ==========
console.log(`[BatchDelete] 开始永久删除 ${ids.length} 条素材...`)
// 1. 先查询所有素材(获取文件路径和素材类型)
const materials = await prisma.material.findMany({
where: { id: { in: ids } },
})
if (materials.length === 0) {
return NextResponse.json({ error: '未找到要删除的素材' }, { status: 404 })
}
// 2. 收集所有文件并删除
let totalFilesDeleted = 0
let totalFilesFailed = 0
const allErrors: Array<{ materialId: string; fileUrl: string; error: string }> = []
for (const material of materials) {
const materialType = (material as any).materialType || ''
// 收集该素材的所有文件
const fileInfo = await collectMaterialFiles(material.id, material)
const filesToDelete = [...fileInfo.fileUrls, ...fileInfo.directories]
console.log(`[BatchDelete] Material ${material.id} (${materialType}):`, {
fileUrls: fileInfo.fileUrls.length,
directories: fileInfo.directories.length,
relatedRecords: fileInfo.relatedRecords,
})
// 删除文件
if (filesToDelete.length > 0) {
const result = await deleteFiles(filesToDelete)
totalFilesDeleted += result.deletedCount
totalFilesFailed += result.failedCount
if (result.errors.length > 0) {
allErrors.push(...result.errors.map(e => ({
materialId: material.id,
fileUrl: e.fileUrl,
error: e.error,
})))
}
}
// 删除关联记录(MaterialAsset, MaterialSku)
if (materialType) {
await deleteMaterialRelatedRecords(material.id, materialType)
}
}
// 3. 删除数据库记录
const deleteResult = await prisma.material.deleteMany({
where: { id: { in: ids } }
})
console.log(`[BatchDelete] 永久删除完成:`, {
requestedCount: ids.length,
deletedRecords: deleteResult.count,
filesDeleted: totalFilesDeleted,
filesFailed: totalFilesFailed,
errorCount: allErrors.length,
})
// 如果有文件删除失败,记录警告但不影响整体结果
if (allErrors.length > 0) {
console.warn(`[BatchDelete] 部分文件删除失败:`, allErrors.slice(0, 5))
}
return NextResponse.json({
success: true,
message: `批量永久删除成功`,
details: {
deletedRecords: deleteResult.count,
filesDeleted: totalFilesDeleted,
filesFailed: totalFilesFailed,
}
})
} else {
// ========== 软删除:仅标记 deletedAt ==========
// 查询素材(需要处理任务号)
const materials = await prisma.material.findMany({
where: { id: { in: ids } },
select: { id: true, taskNo: true }
})
if (materials.length === 0) {
return NextResponse.json({ error: '未找到要删除的素材' }, { status: 404 })
}
const now = Date.now()
await Promise.all(materials.map((m, index) =>
prisma.material.update({
where: { id: m.id },
data: {
deletedAt: new Date(),
// 如果有任务号,加上删除时间戳后缀使其作废(防止重复)
...(m.taskNo ? { taskNo: `${m.taskNo}_DELETED_${now}_${index}` } : {})
}
})
))
console.log(`[BatchDelete] 软删除完成: ${materials.length} 条素材`)
return NextResponse.json({
success: true,
message: '批量删除成功(已移入回收站)',
details: {
deletedRecords: materials.length,
}
})
}
} catch (error) {
console.error('[BatchDelete] Error:', error)
return NextResponse.json({ error: '批量删除失败' }, { status: 500 })
}
}