162 lines
5.3 KiB
TypeScript
162 lines
5.3 KiB
TypeScript
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 }
|
||
)
|
||
}
|
||
}
|