import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import https from 'https'; import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; // 简单的密码hash函数(临时使用) function hashPassword(password: string): string { return crypto.createHash('sha256').update(password).digest('hex'); } // 配置 const UPLOADS_DIR = '/app/uploads'; const S3_BUCKET_URL = process.env.RESTORE_S3_BUCKET_URL as string; const S3_PUBLIC_URL = process.env.RESTORE_S3_PUBLIC_URL as string; // 默认配置 const ADMIN_EMAIL = 'admin'; const ADMIN_USERNAME = 'admin'; const ADMIN_PASSWORD = 'admin123456'; const DEFAULT_CATEGORY = '默认店铺'; const DEFAULT_SCENE = '默认场景'; const DEFAULT_RENDERER = 'CR'; const DEFAULT_MATERIAL_TYPE = '3DMAX'; // 解析S3 XML function parseS3XML(xml: string): string[] { const keys: string[] = []; const keyRegex = /([^<]+)<\/Key>/g; let match; while ((match = keyRegex.exec(xml)) !== null) { keys.push(match[1]); } return keys; } // 获取S3预览图列表 async function getS3PreviewFiles(): Promise { return new Promise((resolve, reject) => { https.get(S3_BUCKET_URL, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { const allKeys = parseS3XML(data); const previewFiles = allKeys.filter(key => key.includes('preview-')); resolve(previewFiles); }); }).on('error', reject); }); } // 获取本地素材文件 function getLocalMaterialFiles(uploadsDir: string): string[] { try { const files = fs.readdirSync(uploadsDir); return files.filter(f => f.startsWith('zip-') && (f.endsWith('.zip') || f.endsWith('.rar') || f.endsWith('.7z')) ); } catch (error: any) { console.error('读取本地文件失败:', error.message); return []; } } // 提取时间戳 function extractTimestamp(filename: string): number | null { const match = filename.match(/-(\d{13})-/); return match ? parseInt(match[1]) : null; } // 提取原始名称 function extractOriginalName(filename: string): string { const parts = filename.split('-'); if (parts.length >= 3) { const namePart = parts.slice(2).join('-'); return namePart.replace(/\.(zip|rar|7z)$/i, ''); } return filename; } // 匹配文件 interface Match { previewFile: string | null; materialFile: string; timestamp: number; } function matchFiles(previewFiles: string[], materialFiles: string[]): Match[] { const matches: Match[] = []; for (const materialFile of materialFiles) { const materialTimestamp = extractTimestamp(materialFile); if (!materialTimestamp) continue; const matchedPreview = previewFiles.find(previewFile => { const previewTimestamp = extractTimestamp(previewFile); if (!previewTimestamp) return false; return Math.abs(previewTimestamp - materialTimestamp) < 1000; }); matches.push({ previewFile: matchedPreview || null, materialFile: materialFile, timestamp: materialTimestamp }); } return matches; } export async function POST(req: NextRequest) { try { const output: string[] = []; output.push('🚀 开始数据恢复...\n'); // 创建管理员用户 output.push('📝 步骤1: 创建管理员用户'); let admin; try { const hashedPassword = hashPassword(ADMIN_PASSWORD); admin = await prisma.user.create({ data: { email: ADMIN_EMAIL, username: ADMIN_USERNAME, password: hashedPassword, role: 'ADMIN', isActive: true, canDownload: true } }); output.push(`✅ 创建管理员用户: ${ADMIN_USERNAME}`); } catch (error: any) { if (error.code === 'P2002') { admin = await prisma.user.findUnique({ where: { email: ADMIN_EMAIL } }); output.push(`ℹ️ 管理员用户已存在`); } else { throw error; } } if (!admin) { throw new Error('无法创建或找到管理员用户'); } // 创建默认分类 output.push('\n📝 步骤2: 创建默认分类和场景'); try { await prisma.category.create({ data: { name: DEFAULT_CATEGORY } }); output.push(`✅ 创建默认分类: ${DEFAULT_CATEGORY}`); } catch (error: any) { if (error.code === 'P2002') { output.push(`ℹ️ 默认分类已存在`); } } // 创建默认场景 try { await prisma.scene.create({ data: { name: DEFAULT_SCENE } }); output.push(`✅ 创建默认场景: ${DEFAULT_SCENE}`); } catch (error: any) { if (error.code === 'P2002') { output.push(`ℹ️ 默认场景已存在`); } } // 获取S3预览图 output.push('\n📝 步骤3: 获取S3预览图列表'); const previewFiles = await getS3PreviewFiles(); output.push(`✅ 从S3找到 ${previewFiles.length} 个预览图`); // 获取本地素材文件 output.push('\n📝 步骤4: 扫描本地素材文件'); const materialFiles = getLocalMaterialFiles(UPLOADS_DIR); output.push(`✅ 从本地找到 ${materialFiles.length} 个素材文件`); // 匹配文件 output.push('\n📝 步骤5: 匹配预览图和素材文件'); const matches = matchFiles(previewFiles, materialFiles); const withPreview = matches.filter(m => m.previewFile).length; const withoutPreview = matches.length - withPreview; output.push(`✅ 总计 ${matches.length} 个素材 (${withPreview} 个有预览图, ${withoutPreview} 个无预览图)`); // 恢复素材数据 output.push('\n📝 步骤6: 恢复素材数据到数据库'); const HOST_UPLOADS_DIR = '/mnt/PoolOne/apps/youyidian-3d/uploads'; let successCount = 0; let failCount = 0; for (const match of matches) { try { const originalName = extractOriginalName(match.materialFile); const containerPath = path.join(UPLOADS_DIR, match.materialFile); const hostPath = path.join(HOST_UPLOADS_DIR, match.materialFile); const stats = fs.statSync(containerPath); await prisma.material.create({ data: { previewUrl: match.previewFile ? `${S3_PUBLIC_URL}${match.previewFile}` : '', productModel: originalName.substring(0, 50), categoryName: DEFAULT_CATEGORY, sceneName: DEFAULT_SCENE, materialType: DEFAULT_MATERIAL_TYPE, renderer: DEFAULT_RENDERER, authorName: ADMIN_USERNAME, zipFileName: match.materialFile, zipFilePath: hostPath, zipFileSize: BigInt(stats.size), downloadCount: 0, likeCount: 0, userId: admin.id } }); successCount++; if (successCount % 10 === 0) { output.push(`✅ 已恢复: ${successCount}/${matches.length}`); } } catch (error: any) { failCount++; output.push(`❌ 恢复失败: ${match.materialFile} - ${error.message}`); } } output.push(`\n📊 恢复完成统计:`); output.push(` ✅ 成功: ${successCount} 个`); output.push(` ❌ 失败: ${failCount} 个`); output.push(` 📁 总计: ${matches.length} 个文件`); output.push(`\n🎉 数据恢复完成!`); output.push(`\n📌 默认信息:`); output.push(` 管理员账号: ${ADMIN_USERNAME}`); output.push(` 管理员密码: ${ADMIN_PASSWORD}`); output.push(` 默认分类: ${DEFAULT_CATEGORY}`); output.push(` 默认场景: ${DEFAULT_SCENE}`); output.push(`\n⚠️ 提示: 请登录后台修改素材的SPU、分类、场景等信息`); return NextResponse.json({ success: true, message: output.join('\n'), stats: { successCount, failCount, total: matches.length } }); } catch (error: any) { return NextResponse.json({ success: false, message: error.message }, { status: 500 }); } }