diff --git a/app/api/materials/__tests__/template-fields-routes.test.ts b/app/api/materials/__tests__/template-fields-routes.test.ts index bb4b21b..1280ac5 100644 --- a/app/api/materials/__tests__/template-fields-routes.test.ts +++ b/app/api/materials/__tests__/template-fields-routes.test.ts @@ -238,7 +238,15 @@ describe('素材模板字段链路测试', () => { expect(response.status).toBe(400) expect(data.errorCode).toBe('TEMPLATE_REQUIRED_FIELDS_MISSING') expect(prisma.material.create).not.toHaveBeenCalled() - expect(validateTemplateId).not.toHaveBeenCalled() + expect(validateTemplateId).toHaveBeenCalledWith( + 'template-1', + expect.objectContaining({ + checkStatus: true, + logPerformance: true, + }) + ) + expect(uploadToS3).not.toHaveBeenCalled() + expect(uploadStreamToS3).not.toHaveBeenCalled() }) test('上传成功时应写入 templateId 和自定义字段 metadata', async () => { @@ -273,6 +281,35 @@ describe('素材模板字段链路测试', () => { expect(data.material.zipFileSize).toBe('0') }) + test('模板不可用时应在上传文件前拒绝', async () => { + const zipFile = createMockFile('inactive-template.zip', 'application/zip', 'zip-data') + validateTemplateId.mockResolvedValueOnce({ + valid: false, + error: '模板已停用', + errorCode: 'TEMPLATE_INACTIVE', + templateStatus: 'inactive', + }) + + const response = await uploadMaterial( + buildUploadRequest({ + zipFile, + templateFields: JSON.stringify({ + productModel: '稳定材质', + customCode: 'CODE-INACTIVE', + }), + }) + ) + const data = await response.json() + + expect(response.status).toBe(400) + expect(data.error).toBe('模板已停用') + expect(data.errorCode).toBe('TEMPLATE_INACTIVE') + expect(getTemplateWithFields).not.toHaveBeenCalled() + expect(uploadToS3).not.toHaveBeenCalled() + expect(uploadStreamToS3).not.toHaveBeenCalled() + expect(prisma.material.create).not.toHaveBeenCalled() + }) + test('非音频原始素材上传应走流式上传,避免二次整文件 Buffer', async () => { const zipFile = createMockFile('model.zip', 'application/zip', 'zip-data') diff --git a/app/api/materials/upload/route.ts b/app/api/materials/upload/route.ts index de93547..1efd59a 100644 --- a/app/api/materials/upload/route.ts +++ b/app/api/materials/upload/route.ts @@ -293,6 +293,36 @@ export async function POST(req: NextRequest) { console.log('[Upload] User found:', user.username, 'Author name:', authorName) if (templateId) { + console.log('[Upload] Validating template before file upload:', templateId) + const validation = await validateTemplateId(templateId, { + checkStatus: true, + logPerformance: true, + }) + + if (!validation.valid) { + console.error('[Upload] Template validation failed before file upload:', { + templateId, + error: validation.error, + errorCode: validation.errorCode, + validationTime: validation.validationTime, + }) + return NextResponse.json( + { + error: validation.error || '模板验证失败', + errorCode: validation.errorCode, + }, + { status: 400 } + ) + } + + console.log('[Upload] Template validation passed before file upload:', { + templateId, + isBuiltin: validation.isBuiltin, + templateKey: validation.templateKey, + templateName: validation.templateName, + validationTime: validation.validationTime, + }) + const templateFieldsValidation = await validateSubmittedTemplateFields({ templateId, templateFields, @@ -634,39 +664,6 @@ export async function POST(req: NextRequest) { )) } - // 验证模板ID(如果提供) - if (templateId) { - console.log('[Upload] Validating template:', templateId) - const validation = await validateTemplateId(templateId, { - checkStatus: true, // 检查模板状态 - logPerformance: true, // 记录性能日志 - }) - - if (!validation.valid) { - console.error('[Upload] Template validation failed:', { - templateId, - error: validation.error, - errorCode: validation.errorCode, - validationTime: validation.validationTime, - }) - return await failWithCleanup(NextResponse.json( - { - error: validation.error || '模板验证失败', - errorCode: validation.errorCode, - }, - { status: 400 } - )) - } - - console.log('[Upload] Template validation passed:', { - templateId, - isBuiltin: validation.isBuiltin, - templateKey: validation.templateKey, - templateName: validation.templateName, - validationTime: validation.validationTime, - }) - } - try { const materialId = await generateMaterialId() const material = await prisma.material.create({ diff --git a/docs/商业化地基排查问题清单_20260524.md b/docs/商业化地基排查问题清单_20260524.md index 2177618..e122619 100644 --- a/docs/商业化地基排查问题清单_20260524.md +++ b/docs/商业化地基排查问题清单_20260524.md @@ -529,11 +529,13 @@ - 已修复:上传链路现在通过 `uploadedArtifactUrls` 记录本次已经写入的对象。 - 已修复:`failWithCleanup()` 会在模板校验、数据库写入等失败路径调用 `cleanupUploadedArtifacts()`。 - 已修复:成功创建素材记录后会清空待清理列表,避免误删正常素材文件。 +- 已修复:上传入口会在处理预览图、展示图和原始素材文件前先确认模板存在且启用。模板已停用或不存在时会直接拒绝,不再让客户等大文件上传完才失败,也不会写入对象存储。 - 仍需后续处理:上传过程仍有整文件进内存问题,归到 P1-11。 建议动作: - 保持现有补偿清理测试覆盖。 +- 保持“模板不可用时不上传文件”的测试覆盖,后续改上传方式时不能退回到文件写入后才校验模板。 - 后续如引入分片/直传上传,要继续保留“数据库失败自动清理已上传对象”的规则。 ### P1-08 永久删除和清理失败时仍删除数据库记录(已完成第一轮修复)