424 lines
14 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.

/**
* 外部接口:产品全案素材查询和删除
* GET - 通过任务号查询素材详情
* DELETE - 通过任务号删除素材
*/
import { NextRequest } from 'next/server';
import {
validateApiKey,
getApiKeyFromRequest,
logApiCall,
errorResponse,
successResponse,
API_ERROR,
getClientIp,
} from '@/lib/external-api';
import {
validateTaskNo,
getMaterialByTaskNo,
updateTaskStatus,
softDeleteByTaskNo,
TASK_STATUS,
ASSET_TYPE,
} from '@/lib/product-package';
import { generateDownloadUrl, DownloadType } from '@/lib/download-token';
import { rewriteStorageUrlForClient } from '@/lib/url-rewrite';
import { isProductPackageType } from '@/lib/material-type-mapper';
import path from 'path';
// 获取基础 URL
function getBaseUrl(request: NextRequest): string {
const host = request.headers.get('host') || 'localhost:3000';
const protocol = request.headers.get('x-forwarded-proto') || 'http';
return `${protocol}://${host}`;
}
/**
* GET - 查询素材详情
*/
export async function GET(request: NextRequest) {
const startTime = Date.now();
const apiKey = getApiKeyFromRequest(request);
const taskNo = request.nextUrl.searchParams.get('taskNo');
// 验证 API Key
const keyRecord = await validateApiKey(apiKey);
if (!keyRecord) {
return errorResponse(API_ERROR.INVALID_API_KEY);
}
// 验证任务号格式
if (!taskNo) {
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'GET',
statusCode: 400,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return errorResponse(API_ERROR.INVALID_TASK_NO);
}
if (!validateTaskNo(taskNo)) {
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'GET',
taskNo,
statusCode: 400,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return errorResponse(API_ERROR.INVALID_TASK_NO);
}
try {
// 查询素材
console.log('[external/product-assets] Querying material for taskNo:', taskNo);
const material: any = await getMaterialByTaskNo(taskNo);
console.log('[external/product-assets] Material found:', material ? {
id: material.id,
taskNo: material.taskNo,
materialType: material.materialType,
taskStatus: material.taskStatus,
assetsCount: material.assets?.length || 0,
hasPreviewUrl: !!material.previewUrl,
} : 'null');
if (!material) {
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'GET',
taskNo,
statusCode: 404,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return errorResponse(API_ERROR.TASK_NOT_FOUND);
}
if (material.deletedAt) {
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'GET',
taskNo,
statusCode: 410,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return errorResponse(API_ERROR.MATERIAL_DELETED);
}
// 更新任务状态为 fetched
if (material.taskStatus !== TASK_STATUS.FETCHED) {
await updateTaskStatus(material.id, TASK_STATUS.FETCHED, {
taskFetchedAt: new Date(),
});
}
const baseUrl = getBaseUrl(request);
console.log('[external/product-assets] Base URL:', baseUrl);
console.log('[external/product-assets] Material type:', material.materialType);
// 根据素材类型返回不同结构
if (isProductPackageType(material.materialType)) {
// 任务-详情(兼容老的"产品全案")
// 注意:任务平台主要使用 downloadUrls.all,其他字段保持兼容即可
const mainAssets = (material.assets || []).filter((a: any) => a.assetType === ASSET_TYPE.MAIN);
const skuAssets = (material.assets || []).filter((a: any) => a.assetType === ASSET_TYPE.SKU);
const detailAssets = (material.assets || []).filter((a: any) => a.assetType === ASSET_TYPE.DETAIL);
const psdAssets = (material.assets || []).filter((a: any) => a.assetType === ASSET_TYPE.PSD);
console.log('[external/product-assets] Assets grouped:', {
main: mainAssets.length,
sku: skuAssets.length,
detail: detailAssets.length,
psd: psdAssets.length
});
// 检查是否有资产(产品全案至少应该有主图、SKU图或详情图)
if (mainAssets.length === 0 && skuAssets.length === 0 && detailAssets.length === 0) {
console.warn('[external/product-assets] Warning: Product package material has no assets:', {
materialId: material.id,
taskNo: material.taskNo,
});
}
// 生成资产列表(带下载链接)
const formatAsset = (asset: any, downloadType: DownloadType) => {
try {
// 验证资产数据
if (!asset || !asset.id) {
throw new Error(`Invalid asset data: ${JSON.stringify(asset)}`);
}
if (!asset.fileUrl) {
console.warn('[external/product-assets] Asset has no fileUrl:', {
assetId: asset.id,
assetType: asset.assetType,
fileName: asset.fileName,
});
}
if (!material.id) {
throw new Error(`Material has no id: ${JSON.stringify({ taskNo: material.taskNo, materialId: material.id })}`);
}
const rewrittenUrl = rewriteStorageUrlForClient(asset.fileUrl, request);
const downloadUrl = generateDownloadUrl(baseUrl, {
materialId: material.id,
downloadType: asset.assetType === ASSET_TYPE.PSD ? 'psd' : downloadType,
assetId: asset.assetType === ASSET_TYPE.PSD ? asset.id : undefined,
});
return {
id: asset.id,
url: rewrittenUrl,
downloadUrl,
fileName: asset.fileName || '',
fileSize: Number(asset.fileSize || 0),
...(asset.assetType === ASSET_TYPE.DETAIL && { sortOrder: asset.sortOrder || 0 }),
};
} catch (formatError) {
console.error('[external/product-assets] Error formatting asset:', {
assetId: asset?.id,
assetType: asset?.assetType,
materialId: material.id,
taskNo: material.taskNo,
error: formatError instanceof Error ? formatError.message : String(formatError),
stack: formatError instanceof Error ? formatError.stack : undefined,
});
throw formatError;
}
};
const responseData = {
taskNo: material.taskNo,
productModel: material.productModel,
categoryName: material.categoryName,
sceneName: material.sceneName,
status: TASK_STATUS.FETCHED,
createdAt: material.createdAt.toISOString(),
updatedAt: material.updatedAt.toISOString(),
assets: {
main: mainAssets.map((a: any) => formatAsset(a, 'main')),
sku: skuAssets.map((a: any) => formatAsset(a, 'sku')),
detail: detailAssets.map((a: any) => formatAsset(a, 'detail')),
psd: psdAssets.map((a: any) => formatAsset(a, 'psd')),
},
downloadUrls: {
all: generateDownloadUrl(baseUrl, { materialId: material.id, downloadType: 'all' }), // 任务平台主要使用这个
main: generateDownloadUrl(baseUrl, { materialId: material.id, downloadType: 'main' }),
sku: generateDownloadUrl(baseUrl, { materialId: material.id, downloadType: 'sku' }),
detail: generateDownloadUrl(baseUrl, { materialId: material.id, downloadType: 'detail' }),
psd: psdAssets.length > 0 ? generateDownloadUrl(baseUrl, { materialId: material.id, downloadType: 'psd' }) : null,
},
};
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'GET',
taskNo,
statusCode: 200,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return successResponse(responseData);
} else {
// 其他素材类型:返回统一的 files 结构和单个下载链接
const previewUrl = material.previewUrl || '';
const rewrittenPreviewUrl = previewUrl ? rewriteStorageUrlForClient(previewUrl, request) : '';
// 生成文件名(从zipFileName或productModel推断)
let fileName = material.zipFileName || '';
if (!fileName) {
// 如果没有zipFileName,尝试从productModel生成
if (material.productModel) {
// 根据素材类型推断扩展名
const ext = material.materialType === '音乐' ? '.mp3' :
['主图视频', '安装视频', '种草视频'].includes(material.materialType) ? '.mp4' :
material.materialType === '品牌VI灵感' ? '.pdf' :
'.zip';
fileName = `${material.productModel}${ext}`;
} else {
fileName = 'material.zip';
}
}
const responseData = {
taskNo: material.taskNo,
productModel: material.productModel,
categoryName: material.categoryName,
sceneName: material.sceneName,
status: material.taskStatus || TASK_STATUS.FETCHED,
createdAt: material.createdAt.toISOString(),
updatedAt: material.updatedAt.toISOString(),
files: [
{
id: material.id,
url: rewrittenPreviewUrl,
downloadUrl: generateDownloadUrl(baseUrl, { materialId: material.id, downloadType: 'all' }),
fileName: fileName || 'material.zip',
fileSize: Number(material.zipFileSize || 0),
...(material.videoDuration && { duration: material.videoDuration }),
},
],
downloadUrls: {
all: generateDownloadUrl(baseUrl, { materialId: material.id, downloadType: 'all' }),
},
};
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'GET',
taskNo,
statusCode: 200,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return successResponse(responseData);
}
} catch (error) {
// 记录详细的错误信息
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
console.error('[external/product-assets] GET error:', {
taskNo,
error: errorMessage,
stack: errorStack,
apiKeyId: keyRecord?.id,
timestamp: new Date().toISOString(),
});
// 尝试记录日志(即使keyRecord可能为null)
try {
if (keyRecord) {
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'GET',
taskNo,
statusCode: 500,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
}
} catch (logError) {
console.error('[external/product-assets] Failed to log API call:', logError);
}
return errorResponse(API_ERROR.SERVER_ERROR);
}
}
/**
* DELETE - 删除素材
*/
export async function DELETE(request: NextRequest) {
const startTime = Date.now();
const apiKey = getApiKeyFromRequest(request);
const taskNo = request.nextUrl.searchParams.get('taskNo');
// 验证 API Key
const keyRecord = await validateApiKey(apiKey);
if (!keyRecord) {
return errorResponse(API_ERROR.INVALID_API_KEY);
}
// 验证任务号格式
if (!taskNo || !validateTaskNo(taskNo)) {
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'DELETE',
taskNo: taskNo || undefined,
statusCode: 400,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return errorResponse(API_ERROR.INVALID_TASK_NO);
}
try {
// 查询素材
const material: any = await getMaterialByTaskNo(taskNo);
if (!material) {
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'DELETE',
taskNo,
statusCode: 404,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return errorResponse(API_ERROR.TASK_NOT_FOUND);
}
if (material.deletedAt) {
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'DELETE',
taskNo,
statusCode: 410,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return errorResponse(API_ERROR.MATERIAL_DELETED);
}
// 软删除
await softDeleteByTaskNo(taskNo);
// 记录日志
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'DELETE',
taskNo,
statusCode: 200,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return successResponse({
taskNo,
deletedAt: new Date().toISOString(),
}, '素材已删除');
} catch (error) {
console.error('[external/product-assets] DELETE error:', error);
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/product-assets',
method: 'DELETE',
taskNo,
statusCode: 500,
duration: Date.now() - startTime,
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
});
return errorResponse(API_ERROR.SERVER_ERROR);
}
}