2026-05-28 15:11:36 +08:00

202 lines
5.5 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { createMaterialShareToken, findValidMaterialShareToken } from '@/lib/material-share-token'
import { toSharedMaterialAssetProxyUrl } from '@/lib/url-rewrite'
import {
canManageMaterialShare,
getMaterialShareActor,
} from '@/lib/material-share-permissions'
import { isPublicShareEnabled } from '@/lib/public-share-settings'
function parseDisplayImages(value: string | null): string[] {
if (!value) return []
try {
const parsed = JSON.parse(value)
if (Array.isArray(parsed)) {
return parsed.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
}
} catch (error) {
console.warn('[materials/share] Failed to parse displayImages JSON, fallback to single value:', error)
}
return value ? [value] : []
}
export const dynamic = 'force-dynamic'
// 创建公开分享票据(登录用户)
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
const actor = await getMaterialShareActor(session)
if (!actor) {
return NextResponse.json(
{ error: '请先登录' },
{ status: 401 }
)
}
if (!(await isPublicShareEnabled())) {
return NextResponse.json(
{ error: '公开分享功能已关闭' },
{ status: 403 }
)
}
const body = await req.json().catch(() => ({}))
const materialId = typeof body?.materialId === 'string' ? body.materialId.trim() : ''
if (!materialId) {
return NextResponse.json(
{ error: '缺少素材ID' },
{ status: 400 }
)
}
const material = await prisma.material.findFirst({
where: {
id: materialId,
deletedAt: null,
},
select: {
id: true,
userId: true,
ownerType: true,
ownerId: true,
createdByUserId: true,
user: {
select: {
id: true,
department: true,
},
},
},
})
if (!material) {
return NextResponse.json(
{ error: '素材不存在或已删除' },
{ status: 404 }
)
}
if (!(await canManageMaterialShare(material, actor))) {
return NextResponse.json(
{ error: '无权分享该素材' },
{ status: 403 }
)
}
const tokenResult = await createMaterialShareToken(material.id, actor.id)
return NextResponse.json({
materialId: material.id,
shareToken: tokenResult.token,
expiresAt: tokenResult.expiresAt.toISOString(),
})
} catch (error) {
console.error('Error creating material share token:', error)
return NextResponse.json(
{ error: '创建分享链接失败' },
{ status: 500 }
)
}
}
// Get single material for sharing (公开访问,但必须携带有效分享票据)
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const id = searchParams.get('id')
const shareToken = searchParams.get('token') || searchParams.get('shareToken') || ''
if (!id) {
return NextResponse.json(
{ error: '缺少素材ID' },
{ status: 400 }
)
}
if (!(await isPublicShareEnabled())) {
return NextResponse.json(
{ error: '公开分享功能已关闭' },
{ status: 403 }
)
}
const validShareToken = await findValidMaterialShareToken(shareToken, id)
if (!validShareToken) {
return NextResponse.json(
{ error: '分享链接无效或已过期' },
{ status: 403 }
)
}
const material = await prisma.material.findFirst({
where: {
id,
deletedAt: null,
},
select: {
id: true,
previewUrl: true,
productModel: true,
brandName: true,
seriesName: true,
skuCode: true,
materialTexture: true,
categoryName: true,
sceneName: true,
materialType: true,
renderer: true,
renderTime: true,
imageResolution: true,
colorMode: true,
authorName: true,
zipFileName: true,
zipFileSize: true,
downloadCount: true,
likeCount: true,
displayImages: true,
createdAt: true,
isPremium: true,
videoCoverUrl: true,
videoPreview540Url: true,
videoPreviewClipUrl: true,
audioUrl: true,
}
})
if (!material) {
return NextResponse.json(
{ error: '素材不存在' },
{ status: 404 }
)
}
// 转换BigInt为字符串
const materialData = {
...material,
zipFileSize: material.zipFileSize.toString(),
previewUrl: toSharedMaterialAssetProxyUrl(material.previewUrl, shareToken),
displayImages: parseDisplayImages(material.displayImages).map((url) =>
toSharedMaterialAssetProxyUrl(url, shareToken)
),
videoCoverUrl: toSharedMaterialAssetProxyUrl(material.videoCoverUrl, shareToken),
videoPreview540Url: toSharedMaterialAssetProxyUrl(material.videoPreview540Url, shareToken),
videoPreviewClipUrl: toSharedMaterialAssetProxyUrl(material.videoPreviewClipUrl, shareToken),
audioUrl: toSharedMaterialAssetProxyUrl(material.audioUrl, shareToken),
}
return NextResponse.json(materialData)
} catch (error) {
console.error('Error fetching material for share:', error)
return NextResponse.json(
{ error: '获取素材失败' },
{ status: 500 }
)
}
}