import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { writeFile, mkdir } from 'fs/promises' import path from 'path' import { existsSync } from 'fs' import { sanitizeFileName } from '@/lib/path-security' import { canManageSkuMaterial } from '../authorization' import { assertBoundedMultipartHeaders, assertMultipartFiles } from '@/lib/multipart-request-guard' import { boundedMultipartRequestLimiter, getBoundedMultipartFailure, MULTIPART_ROUTE_LIMITS, } from '@/lib/multipart-route-limits' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' export const maxDuration = 10800 // SKU文件上传API export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { let releaseRequestSlot: (() => void) | null = null try { const session = await getServerSession(authOptions) if (!session?.user || !(session.user as any).id) { return NextResponse.json({ error: '请先登录' }, { status: 401 }) } const materialId = params.id // 检查素材是否存在且是标准模型类型 const material = await prisma.material.findUnique({ where: { id: materialId, deletedAt: null }, select: { id: true, materialType: true, userId: true } }) if (!material) { return NextResponse.json({ error: '素材不存在' }, { status: 404 }) } if (material.materialType !== '标准模型') { return NextResponse.json({ error: '只有标准模型类型才能上传 SKU' }, { status: 400 }) } if (!(await canManageSkuMaterial(session.user as any, material))) { return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 }) } assertBoundedMultipartHeaders(request.headers, MULTIPART_ROUTE_LIMITS.skuArchiveRequestBytes) releaseRequestSlot = await boundedMultipartRequestLimiter.acquire() const formData = await request.formData() assertMultipartFiles(formData, { fields: { file: { maxCount: 1, maxFileBytes: MULTIPART_ROUTE_LIMITS.skuArchiveFileBytes, }, }, maxFiles: 1, maxTotalBytes: MULTIPART_ROUTE_LIMITS.skuArchiveFileBytes, }) const skuName = formData.get('skuName') as string const file = formData.get('file') as File if (!skuName || !file) { return NextResponse.json({ error: '缺少必填字段' }, { status: 400 }) } const archiveExtension = path.extname(path.basename(file.name)).toLowerCase() if (!['.zip', '.rar', '.7z'].includes(archiveExtension)) { return NextResponse.json({ error: '模型压缩包只支持 ZIP、RAR 或 7Z 格式' }, { status: 415 }) } // 创建上传目录 const uploadDir = path.join(process.cwd(), 'uploads', 'skus', materialId) if (!existsSync(uploadDir)) { await mkdir(uploadDir, { recursive: true }) } // 生成文件名 const timestamp = Date.now() const ext = path.extname(path.basename(file.name)) const fileName = `${sanitizeFileName(skuName)}_${timestamp}${ext}` const filePath = path.join(uploadDir, fileName) // 保存文件 const bytes = await file.arrayBuffer() const buffer = Buffer.from(bytes) await writeFile(filePath, buffer) // 相对路径(用于数据库存储) const relativePath = path.join('skus', materialId, fileName).replace(/\\/g, '/') // 统一使用正斜杠 // 只返回文件信息,不创建SKU记录(SKU记录由 /api/materials/[id]/skus POST 接口创建) return NextResponse.json({ fileName: file.name, filePath: relativePath, fileSize: file.size }, { status: 200 }) } catch (error) { console.error('上传 SKU 失败:', error) const boundedFailure = getBoundedMultipartFailure(error) if (boundedFailure) { return NextResponse.json( { error: boundedFailure.message, code: boundedFailure.code }, { status: boundedFailure.status, ...(boundedFailure.retryAfterSeconds ? { headers: { 'Retry-After': String(boundedFailure.retryAfterSeconds) } } : {}), }, ) } return NextResponse.json({ error: '上传 SKU 失败' }, { status: 500 }) } finally { releaseRequestSlot?.() } }