255 lines
7.5 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'
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<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 },
})
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<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 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,
},
},
}),
])
}