chenpei ea9076df45 feat: 实现统一的下载错误处理系统并修复产品全案数据库Schema
- 创建统一的下载错误处理模块 (lib/download-errors.ts)
- 实现13种错误类型分类和中文错误消息
- 后端统一使用 createDownloadError 创建标准化错误响应
- 前端统一使用 parseDownloadError 解析错误并显示
- 修复正式系统数据库Schema,添加产品全案相关字段和表
- 执行数据库迁移:添加 task_no, task_status, task_fetched_at 字段
- 创建 material_assets, api_keys, api_call_logs 表
2026-01-05 17:52:42 +08:00

259 lines
7.9 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.

/**
* 产品全案上传完成接口
* POST - 保存资产信息,更新任务状态
*/
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { addAssets, updateTaskStatus, TASK_STATUS, ASSET_TYPE, cleanupProductPackageFiles } from '@/lib/product-package';
interface AssetData {
fileName: string;
fileSize: number;
fileUrl: string;
sortOrder?: number;
}
interface CompleteRequestBody {
materialId: string;
assets: {
main?: AssetData[];
sku?: AssetData[];
detail?: AssetData[];
psd?: AssetData;
};
}
export async function POST(request: NextRequest) {
let materialId: string | null = null;
let uploadedFileUrls: string[] = [];
try {
// 验证用户登录
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const body: CompleteRequestBody = await request.json();
const { materialId: bodyMaterialId, assets } = body;
// 验证必填字段
if (!bodyMaterialId || !assets) {
return NextResponse.json(
{ success: false, error: '缺少必填参数' },
{ status: 400 }
);
}
materialId = bodyMaterialId;
// 收集所有已上传的文件 URL(用于失败时清理)
if (assets.main) uploadedFileUrls.push(...assets.main.map(a => a.fileUrl));
if (assets.sku) uploadedFileUrls.push(...assets.sku.map(a => a.fileUrl));
if (assets.detail) uploadedFileUrls.push(...assets.detail.map(a => a.fileUrl));
if (assets.psd) uploadedFileUrls.push(assets.psd.fileUrl);
// 验证素材是否存在且属于当前用户
const material: any = await prisma.material.findUnique({
where: { id: materialId! },
});
if (!material) {
return NextResponse.json(
{ success: false, error: '素材不存在' },
{ status: 404 }
);
}
if (material.userId !== session.user.id) {
return NextResponse.json(
{ success: false, error: '无权限操作此素材' },
{ status: 403 }
);
}
// 准备资产数据
const assetRecords: Array<{
materialId: string;
assetType: any; // 使用 any 避免类型错误
fileUrl: string;
fileName?: string;
fileSize?: bigint;
sortOrder?: number;
}> = [];
// 主图
if (assets.main && assets.main.length > 0) {
assets.main.forEach((asset, index) => {
assetRecords.push({
materialId: materialId!,
assetType: ASSET_TYPE.MAIN,
fileUrl: asset.fileUrl,
fileName: asset.fileName,
fileSize: BigInt(asset.fileSize),
sortOrder: index,
});
});
}
// SKU图
if (assets.sku && assets.sku.length > 0) {
assets.sku.forEach((asset, index) => {
assetRecords.push({
materialId: materialId!,
assetType: ASSET_TYPE.SKU,
fileUrl: asset.fileUrl,
fileName: asset.fileName,
fileSize: BigInt(asset.fileSize),
sortOrder: index,
});
});
}
// 详情图
if (assets.detail && assets.detail.length > 0) {
assets.detail.forEach((asset) => {
assetRecords.push({
materialId: materialId!,
assetType: ASSET_TYPE.DETAIL,
fileUrl: asset.fileUrl,
fileName: asset.fileName,
fileSize: BigInt(asset.fileSize),
sortOrder: asset.sortOrder || 0,
});
});
}
// PSD源文件(支持多个)
if (assets.psd && Array.isArray(assets.psd) && assets.psd.length > 0) {
assets.psd.forEach((psdAsset, index) => {
assetRecords.push({
materialId: materialId!,
assetType: ASSET_TYPE.PSD,
fileUrl: psdAsset.fileUrl,
fileName: psdAsset.fileName,
fileSize: BigInt(psdAsset.fileSize),
sortOrder: index,
});
});
} else if (assets.psd && !Array.isArray(assets.psd)) {
// 兼容旧格式(单个PSD文件)
assetRecords.push({
materialId: materialId!,
assetType: ASSET_TYPE.PSD,
fileUrl: assets.psd.fileUrl,
fileName: assets.psd.fileName,
fileSize: BigInt(assets.psd.fileSize),
sortOrder: 0,
});
}
// 保存资产
if (assetRecords.length > 0) {
// 添加详细日志,验证保存前的数据
console.log(`[Upload Complete] ========== 准备保存资产 ==========`);
console.log(`[Upload Complete] MaterialId: ${materialId}`);
console.log(`[Upload Complete] Total asset records: ${assetRecords.length}`);
assetRecords.forEach((record, index) => {
console.log(`[Upload Complete] Asset ${index}: type=${record.assetType}, url=${record.fileUrl}, fileName=${record.fileName}`);
});
// 检查是否有重复的fileUrl
const urlCounts: { [key: string]: { count: number; types: string[] } } = {};
assetRecords.forEach((record) => {
if (record.fileUrl) {
if (!urlCounts[record.fileUrl]) {
urlCounts[record.fileUrl] = { count: 0, types: [] };
}
urlCounts[record.fileUrl].count++;
urlCounts[record.fileUrl].types.push(record.assetType);
}
});
const duplicateUrls = Object.entries(urlCounts).filter(([url, info]) => info.count > 1);
if (duplicateUrls.length > 0) {
console.error(`[Upload Complete] ⚠️ WARNING: Found duplicate fileUrls before saving!`);
duplicateUrls.forEach(([url, info]) => {
console.error(` URL: ${url}`);
console.error(` Used by ${info.count} assets with types: ${info.types.join(', ')}`);
});
}
console.log(`[Upload Complete] ====================================`);
await addAssets(assetRecords);
console.log(`[Upload Complete] ✅ Assets saved successfully`);
}
// 获取封面图(第一张主图或第一张SKU图)
let previewUrl = '';
if (assets.main && assets.main.length > 0) {
previewUrl = assets.main[0].fileUrl;
} else if (assets.sku && assets.sku.length > 0) {
previewUrl = assets.sku[0].fileUrl;
}
// 更新素材状态和封面图
await prisma.material.update({
where: { id: materialId! },
data: {
previewUrl,
taskStatus: TASK_STATUS.COMPLETED,
} as any,
});
// 记录日志
await prisma.materialLog.create({
data: {
materialId: materialId!,
userId: session.user.id,
action: 'UPLOAD',
changes: JSON.stringify({
taskNo: material.taskNo,
assetCounts: {
main: assets.main?.length || 0,
sku: assets.sku?.length || 0,
detail: assets.detail?.length || 0,
psd: assets.psd ? 1 : 0,
},
}),
},
});
return NextResponse.json({
success: true,
data: {
materialId: materialId!,
taskNo: material.taskNo,
message: '上传完成',
},
});
} catch (error) {
console.error('[product-package/complete] error:', error);
// 如果失败,尝试清理已上传的文件,避免产生垃圾文件
if (materialId) {
console.log(`[product-package/complete] Attempting to cleanup files for failed upload: ${materialId}`);
try {
const cleanupResult = await cleanupProductPackageFiles(materialId, uploadedFileUrls);
console.log(`[product-package/complete] Cleanup result:`, cleanupResult);
} catch (cleanupError) {
console.error('[product-package/complete] Cleanup failed:', cleanupError);
// 清理失败不影响错误返回
}
}
return NextResponse.json(
{ success: false, error: '服务器错误' },
{ status: 500 }
);
}
}