chenpei cd6e40d9d2 优化:网络传输性能和稳定性 - TCP缓冲区+并发控制优化
主要改动:
1. TCP缓冲区优化(16MB)- 解决公网上传速度慢问题
2. 并发数降低到1-3 - 避免4并发导致ERR_CONNECTION_RESET
3. 动态并发控制 - 根据网络环境自动调整
4. Nginx超时增加到120s - 支持大分片上传
5. 上传进度修复 - 修复剩余时间计算
6. 新增任务-页面上传器和详情组件
7. 统一部署文档为deploy-remote-build.ps1

问题修复:
- 公网用户上传从200-900KB/s提升到预期3-5MB/s
- 内网用户上传更稳定,无连接重置错误
- UploadProgress组件剩余时间计算修正

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-27 09:33:09 +08:00

165 lines
6.6 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 { deleteFromS3, uploadToS3 } from '@/lib/s3';
import { compressImage, sanitizeFileName } from '@/lib/image-processor';
import { requiresCategoryAndScene, requiresRenderer, requiresProductCategoryName, getDefaultRenderer } from '@/lib/material-validation';
// PUT /api/materials/update - 更新素材(包括预览图)
export async function PUT(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: '请先登录' }, { status: 401 });
}
const formData = await request.formData();
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 platformName = formData.get('platformName') as string | null;
const productCategoryName = formData.get('productCategoryName') as string | null;
const materialType = (formData.get('materialType') as string) || '3D场景';
const renderer = formData.get('renderer') as string | null;
const authorName = formData.get('authorName') as string;
const newUserId = formData.get('userId') as string | null; // 添加userId字段
const previewImage = formData.get('previewImage') as File | null;
// 使用统一的验证逻辑
// 基础必填字段验证
if (!id || !productModel || !materialType || !authorName) {
return NextResponse.json({ error: '缺少必填字段' }, { status: 400 });
}
if (requiresCategoryAndScene(materialType) && (!categoryName || !sceneName)) {
return NextResponse.json({ error: '请填写所有必填字段(店铺、场景)' }, { status: 400 });
}
// 标准模型必须选择品类
if (requiresProductCategoryName(materialType) && !productCategoryName) {
return NextResponse.json({ error: '标准模型类型必须选择品类' }, { status: 400 });
}
// renderer 只在3D场景类型时必填
if (requiresRenderer(materialType) && !renderer) {
return NextResponse.json({ error: '3D场景类型必须指定渲染器' }, { status: 400 });
}
// 检查素材是否存在(排除已删除的素材)
const material = await prisma.material.findFirst({
where: { id, deletedAt: null },
});
if (!material) {
return NextResponse.json({ error: '素材不存在或已被删除' }, { status: 404 });
}
// 检查权限:素材所有者、系统管理员或超级管理员都可以编辑
const user = session.user as any;
const userId = user.id;
const userRole = user.role;
// 检查是否是超级管理员或系统管理员
const { isSuperAdmin, isSystemAdmin } = await import('@/lib/department-permission');
const isUserSuperAdmin = await isSuperAdmin(userId);
const isUserSystemAdmin = isSystemAdmin(userRole);
if (material.userId !== userId && !isUserSystemAdmin && !isUserSuperAdmin) {
return NextResponse.json({ error: '您没有权限编辑此素材' }, { status: 403 });
}
let previewUrl = material.previewUrl;
// 如果上传了新的预览图
if (previewImage && previewImage.size > 0) {
// 删除旧的预览图
try {
await deleteFromS3(material.previewUrl);
} catch (error) {
console.error('Error deleting old preview:', error);
}
// 压缩并上传新的预览图
const imageBuffer = Buffer.from(await previewImage.arrayBuffer());
const compressedBuffer = await compressImage(imageBuffer, {
maxSizeKB: 300,
maxWidth: 600,
format: 'webp',
});
const sanitizedName = sanitizeFileName(previewImage.name);
const fileName = `preview-${Date.now()}-${sanitizedName}`;
previewUrl = await uploadToS3(compressedBuffer, fileName, 'image/webp');
}
// 更新素材(包含所有字段,确保数据不丢失)
const updateData: any = {
previewUrl,
productModel,
// 使用统一的验证逻辑判断字段是否需要更新
categoryName: requiresCategoryAndScene(materialType) ? (categoryName || material.categoryName) : (categoryName !== undefined && categoryName !== null ? categoryName : material.categoryName),
sceneName: requiresCategoryAndScene(materialType) ? (sceneName || material.sceneName) : (sceneName !== undefined && sceneName !== null ? sceneName : material.sceneName),
platformName: platformName !== undefined && platformName !== null ? platformName : (material.platformName || ''),
materialType,
authorName,
};
// 如果提供了新的userId,则更新userId
if (newUserId && newUserId !== material.userId) {
// 验证新用户是否存在
const newUser = await prisma.user.findUnique({
where: { id: newUserId },
});
if (!newUser) {
return NextResponse.json({ error: '指定的用户不存在' }, { status: 400 });
}
updateData.userId = newUserId;
}
// 标准模型必须更新 productCategoryName
if (requiresProductCategoryName(materialType)) {
updateData.productCategoryName = productCategoryName || material.productCategoryName;
} else {
updateData.productCategoryName = productCategoryName !== undefined && productCategoryName !== null ? productCategoryName : material.productCategoryName;
}
// 使用统一的验证逻辑更新renderer
if (requiresRenderer(materialType)) {
// 3D场景类型:必须提供renderer,如果没有提供则使用默认值或保留原值
updateData.renderer = renderer || material.renderer || getDefaultRenderer(materialType);
} else {
// 非3D场景类型,保持原renderer值(如果有)
updateData.renderer = material.renderer;
}
const updatedMaterial = await prisma.material.update({
where: { id },
data: updateData,
include: {
user: {
select: {
username: true,
},
},
},
});
const response = {
...updatedMaterial,
zipFileSize: updatedMaterial.zipFileSize.toString(),
};
return NextResponse.json(
{ message: '更新成功', material: response },
{ status: 200 }
);
} catch (error) {
console.error('Update error:', error);
return NextResponse.json({ error: '更新失败,请稍后再试' }, { status: 500 });
}
}