const { PrismaClient } = require('@prisma/client'); const fs = require('fs'); const path = require('path'); const https = require('https'); const crypto = require('crypto'); // 简单的密码hash函数(临时使用,建议后续修改密码) function hashPassword(password) { return crypto.createHash('sha256').update(password).digest('hex'); } const prisma = new PrismaClient(); // 配置(容器内路径) const UPLOADS_DIR = '/app/uploads'; const S3_BUCKET_URL = process.env.RESTORE_S3_BUCKET_URL; const S3_PUBLIC_URL = process.env.RESTORE_S3_PUBLIC_URL; // 默认配置 const DEFAULT_CATEGORY = '默认店铺'; const DEFAULT_SCENE = '默认场景'; const DEFAULT_RENDERER = 'CR'; const DEFAULT_MATERIAL_TYPE = '3DMAX'; const ADMIN_EMAIL = 'admin@youyidian.com'; const ADMIN_USERNAME = 'admin'; const ADMIN_PASSWORD = 'admin123456'; // 解析S3 XML响应 function parseS3XML(xml) { const keys = []; const keyRegex = /([^<]+)<\/Key>/g; let match; while ((match = keyRegex.exec(xml)) !== null) { keys.push(match[1]); } return keys; } // 获取S3中的预览图列表 async function getS3PreviewFiles() { return new Promise((resolve, reject) => { https.get(S3_BUCKET_URL, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { console.log(`📊 S3响应大小: ${data.length} 字节`); const allKeys = parseS3XML(data); console.log(`📊 解析到 ${allKeys.length} 个总文件`); const previewFiles = allKeys.filter(key => key.includes('preview-')); console.log(`✅ 从S3找到 ${previewFiles.length} 个预览图`); resolve(previewFiles); }); }).on('error', reject); }); } // 获取本地上传的素材文件 function getLocalMaterialFiles(uploadsDir) { try { const files = fs.readdirSync(uploadsDir); const materialFiles = files.filter(f => f.startsWith('zip-') && (f.endsWith('.zip') || f.endsWith('.rar') || f.endsWith('.7z')) ); console.log(`✅ 从本地找到 ${materialFiles.length} 个素材文件`); return materialFiles; } catch (error) { console.error('❌ 读取本地文件失败:', error.message); return []; } } // 提取时间戳 function extractTimestamp(filename) { const match = filename.match(/-(\d{13})-/); return match ? parseInt(match[1]) : null; } // 从文件名提取原始名称 function extractOriginalName(filename) { // zip-1760682783914-SZ0168+策划+涂涂(欧意朗)+王异主图1.7z const parts = filename.split('-'); if (parts.length >= 3) { const namePart = parts.slice(2).join('-'); return namePart.replace(/\.(zip|rar|7z)$/i, ''); } return filename; } // 匹配预览图和素材文件 function matchFiles(previewFiles, materialFiles) { const matches = []; let withPreview = 0; let withoutPreview = 0; let skipped = 0; for (const materialFile of materialFiles) { const materialTimestamp = extractTimestamp(materialFile); if (!materialTimestamp) { console.log(`⚠️ 无法提取时间戳: ${materialFile}`); skipped++; continue; } // 查找时间戳相近的预览图(1秒内) 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 }); if (matchedPreview) { withPreview++; } else { withoutPreview++; } } console.log(`✅ 总计 ${matches.length} 个素材 (${withPreview} 个有预览图, ${withoutPreview} 个无预览图)`); return matches; } // 创建管理员用户 async function createAdminUser() { try { const hashedPassword = hashPassword(ADMIN_PASSWORD); const admin = await prisma.user.create({ data: { email: ADMIN_EMAIL, username: ADMIN_USERNAME, password: hashedPassword, role: 'ADMIN', isActive: true, canDownload: true } }); console.log(`✅ 创建管理员用户: ${ADMIN_USERNAME}`); return admin; } catch (error) { if (error.code === 'P2002') { const admin = await prisma.user.findUnique({ where: { email: ADMIN_EMAIL } }); console.log(`ℹ️ 管理员用户已存在`); return admin; } throw error; } } // 创建默认分类 async function createDefaultCategory() { try { const category = await prisma.category.create({ data: { name: DEFAULT_CATEGORY } }); console.log(`✅ 创建默认分类: ${DEFAULT_CATEGORY}`); return category; } catch (error) { if (error.code === 'P2002') { const category = await prisma.category.findFirst({ where: { name: DEFAULT_CATEGORY } }); console.log(`ℹ️ 默认分类已存在`); return category; } throw error; } } // 创建默认场景 async function createDefaultScene() { try { const scene = await prisma.scene.create({ data: { name: DEFAULT_SCENE } }); console.log(`✅ 创建默认场景: ${DEFAULT_SCENE}`); return scene; } catch (error) { if (error.code === 'P2002') { const scene = await prisma.scene.findFirst({ where: { name: DEFAULT_SCENE } }); console.log(`ℹ️ 默认场景已存在`); return scene; } throw error; } } // 恢复素材数据 async function restoreMaterials(matches, adminUserId, uploadsDir) { let successCount = 0; let failCount = 0; // 容器外的实际路径(用于数据库存储) const HOST_UPLOADS_DIR = '/mnt/PoolOne/apps/youyidian-3d/uploads'; for (const match of matches) { try { const originalName = extractOriginalName(match.materialFile); const containerPath = path.join(uploadsDir, match.materialFile); const hostPath = path.join(HOST_UPLOADS_DIR, match.materialFile); const stats = fs.statSync(containerPath); const material = await prisma.material.create({ data: { previewUrl: match.previewFile ? `${S3_PUBLIC_URL}${match.previewFile}` : '', productModel: originalName.substring(0, 50), // 作为SPU 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: adminUserId } }); successCount++; console.log(`✅ [${successCount}/${matches.length}] 恢复: ${originalName}`); } catch (error) { failCount++; console.error(`❌ 恢复失败: ${match.materialFile}`, error.message); } } return { successCount, failCount }; } // 主函数 async function main() { console.log('🚀 开始数据恢复...\n'); try { // 1. 创建管理员用户 console.log('📝 步骤1: 创建管理员用户'); const admin = await createAdminUser(); console.log(''); // 2. 创建默认分类和场景 console.log('📝 步骤2: 创建默认分类和场景'); await createDefaultCategory(); await createDefaultScene(); console.log(''); // 3. 获取预览图列表 console.log('📝 步骤3: 获取S3预览图列表'); const previewFiles = await getS3PreviewFiles(); console.log(''); // 4. 获取本地素材文件 console.log('📝 步骤4: 扫描本地素材文件'); const materialFiles = getLocalMaterialFiles(UPLOADS_DIR); console.log(''); // 5. 匹配文件 console.log('📝 步骤5: 匹配预览图和素材文件'); const matches = matchFiles(previewFiles, materialFiles); console.log(''); // 6. 恢复数据 console.log('📝 步骤6: 恢复素材数据到数据库'); const { successCount, failCount } = await restoreMaterials(matches, admin.id, UPLOADS_DIR); console.log(''); // 7. 统计 console.log('📊 恢复完成统计:'); console.log(` ✅ 成功: ${successCount} 个`); console.log(` ❌ 失败: ${failCount} 个`); console.log(` 📁 总计: ${materialFiles.length} 个文件`); console.log(''); console.log('🎉 数据恢复完成!'); console.log(''); console.log('📌 默认信息:'); console.log(` 管理员账号: ${ADMIN_USERNAME}`); console.log(` 管理员密码: ${ADMIN_PASSWORD}`); console.log(` 默认分类: ${DEFAULT_CATEGORY}`); console.log(` 默认场景: ${DEFAULT_SCENE}`); console.log(''); console.log('⚠️ 提示: 请登录后台修改素材的SPU、分类、场景等信息'); } catch (error) { console.error('❌ 恢复过程出错:', error); throw error; } finally { await prisma.$disconnect(); } } main().catch(console.error);