Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
131 lines
4.1 KiB
TypeScript
131 lines
4.1 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 { createMaterialLog } from '@/lib/material-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) {
|
||
return NextResponse.json({
|
||
error: '批量永久删除已暂停:请使用可恢复删除;待持久化清理任务上线后再开放物理删除',
|
||
code: 'PERMANENT_DELETE_DISABLED',
|
||
}, { status: 409 })
|
||
|
||
} 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 })
|
||
}
|
||
}
|