271 lines
8.2 KiB
TypeScript
271 lines
8.2 KiB
TypeScript
import { promises as fs } from 'fs'
|
|
import path from 'path'
|
|
import JSZip from 'jszip'
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { getServerSession } from 'next-auth'
|
|
import { authOptions } from '@/lib/auth'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { ensureMaterialDownloadLogsTable } from '@/lib/material-download-log'
|
|
import {
|
|
getWhiteBackgroundAssetWhereForSku,
|
|
groupWhiteBackgroundAssetsBySku,
|
|
hasAnyWhiteBackgroundAssetGroup,
|
|
STANDARD_MODEL_WHITE_BG_VIEWS,
|
|
type GroupedWhiteBackgroundAssets,
|
|
} from '@/lib/standard-model-white-background'
|
|
import { getUploadRootDir, resolveStoredUploadPath } from '@/lib/upload-paths'
|
|
import { isPathInAllowedDirs } from '@/lib/path-security'
|
|
import { canDownloadSkuMaterial } from '../authorization'
|
|
|
|
type DownloadType = 'model' | 'white-background'
|
|
|
|
type WhiteBackgroundAssetRecord = {
|
|
assetType: string
|
|
fileUrl: string
|
|
fileName: string | null
|
|
sortOrder: number
|
|
}
|
|
|
|
function resolveModelDownloadFileName(
|
|
skuName: string,
|
|
rawFileName?: string | null,
|
|
rawFilePath?: string | null,
|
|
): string {
|
|
const ext = path.extname(rawFileName || rawFilePath || '') || '.zip'
|
|
return `${skuName}${ext}`
|
|
}
|
|
|
|
function resolveLocalFilePath(fileUrl: string): string | null {
|
|
const resolvedPath = resolveStoredUploadPath(fileUrl)
|
|
return isPathInAllowedDirs(resolvedPath, [getUploadRootDir()]) ? resolvedPath : null
|
|
}
|
|
|
|
function getWhiteBackgroundZipEntries(group: GroupedWhiteBackgroundAssets<WhiteBackgroundAssetRecord>) {
|
|
const fixedEntries = STANDARD_MODEL_WHITE_BG_VIEWS
|
|
.map((view) => {
|
|
const asset = group.fixed[view.key]
|
|
if (!asset) {
|
|
return null
|
|
}
|
|
return {
|
|
fileName: asset.fileName || `${view.label}${path.extname(asset.fileUrl) || '.jpg'}`,
|
|
fileUrl: asset.fileUrl,
|
|
}
|
|
})
|
|
.filter((entry): entry is { fileName: string; fileUrl: string } => Boolean(entry))
|
|
|
|
const otherEntries = group.other.map((asset) => ({
|
|
fileName: asset.fileName || path.basename(asset.fileUrl),
|
|
fileUrl: asset.fileUrl,
|
|
directory: 'other',
|
|
}))
|
|
|
|
return {
|
|
fixedEntries,
|
|
otherEntries,
|
|
}
|
|
}
|
|
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
try {
|
|
const session = await getServerSession(authOptions)
|
|
if (!session?.user || !(session.user as any).id) {
|
|
return NextResponse.json({ error: '请先登录' }, { status: 401 })
|
|
}
|
|
|
|
const userId = (session.user as any).id
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { canDownload: true },
|
|
})
|
|
|
|
if (!user?.canDownload) {
|
|
return NextResponse.json({ error: '您没有下载权限,请联系管理员' }, { status: 403 })
|
|
}
|
|
|
|
const materialId = params.id
|
|
const body = await request.json()
|
|
const skuIds = Array.isArray(body?.skuIds) ? body.skuIds : []
|
|
const downloadType: DownloadType = body?.downloadType === 'white-background' ? 'white-background' : 'model'
|
|
|
|
if (skuIds.length === 0) {
|
|
return NextResponse.json({ error: '请选择要下载的 SKU' }, { status: 400 })
|
|
}
|
|
|
|
const material = await prisma.material.findFirst({
|
|
where: { id: materialId, deletedAt: null },
|
|
select: { id: true, productModel: true, materialType: true, userId: true },
|
|
})
|
|
|
|
if (!material) {
|
|
return NextResponse.json({ error: '素材不存在或已被删除' }, { status: 404 })
|
|
}
|
|
|
|
if (material.materialType !== '标准模型') {
|
|
return NextResponse.json({ error: '只有标准模型类型才支持 SKU 批量下载' }, { status: 400 })
|
|
}
|
|
|
|
if (!(await canDownloadSkuMaterial(session.user as any, material))) {
|
|
return NextResponse.json({ error: '无权限下载此素材' }, { status: 403 })
|
|
}
|
|
|
|
const skus = await prisma.materialSku.findMany({
|
|
where: {
|
|
id: { in: skuIds },
|
|
materialId,
|
|
deletedAt: null,
|
|
},
|
|
select: {
|
|
id: true,
|
|
skuName: true,
|
|
filePath: true,
|
|
fileName: true,
|
|
},
|
|
orderBy: { sortOrder: 'asc' },
|
|
})
|
|
|
|
if (skus.length === 0) {
|
|
return NextResponse.json({ error: '没有找到可下载的 SKU' }, { status: 404 })
|
|
}
|
|
|
|
const zip = new JSZip()
|
|
let addedCount = 0
|
|
|
|
if (downloadType === 'white-background') {
|
|
const whiteBackgroundAssets = await prisma.materialAsset.findMany({
|
|
where: {
|
|
materialId,
|
|
OR: skuIds.flatMap((skuId: string) => getWhiteBackgroundAssetWhereForSku(skuId).OR),
|
|
},
|
|
select: {
|
|
assetType: true,
|
|
fileUrl: true,
|
|
fileName: true,
|
|
sortOrder: true,
|
|
},
|
|
})
|
|
|
|
const whiteBackgroundMap = groupWhiteBackgroundAssetsBySku<WhiteBackgroundAssetRecord>(
|
|
whiteBackgroundAssets,
|
|
)
|
|
|
|
for (const sku of skus) {
|
|
const whiteBackgroundGroup = whiteBackgroundMap.get(sku.id)
|
|
if (!whiteBackgroundGroup || !hasAnyWhiteBackgroundAssetGroup(whiteBackgroundGroup)) {
|
|
continue
|
|
}
|
|
|
|
const { fixedEntries, otherEntries } = getWhiteBackgroundZipEntries(whiteBackgroundGroup)
|
|
if (fixedEntries.length === 0 && otherEntries.length === 0) {
|
|
continue
|
|
}
|
|
|
|
try {
|
|
for (const entry of fixedEntries) {
|
|
const filePath = resolveLocalFilePath(entry.fileUrl)
|
|
if (!filePath) {
|
|
throw new Error('白底图文件路径无效')
|
|
}
|
|
const fileBuffer = await fs.readFile(filePath)
|
|
zip.file(`${sku.skuName}/${entry.fileName}`, fileBuffer)
|
|
}
|
|
|
|
for (const entry of otherEntries) {
|
|
const filePath = resolveLocalFilePath(entry.fileUrl)
|
|
if (!filePath) {
|
|
throw new Error('白底图文件路径无效')
|
|
}
|
|
const fileBuffer = await fs.readFile(filePath)
|
|
zip.file(`${sku.skuName}/other/${entry.fileName}`, fileBuffer)
|
|
}
|
|
|
|
addedCount++
|
|
} catch (error) {
|
|
console.error(`[SKU Batch Download] 添加白底图失败: ${sku.id}`, error)
|
|
}
|
|
}
|
|
} else {
|
|
for (const sku of skus) {
|
|
try {
|
|
if (!sku.filePath) {
|
|
continue
|
|
}
|
|
|
|
const filePath = resolveLocalFilePath(sku.filePath)
|
|
if (!filePath) {
|
|
throw new Error('SKU 文件路径无效')
|
|
}
|
|
const fileName = resolveModelDownloadFileName(
|
|
sku.skuName,
|
|
sku.fileName || null,
|
|
sku.filePath || null,
|
|
)
|
|
|
|
const fileBuffer = await fs.readFile(filePath)
|
|
zip.file(fileName, fileBuffer)
|
|
addedCount++
|
|
} catch (error) {
|
|
console.error(`[SKU Batch Download] 添加模型失败: ${sku.id}`, error)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (addedCount === 0) {
|
|
return NextResponse.json(
|
|
{ error: downloadType === 'white-background' ? '没有可下载的白底图文件' : '没有可下载的模型文件' },
|
|
{ status: 404 }
|
|
)
|
|
}
|
|
|
|
const zipBuffer = await zip.generateAsync({
|
|
type: 'nodebuffer',
|
|
compression: 'DEFLATE',
|
|
compressionOptions: { level: 6 },
|
|
})
|
|
|
|
const filename = `${material.productModel}_${downloadType === 'white-background' ? 'SKU白底图' : 'SKU模型'}_${Date.now()}.zip`
|
|
|
|
recordBatchDownloadLog(materialId, userId).catch((error) => {
|
|
console.error('[SKU Batch Download] 记录下载日志失败:', error)
|
|
})
|
|
|
|
return new NextResponse(zipBuffer as any, {
|
|
headers: {
|
|
'Content-Type': 'application/zip',
|
|
'Content-Length': zipBuffer.length.toString(),
|
|
'Content-Disposition': `attachment; filename="${encodeURIComponent(filename)}"`,
|
|
},
|
|
})
|
|
} catch (error) {
|
|
console.error('[SKU Batch Download] 下载失败:', error)
|
|
return NextResponse.json(
|
|
{ error: '下载失败: ' + (error instanceof Error ? error.message : '未知错误') },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
async function recordBatchDownloadLog(materialId: string, userId: string) {
|
|
await ensureMaterialDownloadLogsTable()
|
|
await Promise.all([
|
|
prisma.materialDownloadLog.create({
|
|
data: {
|
|
materialId,
|
|
userId,
|
|
downloadedAt: new Date(),
|
|
},
|
|
}),
|
|
prisma.material.update({
|
|
where: { id: materialId },
|
|
data: {
|
|
downloadCount: {
|
|
increment: 1,
|
|
},
|
|
},
|
|
}),
|
|
])
|
|
}
|