250 lines
8.7 KiB
TypeScript
250 lines
8.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
||
import { getServerSession } from 'next-auth';
|
||
import { authOptions } from '@/lib/auth';
|
||
import { prisma } from '@/lib/prisma';
|
||
import { rewriteMaterialStorageUrlsForClient, rewriteMediaUrlForClient } from '@/lib/url-rewrite';
|
||
import { detectIsIntranet } from '@/lib/network-detection';
|
||
import { canUserDownloadSource, isSuperAdmin } from '@/lib/department-permission';
|
||
|
||
// 解析 displayImages 字段
|
||
function parseDisplayImages(displayImages: any): string[] {
|
||
if (!displayImages) return [];
|
||
|
||
if (Array.isArray(displayImages)) {
|
||
return displayImages.filter((img: any) => typeof img === 'string' && img.trim().length > 0);
|
||
}
|
||
|
||
if (typeof displayImages === 'string') {
|
||
try {
|
||
const parsed = JSON.parse(displayImages);
|
||
if (Array.isArray(parsed)) {
|
||
return parsed.filter((img: any) => typeof img === 'string' && img.trim().length > 0);
|
||
}
|
||
} catch (e) {
|
||
console.warn('[materials/share] Failed to parse displayImages JSON, fallback to single value:', e);
|
||
}
|
||
return displayImages.trim() ? [displayImages] : [];
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
// GET /api/materials/share - 获取分享素材信息(无需登录,但登录用户会返回点赞和收藏状态)
|
||
export async function GET(request: NextRequest) {
|
||
try {
|
||
const { searchParams } = new URL(request.url);
|
||
const id = searchParams.get('id');
|
||
|
||
if (!id) {
|
||
return NextResponse.json({ error: '缺少素材ID' }, { status: 400 });
|
||
}
|
||
|
||
// 获取当前登录用户(如果有)
|
||
const session = await getServerSession(authOptions);
|
||
const userId = session?.user ? (session.user as any).id : undefined;
|
||
|
||
const material = await prisma.material.findUnique({
|
||
where: { id },
|
||
select: {
|
||
id: true,
|
||
previewUrl: true,
|
||
productModel: true,
|
||
categoryName: true,
|
||
sceneName: true,
|
||
platformName: true,
|
||
productCategoryName: true,
|
||
materialType: true,
|
||
authorName: true,
|
||
zipFileName: true,
|
||
zipFileSize: true,
|
||
downloadCount: true,
|
||
likeCount: true,
|
||
displayImages: true,
|
||
createdAt: true,
|
||
userId: true,
|
||
videoSourceUrl: true,
|
||
videoPreview540Url: true,
|
||
videoPreviewClipUrl: true,
|
||
videoCoverUrl: true,
|
||
videoDuration: true,
|
||
videoProcessingStatus: true,
|
||
taskNo: true,
|
||
taskStatus: true,
|
||
taskFetchedAt: true,
|
||
} as any,
|
||
});
|
||
|
||
if (!material) {
|
||
return NextResponse.json({ error: '素材不存在' }, { status: 404 });
|
||
}
|
||
|
||
// 如果是任务类型素材,查询资产列表
|
||
const taskMaterialTypes = ['任务-详情', '任务-3D原图', '任务-AI原图', '任务-实拍原图'];
|
||
const isTaskMaterial = taskMaterialTypes.includes(material.materialType as unknown as string);
|
||
|
||
let assets: any[] = [];
|
||
if (isTaskMaterial) {
|
||
const rawAssets = await prisma.materialAsset.findMany({
|
||
where: {
|
||
materialId: id,
|
||
},
|
||
orderBy: { sortOrder: 'asc' },
|
||
select: {
|
||
id: true,
|
||
assetType: true,
|
||
fileUrl: true,
|
||
fileName: true,
|
||
fileSize: true,
|
||
sortOrder: true,
|
||
},
|
||
});
|
||
|
||
// 添加详细日志
|
||
console.log(`[Share API] ========== 查询资产 ==========`);
|
||
console.log(`[Share API] MaterialId: ${id}`);
|
||
console.log(`[Share API] MaterialType: ${material.materialType}`);
|
||
console.log(`[Share API] Total assets: ${rawAssets.length}`);
|
||
rawAssets.forEach((asset: any, index: number) => {
|
||
console.log(`[Share API] Asset ${index}: id=${asset.id}, type=${asset.assetType}, url=${asset.fileUrl}, fileName=${asset.fileName}`);
|
||
});
|
||
|
||
// 检查是否有重复的fileUrl
|
||
const urlCounts: { [key: string]: { count: number; types: string[] } } = {};
|
||
rawAssets.forEach((asset: any) => {
|
||
if (asset.fileUrl) {
|
||
if (!urlCounts[asset.fileUrl]) {
|
||
urlCounts[asset.fileUrl] = { count: 0, types: [] };
|
||
}
|
||
urlCounts[asset.fileUrl].count++;
|
||
urlCounts[asset.fileUrl].types.push(asset.assetType);
|
||
}
|
||
});
|
||
|
||
const duplicateUrls = Object.entries(urlCounts).filter(([url, info]) => info.count > 1);
|
||
if (duplicateUrls.length > 0) {
|
||
console.error(`[Share API] ⚠️ WARNING: Found duplicate fileUrls!`);
|
||
duplicateUrls.forEach(([url, info]) => {
|
||
console.error(` URL: ${url}`);
|
||
console.error(` Used by ${info.count} assets with types: ${info.types.join(', ')}`);
|
||
});
|
||
}
|
||
|
||
console.log(`[Share API] ====================================`);
|
||
|
||
// 将 BigInt 转换为字符串
|
||
assets = rawAssets.map(asset => ({
|
||
...asset,
|
||
fileSize: asset.fileSize.toString(),
|
||
}));
|
||
}
|
||
|
||
// 如果用户已登录,检查点赞和收藏状态
|
||
let isLiked = false;
|
||
let isFavorited = false;
|
||
|
||
if (userId) {
|
||
const [like, favorite] = await Promise.all([
|
||
prisma.materialLike.findFirst({
|
||
where: {
|
||
materialId: id,
|
||
userId,
|
||
},
|
||
}),
|
||
prisma.materialFavorite.findFirst({
|
||
where: {
|
||
materialId: id,
|
||
userId,
|
||
},
|
||
}),
|
||
]);
|
||
|
||
isLiked = !!like;
|
||
isFavorited = !!favorite;
|
||
}
|
||
|
||
// 任务类型素材:检查源文件下载权限
|
||
// - 任务-详情:参考"详情模板"类型的权限
|
||
// - 任务-3D原图/AI原图/实拍原图:参考"3D场景"类型的权限
|
||
let canDownloadSource = false;
|
||
if (isTaskMaterial && userId) {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: userId },
|
||
select: {
|
||
role: true,
|
||
department: true,
|
||
managedDepartment: true,
|
||
},
|
||
});
|
||
|
||
if (user) {
|
||
// 根据素材类型选择参考的权限类型
|
||
let referenceType = '3D场景'; // 默认参考3D场景权限
|
||
if ((material.materialType as unknown as string) === '任务-详情') {
|
||
referenceType = '详情模板';
|
||
}
|
||
|
||
canDownloadSource = await canUserDownloadSource(
|
||
user.department,
|
||
user.role,
|
||
user.managedDepartment,
|
||
referenceType,
|
||
userId
|
||
);
|
||
}
|
||
}
|
||
|
||
const response: any = {
|
||
...material,
|
||
zipFileSize: material.zipFileSize.toString(),
|
||
displayImages: parseDisplayImages(material.displayImages || null),
|
||
isLiked,
|
||
isFavorited,
|
||
assets: isTaskMaterial ? assets : undefined,
|
||
canDownloadSource: isTaskMaterial ? canDownloadSource : undefined,
|
||
};
|
||
|
||
// 使用统一的媒体URL重写规则(方案C:按文件大小阈值)
|
||
const isIntranet = detectIsIntranet(request);
|
||
|
||
// 处理音频文件的zipFileName(特殊处理)
|
||
let processedZipFileName = response.zipFileName;
|
||
if (response.zipFileName && response.materialType === '音乐') {
|
||
const zipFileName = response.zipFileName;
|
||
if (zipFileName.startsWith('http://') || zipFileName.startsWith('https://')) {
|
||
// 老素材:S3 URL,使用统一规则(音频通常>1MB,会走代理)
|
||
processedZipFileName = rewriteMediaUrlForClient(zipFileName, request, 'audio');
|
||
} else {
|
||
// 新素材:本地文件
|
||
if (isIntranet) {
|
||
// 内网用户:直接访问
|
||
processedZipFileName = `/uploads/${zipFileName}`;
|
||
} else {
|
||
// 外网用户:通过API访问
|
||
processedZipFileName = `/api/uploads/${zipFileName}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
const rewrittenResponse = {
|
||
...response,
|
||
// 图片:使用统一规则(小文件直接访问,大文件走代理)
|
||
previewUrl: rewriteMediaUrlForClient(response.previewUrl, request, 'image'),
|
||
videoCoverUrl: rewriteMediaUrlForClient(response.videoCoverUrl, request, 'image'),
|
||
displayImages: response.displayImages?.map((img: string) =>
|
||
rewriteMediaUrlForClient(img, request, 'image')
|
||
),
|
||
// 视频:使用统一规则(通常>1MB,会走代理)
|
||
videoPreview540Url: rewriteMediaUrlForClient(response.videoPreview540Url, request, 'video'),
|
||
videoPreviewClipUrl: rewriteMediaUrlForClient(response.videoPreviewClipUrl, request, 'video'),
|
||
videoSourceUrl: rewriteMediaUrlForClient(response.videoSourceUrl, request, 'video'),
|
||
// 音频文件:特殊处理
|
||
zipFileName: processedZipFileName,
|
||
};
|
||
|
||
return NextResponse.json(rewrittenResponse);
|
||
} catch (error) {
|
||
console.error('Error fetching material for share:', error);
|
||
return NextResponse.json({ error: '获取素材失败' }, { status: 500 });
|
||
}
|
||
}
|