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' 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 { if (path.isAbsolute(fileUrl)) { return fileUrl } return path.join(process.cwd(), fileUrl) } function getWhiteBackgroundZipEntries(group: GroupedWhiteBackgroundAssets) { 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 }, }) if (!material) { return NextResponse.json({ error: '素材不存在或已被删除' }, { status: 404 }) } if (material.materialType !== '标准模型') { return NextResponse.json({ error: '只有标准模型类型才支持 SKU 批量下载' }, { status: 400 }) } 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( 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 fileBuffer = await fs.readFile(resolveLocalFilePath(entry.fileUrl)) zip.file(`${sku.skuName}/${entry.fileName}`, fileBuffer) } for (const entry of otherEntries) { const fileBuffer = await fs.readFile(resolveLocalFilePath(entry.fileUrl)) 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 = path.join(process.cwd(), 'uploads', sku.filePath) 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, }, }, }), ]) }