chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

131 lines
4.1 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 { 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 })
}
}