import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { readFile, mkdir, rm, readdir } from 'fs/promises' import { join } from 'path' import sharp from 'sharp' import { sliceImage } from '@/lib/image-processor' import { hasSystemAdminPermission, isSuperAdmin } from '@/lib/department-permission' import { isPathInAllowedDirs } from '@/lib/path-security' import { getUploadRootDir, resolveStoredUploadPath } from '@/lib/upload-paths' 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 userId = (session.user as any).id const userRole = (session.user as any).role const isSuperAdminUser = await isSuperAdmin(userId) if (!hasSystemAdminPermission(userRole, isSuperAdminUser)) { return NextResponse.json({ error: '无权限操作' }, { status: 403 }) } 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 uploadRootDir = getUploadRootDir() const localPath = resolveStoredUploadPath(detailAsset.fileUrl) if (!isPathInAllowedDirs(localPath, [uploadRootDir])) { return NextResponse.json({ error: '详情图文件路径无效' }, { status: 400 }) } // 创建临时目录 const tempDir = join(uploadRootDir, '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 } ) } }