chenpei 3b825883bb feat: 添加多类型素材支持和系统外观设置功能 (v2.4.0)
新增功能:
- 素材类型系统:支持3DMAX/PSD/AI/CAD/C4D/其他等多种类型
- 智能表单:渲染器选项仅在3DMAX类型时显示
- 类型标签:首页卡片显示素材类型标签(橙色)
- 系统外观设置:自定义导航栏颜色、LOGO和网站名称

数据库变更:
- 新增materialType字段到materials表
- 新增materialType索引优化查询
- 新增SystemSettings表存储系统设置

技术改进:
- 完整的TypeScript类型支持
- 前后端完整适配
- 修复Next.js配置弃用警告
- 添加完整的部署文档

文件变更:
- 修改: 上传/编辑组件支持素材类型
- 修改: API接口处理新字段
- 新增: 系统设置管理页面
- 新增: 系统设置API路由
- 新增: 数据库迁移文件
- 更新: 项目文档和部署指南
2025-10-26 09:58:40 +08:00

202 lines
6.8 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 { uploadToS3 } from '@/lib/s3'
import { processImage, getProcessedFileName } from '@/lib/image-processor'
import { writeFile, mkdir } from 'fs/promises'
import path from 'path'
// Configure route to handle large files
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
// Increase timeout for large file uploads
export const maxDuration = 10800 // 3 hours timeout (10800 seconds)
export async function POST(req: NextRequest) {
try {
console.log('[Upload] Starting upload process...')
const session = await getServerSession(authOptions)
console.log('[Upload] Session checked:', !!session)
if (!session) {
return NextResponse.json(
{ error: '请先登录' },
{ status: 401 }
)
}
console.log('[Upload] Parsing form data...')
const formData = await req.formData()
const previewImage = formData.get('previewImage') as File
const zipFile = formData.get('zipFile') as File
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'
console.log('[Upload] Form data received:', {
hasPreviewImage: !!previewImage,
previewImageSize: previewImage?.size,
hasZipFile: !!zipFile,
zipFileSize: zipFile?.size,
productModel,
categoryName,
sceneName,
materialType
})
// Validation
if (!previewImage || !zipFile || !productModel || !categoryName || !sceneName) {
console.log('[Upload] Validation failed - missing fields')
return NextResponse.json(
{ error: '所有字段都是必填的' },
{ status: 400 }
)
}
// 获取用户信息作为作者花名
console.log('[Upload] Fetching user info...')
const user = await prisma.user.findUnique({
where: { id: (session.user as any).id },
select: { username: true },
})
if (!user) {
console.log('[Upload] User not found')
return NextResponse.json(
{ error: '用户不存在' },
{ status: 404 }
)
}
const authorName = user.username
console.log('[Upload] User found:', authorName)
// 验证预览图大小(限制50MB)
const maxPreviewSize = 50 * 1024 * 1024 // 50MB
if (previewImage.size > maxPreviewSize) {
console.log(`[Upload] Preview image too large: ${(previewImage.size / 1024 / 1024).toFixed(2)}MB`)
return NextResponse.json(
{ error: `预览图过大,请使用小于50MB的图片(当前:${(previewImage.size / 1024 / 1024).toFixed(2)}MB)` },
{ status: 400 }
)
}
// 验证预览图格式
const allowedImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']
if (!allowedImageTypes.includes(previewImage.type)) {
console.log(`[Upload] Invalid image type: ${previewImage.type}`)
return NextResponse.json(
{ error: `不支持的图片格式:${previewImage.type},请使用 JPG、PNG、WebP 或 GIF 格式` },
{ status: 400 }
)
}
// 处理预览图:压缩到300KB以内,转换为webP,缩放到600px
console.log(`[Upload] Processing image... (${previewImage.type}, ${(previewImage.size / 1024).toFixed(2)}KB)`)
let processedBuffer: Buffer
try {
const originalBuffer = Buffer.from(await previewImage.arrayBuffer())
processedBuffer = await processImage(originalBuffer, {
maxSizeKB: 300,
maxWidth: 600,
format: 'webp',
})
console.log('[Upload] Image processed successfully')
} catch (imageError) {
console.error('[Upload] Image processing error:', imageError)
return NextResponse.json(
{ error: `图片处理失败,请确保图片文件未损坏且格式正确。错误:${imageError instanceof Error ? imageError.message : '未知错误'}` },
{ status: 500 }
)
}
// Upload processed preview image to S3
console.log('[Upload] Uploading image to S3...')
const processedFileName = getProcessedFileName(previewImage.name)
const previewFileName = `preview-${Date.now()}-${processedFileName}`
const previewUrl = await uploadToS3(
processedBuffer,
previewFileName,
'image/webp'
)
console.log('[Upload] Image uploaded to S3:', previewUrl)
// Save zip file locally
console.log('[Upload] Saving zip file locally...')
const uploadDir = process.env.UPLOAD_DIR || './uploads'
await mkdir(uploadDir, { recursive: true })
const zipFileName = `zip-${Date.now()}-${zipFile.name}`
const zipFilePath = path.join(uploadDir, zipFileName)
const zipBuffer = Buffer.from(await zipFile.arrayBuffer())
await writeFile(zipFilePath, zipBuffer)
const zipFileSize = BigInt(zipBuffer.length) // 记录文件大小(字节)
console.log('[Upload] Zip file saved:', zipFilePath, `(${(Number(zipFileSize) / 1024 / 1024).toFixed(2)} MB)`)
// Create material record
console.log('[Upload] Creating database record...')
const material = await prisma.material.create({
data: {
previewUrl,
productModel,
categoryName,
sceneName,
materialType,
renderer,
authorName,
zipFileName,
zipFilePath,
zipFileSize,
userId: (session.user as any).id,
},
include: {
user: {
select: {
username: true,
},
},
},
})
console.log('[Upload] Database record created:', material.id)
// 转换BigInt为字符串以支持JSON序列化
const materialData = {
...material,
zipFileSize: material.zipFileSize.toString()
}
console.log('[Upload] Upload completed successfully!')
return NextResponse.json(
{ message: '上传成功', material: materialData },
{ status: 201 }
)
} catch (error) {
console.error('Upload error:', error)
// Log detailed error information
if (error instanceof Error) {
console.error('Error name:', error.name)
console.error('Error message:', error.message)
console.error('Error stack:', error.stack)
}
// Return more specific error message for debugging
const errorMessage = error instanceof Error ? error.message : '上传失败,请稍后再试'
return NextResponse.json(
{
error: '上传失败,请稍后再试',
details: process.env.NODE_ENV === 'development' ? errorMessage : undefined
},
{ status: 500 }
)
}
}