主要修复: - 修复批量上传闭包陷阱,防止文件丢失 - 添加 fileName 参数传递原始文件名,保留正确扩展名 - 添加取消检查回调,改进批量上传取消功能 其他改进: - 优化搜索功能和管理后台 - 添加 FRP 故障诊断文档 - 更新环境验证和网络检测
165 lines
5.7 KiB
TypeScript
165 lines
5.7 KiB
TypeScript
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 })
|
||
}
|
||
}
|