250 lines
8.7 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 { 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 });
}
}