import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { createMaterialLog } from '@/lib/material-log' import { uploadToS3, deleteFromS3 } from '@/lib/s3' import { processImage, getProcessedFileName } from '@/lib/image-processor' import { validateSubmittedTemplateFields } from '@/lib/template-field-validation' import { prepareMaterialData, compressMetadata, readAllFieldValues, checkMetadataSize, } from '@/lib/field-data-storage' import { canManageMaterial, getMaterialPermissionActor } from '@/lib/material-permissions' export async function PUT(req: NextRequest) { try { const session = await getServerSession(authOptions) if (!session?.user) { return NextResponse.json( { error: '请先登录' }, { status: 401 } ) } const formData = await req.formData() const normalizeInput = (value: FormDataEntryValue | null) => { if (!value) return null const trimmed = value.toString().trim() return trimmed ? trimmed : null } const id = formData.get('id') as string const productModel = formData.get('productModel') as string const categoryName = formData.get('categoryName') as string const sceneName = formData.get('sceneName') as string const materialType = (formData.get('materialType') as string) || '3DMAX' const renderer = (formData.get('renderer') as string) || 'CR' const authorName = formData.get('authorName') as string const previewImage = formData.get('previewImage') as File | null const brandName = normalizeInput(formData.get('brandName')) const seriesName = normalizeInput(formData.get('seriesName')) const skuCode = normalizeInput(formData.get('skuCode')) const materialTexture = normalizeInput(formData.get('materialTexture')) const renderTime = normalizeInput(formData.get('renderTime')) const imageResolution = normalizeInput(formData.get('imageResolution')) const colorMode = normalizeInput(formData.get('colorMode')) // 音频相关字段 const audioDuration = normalizeInput(formData.get('audioDuration')) const audioCategory = normalizeInput(formData.get('audioCategory')) const audioScene = normalizeInput(formData.get('audioScene')) const templateId = normalizeInput(formData.get('templateId')) const templateFieldsStr = formData.get('templateFields') as string | null let templateFields: Record = {} if (templateFieldsStr) { try { templateFields = JSON.parse(templateFieldsStr) } catch (error) { console.error('[MaterialsUpdate] Failed to parse templateFields:', error) return NextResponse.json( { error: '模板字段格式无效' }, { status: 400 } ) } } // Validation - 音频素材不需要 categoryName 和 sceneName const isAudioMaterial = materialType === '音频' if (!id || !productModel || !materialType || !authorName) { return NextResponse.json( { error: '所有字段都是必填的' }, { status: 400 } ) } // 非音频素材需要 categoryName 和 sceneName if (!isAudioMaterial && (!categoryName || !sceneName)) { return NextResponse.json( { error: '店铺和场景是必填的' }, { status: 400 } ) } // Get existing material const existingMaterial = await prisma.material.findUnique({ where: { id }, include: { user: { select: { id: true, department: true, }, }, }, }) if (!existingMaterial) { return NextResponse.json( { error: '素材不存在' }, { status: 404 } ) } const actor = await getMaterialPermissionActor(session) if (!(await canManageMaterial(existingMaterial, actor))) { return NextResponse.json( { error: '您没有权限编辑此素材' }, { status: 403 } ) } const userId = actor!.id let previewUrl = existingMaterial.previewUrl // If new preview image is uploaded if (previewImage && previewImage.size > 0) { // Delete old preview image from S3 try { await deleteFromS3(existingMaterial.previewUrl) } catch (error) { console.error('Error deleting old preview:', error) } // 处理预览图:压缩到300KB以内,转换为webP,缩放到600px const originalBuffer = Buffer.from(await previewImage.arrayBuffer()) const processedBuffer = await processImage(originalBuffer, { maxSizeKB: 300, maxWidth: 600, format: 'webp', }) // Upload processed preview image to S3 const processedFileName = getProcessedFileName(previewImage.name) const previewFileName = `preview-${Date.now()}-${processedFileName}` previewUrl = await uploadToS3( processedBuffer, previewFileName, 'image/webp' ) } // 构建更新数据,根据素材类型决定哪些字段需要更新 const updateData: any = { previewUrl, productModel, brandName, seriesName, skuCode, materialTexture, materialType, authorName, } const effectiveTemplateId = templateId ?? existingMaterial.templateId ?? null const currentMaterialFieldValues = { ...readAllFieldValues(existingMaterial as Record), productModel, brandName, seriesName, skuCode, materialTexture, categoryName, sceneName, materialType, renderer, renderTime, imageResolution, colorMode, authorName, audioDuration, audioCategory, audioScene, } const mergedTemplateFields = { ...currentMaterialFieldValues, ...templateFields, } const templateFieldsValidation = await validateSubmittedTemplateFields({ templateId: effectiveTemplateId, templateFields: mergedTemplateFields, allowUnknownKeys: true, }) if (!templateFieldsValidation.valid) { return NextResponse.json( { error: templateFieldsValidation.error, errorCode: templateFieldsValidation.errorCode, details: templateFieldsValidation.details, }, { status: 400 } ) } templateFields = templateFieldsValidation.templateFields const { metadata: rawMetadata } = prepareMaterialData( templateFields, effectiveTemplateId, undefined, existingMaterial.metadata ) const metadata = compressMetadata(rawMetadata) const metadataSize = checkMetadataSize(metadata) if (!metadataSize.ok) { return NextResponse.json( { error: `自定义字段数据过大,当前约 ${metadataSize.size} 字节,最大允许 ${metadataSize.maxSize} 字节。请减少字段内容后重试。`, errorCode: 'METADATA_TOO_LARGE', }, { status: 400 } ) } updateData.templateId = effectiveTemplateId updateData.metadata = metadata // 非音频素材更新这些字段 if (!isAudioMaterial) { updateData.categoryName = categoryName updateData.sceneName = sceneName updateData.renderer = renderer updateData.renderTime = renderTime updateData.imageResolution = imageResolution updateData.colorMode = colorMode } else { // 音频素材更新音频相关字段 updateData.audioDuration = audioDuration updateData.audioCategory = audioCategory updateData.audioScene = audioScene } // Update material record const material = await prisma.material.update({ where: { id }, data: updateData, include: { user: { select: { username: true, }, }, }, }) // 记录变更日志 const changedFields: Record = {} const collectChange = (key: string, newValue: any) => { const oldValue = (existingMaterial as any)[key] const normOld = oldValue ?? null const normNew = newValue ?? null if (normOld === null && normNew === null) return if (String(normOld) !== String(normNew)) { changedFields[key] = { oldValue: normOld, newValue: normNew } } } collectChange('previewUrl', previewUrl) collectChange('productModel', productModel) collectChange('brandName', brandName) collectChange('seriesName', seriesName) collectChange('skuCode', skuCode) collectChange('materialTexture', materialTexture) collectChange('categoryName', categoryName) collectChange('sceneName', sceneName) collectChange('materialType', materialType) collectChange('templateId', effectiveTemplateId) collectChange('renderer', renderer) collectChange('renderTime', renderTime) collectChange('imageResolution', imageResolution) collectChange('colorMode', colorMode) collectChange('authorName', authorName) // 音频相关字段 collectChange('audioDuration', audioDuration) collectChange('audioCategory', audioCategory) collectChange('audioScene', audioScene) if (Object.keys(changedFields).length > 0) { await createMaterialLog({ materialId: id, userId, action: 'UPDATE', changes: changedFields, }) } // 转换BigInt为字符串 const materialData = { ...material, zipFileSize: material.zipFileSize.toString() } return NextResponse.json( { message: '更新成功', material: materialData }, { status: 200 } ) } catch (error) { console.error('Update error:', error) return NextResponse.json( { error: '更新失败,请稍后再试' }, { status: 500 } ) } }