2026-05-07 16:37:22 +08:00

162 lines
5.3 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 { readFile, unlink, mkdir, rm, readdir } from 'fs/promises'
import { join } from 'path'
import sharp from 'sharp'
import { sliceImage } from '@/lib/image-processor'
import {
buildTaskDetailPublicDisplayPath,
uploadPublicDisplayToS3,
} from '@/lib/public-display'
import AdmZip from 'adm-zip'
export async function POST(request: NextRequest) {
try {
// 验证管理员权限
const session = await getServerSession(authOptions)
if (!session || !(session.user as any).id) {
return NextResponse.json({ error: '未登录' }, { status: 401 })
}
const body = await request.json()
const { materialId } = body
if (!materialId) {
return NextResponse.json({ error: '缺少 materialId' }, { status: 400 })
}
console.log('[Regenerate Preview] Material ID:', materialId)
// 获取素材详情
const material = await prisma.material.findUnique({
where: { id: materialId },
})
if (!material) {
return NextResponse.json({ error: '素材不存在' }, { status: 404 })
}
// 查找详情图资产
const detailAssets = await prisma.materialAsset.findMany({
where: {
materialId,
assetType: 'detail',
},
orderBy: { sortOrder: 'asc' },
})
if (detailAssets.length === 0) {
return NextResponse.json({ error: '没有找到详情图' }, { status: 404 })
}
// 获取第一个详情图
const detailAsset = detailAssets[0]
console.log('[Regenerate Preview] Detail asset:', detailAsset.fileUrl)
// 如果是本地文件,解压并处理
if (detailAsset.fileUrl.startsWith('uploads/')) {
const localPath = join(process.cwd(), detailAsset.fileUrl)
// 创建临时目录
const tempDir = join(process.cwd(), 'uploads', 'temp', `regenerate-${materialId}`)
await mkdir(tempDir, { recursive: true })
// ⚠️ 安全修复:使用 AdmZip 替代 unzip 命令,防止命令注入(R40)
try {
const zip = new AdmZip(localPath)
zip.extractAllTo(tempDir, true)
console.log('[Regenerate Preview] Unzip complete')
} catch (unzipError) {
console.error('[Regenerate Preview] Unzip failed:', unzipError)
return NextResponse.json({ error: '解压文件失败' }, { status: 400 })
}
// ⚠️ 安全修复:使用 fs.readdir 替代 ls 命令,防止命令注入(R40)
const files = await readdir(tempDir)
const imageFile = files.find(f => /\.(jpg|jpeg|png|webp)$/i.test(f))
if (!imageFile) {
return NextResponse.json({ error: '未找到图片文件' }, { status: 400 })
}
const imagePath = join(tempDir, imageFile)
console.log('[Regenerate Preview] Image path:', imagePath)
// 读取图片
const buffer = await readFile(imagePath)
const metadata = await sharp(buffer).metadata()
console.log('[Regenerate Preview] Image size:', metadata.width, 'x', metadata.height)
// 切片处理
const sliceBuffers = await sliceImage(buffer, 1500, 200)
console.log('[Regenerate Preview] Slices:', sliceBuffers.length)
// 上传新预览图
const timestamp = Date.now()
const newPreviewUrls: string[] = []
for (let i = 0; i < sliceBuffers.length; i++) {
const fileName = `detail-preview-${timestamp}-${i}-slice${i}.webp`
const url = await uploadPublicDisplayToS3(
sliceBuffers[i],
buildTaskDetailPublicDisplayPath(materialId, 'detail', fileName),
'image/webp',
)
newPreviewUrls.push(url)
console.log('[Regenerate Preview] Uploaded slice', i + 1, '/', sliceBuffers.length, ':', url)
}
// 删除旧的预览图记录
await prisma.materialAsset.deleteMany({
where: {
materialId,
assetType: 'detail-preview',
},
})
// 创建新的预览图记录
const assetRecords = newPreviewUrls.map((url, index) => ({
materialId,
assetType: 'detail-preview',
fileUrl: url,
fileName: `detail-preview-${index}.webp`,
sortOrder: index,
}))
await prisma.materialAsset.createMany({
data: assetRecords,
})
// 更新素材的 previewUrl(用于首页卡片显示)
await prisma.material.update({
where: { id: materialId },
data: {
previewUrl: newPreviewUrls[0], // 使用第一张切片作为卡片预览图
},
})
console.log('[Regenerate Preview] Updated material.previewUrl:', newPreviewUrls[0])
// ⚠️ 安全修复:使用 fs.rm 替代 rm 命令,防止命令注入(R40)
await rm(tempDir, { recursive: true, force: true })
console.log('[Regenerate Preview] Complete! Generated', newPreviewUrls.length, 'previews')
return NextResponse.json({
success: true,
previewCount: newPreviewUrls.length,
previewUrls: newPreviewUrls,
})
}
return NextResponse.json({ error: '不支持的文件类型' }, { status: 400 })
} catch (error) {
console.error('[Regenerate Preview] Error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : '服务器错误' },
{ status: 500 }
)
}
}