- 创建统一的下载错误处理模块 (lib/download-errors.ts) - 实现13种错误类型分类和中文错误消息 - 后端统一使用 createDownloadError 创建标准化错误响应 - 前端统一使用 parseDownloadError 解析错误并显示 - 修复正式系统数据库Schema,添加产品全案相关字段和表 - 执行数据库迁移:添加 task_no, task_status, task_fetched_at 字段 - 创建 material_assets, api_keys, api_call_logs 表
259 lines
7.9 KiB
TypeScript
259 lines
7.9 KiB
TypeScript
/**
|
||
* 产品全案上传完成接口
|
||
* 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 }
|
||
);
|
||
}
|
||
}
|