import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { updateMaterialWithSharedRules, type MaterialUpdateInput } from '@/lib/material-update'; import { isWebFileLike } from '@/lib/web-file'; import { assertBoundedMultipartHeaders, assertMultipartFiles } from '@/lib/multipart-request-guard'; import { boundedMultipartRequestLimiter, getBoundedMultipartFailure, MULTIPART_ROUTE_LIMITS, } from '@/lib/multipart-route-limits'; // PUT /api/materials/update - 更新素材(包括预览图) export async function PUT(request: NextRequest) { let releaseRequestSlot: (() => void) | null = null; try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json({ error: '请先登录' }, { status: 401 }); } const contentType = request.headers.get('content-type') || ''; let id = ''; let input: MaterialUpdateInput; let previewImage: File | null = null; if (contentType.includes('multipart/form-data')) { assertBoundedMultipartHeaders(request.headers, MULTIPART_ROUTE_LIMITS.materialUpdateRequestBytes); releaseRequestSlot = await boundedMultipartRequestLimiter.acquire(); const formData = await request.formData(); assertMultipartFiles(formData, { fields: { previewImage: { maxCount: 1, maxFileBytes: MULTIPART_ROUTE_LIMITS.materialUpdatePreviewBytes, allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif'], }, }, maxFiles: 1, maxTotalBytes: MULTIPART_ROUTE_LIMITS.materialUpdatePreviewBytes, }); id = String(formData.get('id') || ''); input = { productModel: String(formData.get('productModel') || ''), ...(formData.has('reportTitle') ? { reportTitle: formData.get('reportTitle') as string | null } : {}), categoryName: formData.get('categoryName') as string | null, sceneName: formData.get('sceneName') as string | null, platformName: formData.get('platformName') as string | null, productCategoryName: formData.get('productCategoryName') as string | null, materialType: String(formData.get('materialType') || ''), renderer: formData.get('renderer') as string | null, authorName: String(formData.get('authorName') || ''), ...(formData.has('userId') ? { userId: formData.get('userId') as string | null } : {}), ...(formData.has('taskNo') ? { taskNo: formData.get('taskNo') as string | null } : {}), ...(formData.has('dingtalkApprovalNo') ? { dingtalkApprovalNo: formData.get('dingtalkApprovalNo') as string | null } : {}), ...(formData.has('taskDetailSourceType') ? { taskDetailSourceType: formData.get('taskDetailSourceType') as string | null } : {}), ...(formData.has('taskDetailRequesterUserId') ? { taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null } : {}), ...(formData.has('isFullSet') ? { isFullSet: formData.get('isFullSet') as string | null } : {}), }; const formPreview = formData.get('previewImage'); previewImage = isWebFileLike(formPreview) ? formPreview : null; } else { const json = await request.json(); id = String(json.id || ''); input = json; } if (!id) { return NextResponse.json({ error: '缺少素材ID' }, { status: 400 }); } const user = session.user as any; const result = await updateMaterialWithSharedRules({ materialId: id, operatorUserId: user.id, operatorRole: user.role, input, previewImage, }); if ('error' in result) { return NextResponse.json({ error: result.error }, { status: result.status }); } const response = { ...result.material, zipFileSize: result.material.zipFileSize.toString(), isFullSet: Boolean((result.material as any).isFullSet), taskDetailSourceType: (result.material as any).taskDetailSourceType || null, taskDetailRequesterUserId: (result.material as any).taskDetailRequesterUserId || null, taskDetailRequesterName: (result.material as any).taskDetailRequesterName || null, }; return NextResponse.json( { message: '更新成功', material: response }, { status: 200 } ); } catch (error) { console.error('Update error:', 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: '更新失败,请稍后再试' }, { status: 500 }); } finally { releaseRequestSlot?.(); } }