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' import { createMaterialLog } from '@/lib/material-log' import { ensureMaterialDownloadLogsTable } from '@/lib/material-download-log' // 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() const operatorId = (session.user as any).id if (!operatorId) { return NextResponse.json({ error: '缺少操作人信息' }, { status: 401 }) } // 校验 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} 条素材...`) // 确保下载日志表已应用“删除素材后保留日志”策略 await ensureMaterialDownloadLogsTable() // 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 || '' // 记录永久删除日志(先记录,避免数据库删除后日志级联删除前丢失上下文) await createMaterialLog({ materialId: material.id, userId: operatorId, action: 'DELETE', changes: { materialSnapshot: { oldValue: null, newValue: { id: material.id, productModel: material.productModel, materialType: materialType || null, }, }, deletedAt: { oldValue: material.deletedAt ? material.deletedAt.toISOString() : null, newValue: 'PERMANENTLY_DELETED', }, deleteMode: { oldValue: material.deletedAt ? 'soft' : 'active', newValue: 'permanent', }, materialType: { oldValue: materialType || null, newValue: materialType || null, }, }, }) // 收集该素材的所有文件 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, productModel: true, materialType: true } }) if (materials.length === 0) { return NextResponse.json({ error: '未找到要删除的素材' }, { status: 404 }) } const deletedAt = new Date() await prisma.$transaction([ prisma.material.updateMany({ where: { id: { in: materials.map((m) => m.id) } }, data: { deletedAt, }, }), prisma.materialSku.updateMany({ where: { materialId: { in: materials.map((m) => m.id) }, deletedAt: null, }, data: { deletedAt, }, }), ]) await Promise.all( materials.map((material) => createMaterialLog({ materialId: material.id, userId: operatorId, action: 'DELETE', changes: { materialSnapshot: { oldValue: null, newValue: { id: material.id, productModel: material.productModel, materialType: material.materialType, }, }, deletedAt: { oldValue: null, newValue: deletedAt.toISOString(), }, deleteMode: { oldValue: 'active', newValue: 'soft', }, }, }) ) ) 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 }) } }