// 数据恢复API - 从S3和本地文件恢复素材数据 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'; const UPLOADS_DIR = '/app/uploads'; const ADMIN_EMAIL = 'admin'; const ADMIN_USERNAME = 'admin'; const ADMIN_PASSWORD = 'admin123456'; const DEFAULT_CATEGORY = '默认店铺'; const DEFAULT_SCENE = '默认场景'; // 从S3获取预览图列表 async function fetchS3PreviewList(): Promise { return new Promise((resolve, reject) => { https.get('https://s3.1080p.hk/jsxiangche/?max-keys=2000', (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { const keys = parseS3ListResponse(data); resolve(keys.filter(key => key.includes('preview-'))); }); }).on('error', reject); }); } // 解析S3列表响应 function parseS3ListResponse(xml: string): string[] { const keys: string[] = []; const keyRegex = /([^<]+)<\/Key>/g; let match; while ((match = keyRegex.exec(xml)) !== null) { keys.push(match[1]); } return keys; } // 从文件名提取时间戳 function extractTimestamp(filename: string): number | null { const match = filename.match(/-(\d{13})-/); return match ? parseInt(match[1]) : null; } // 扫描本地素材文件 function scanLocalMaterials(dir: string): string[] { try { return fs.readdirSync(dir).filter(file => file.startsWith('zip-') && (file.endsWith('.zip') || file.endsWith('.rar') || file.endsWith('.7z')) ); } catch (error: any) { console.error('读取本地文件失败:', error.message); return []; } } // 匹配预览图和素材文件 function matchPreviewsWithMaterials( previews: string[], materials: string[] ): Array<{ previewFile: string | null; materialFile: string; timestamp: number | null }> { const matches: Array<{ previewFile: string | null; materialFile: string; timestamp: number | null }> = []; for (const material of materials) { const materialTimestamp = extractTimestamp(material); if (!materialTimestamp) continue; // 查找时间戳相近的预览图(允许1秒误差) const matchedPreview = previews.find(preview => { const previewTimestamp = extractTimestamp(preview); if (!previewTimestamp) return false; return Math.abs(previewTimestamp - materialTimestamp) < 1000; }); matches.push({ previewFile: matchedPreview || null, materialFile: material, timestamp: materialTimestamp, }); } return matches; } // 从文件名提取产品名称 function extractProductName(filename: string): string { const parts = filename.split('-'); if (parts.length >= 3) { return parts.slice(2).join('-').replace(/\.(zip|rar|7z)$/i, ''); } return filename; } // POST - 执行数据恢复 export async function POST(request: NextRequest) { try { let adminUser; const logs: string[] = []; logs.push('🚀 开始数据恢复...\n'); // 步骤1: 创建管理员用户 logs.push('📝 步骤1: 创建管理员用户'); try { const hashedPassword = crypto.createHash('sha256').update(ADMIN_PASSWORD).digest('hex'); adminUser = await prisma.user.create({ data: { email: ADMIN_EMAIL, username: ADMIN_USERNAME, password: hashedPassword, role: 'ADMIN', isActive: true, canDownload: true, }, }); logs.push(`✅ 创建管理员用户: ${ADMIN_USERNAME}`); } catch (error: any) { if (error.code === 'P2002') { adminUser = await prisma.user.findUnique({ where: { email: ADMIN_EMAIL } }); logs.push(`ℹ️ 管理员用户已存在`); } else { throw error; } } if (!adminUser) { throw new Error('无法创建或找到管理员用户'); } // 步骤2: 创建默认分类和场景 logs.push('\n📝 步骤2: 创建默认分类和场景'); try { await prisma.category.create({ data: { name: DEFAULT_CATEGORY } }); logs.push(`✅ 创建默认分类: ${DEFAULT_CATEGORY}`); } catch (error: any) { if (error.code === 'P2002') { logs.push(`ℹ️ 默认分类已存在`); } } try { await prisma.scene.create({ data: { name: DEFAULT_SCENE } }); logs.push(`✅ 创建默认场景: ${DEFAULT_SCENE}`); } catch (error: any) { if (error.code === 'P2002') { logs.push(`ℹ️ 默认场景已存在`); } } // 步骤3: 获取S3预览图列表 logs.push('\n📝 步骤3: 获取S3预览图列表'); const s3Previews = await fetchS3PreviewList(); logs.push(`✅ 从S3找到 ${s3Previews.length} 个预览图`); // 步骤4: 扫描本地素材文件 logs.push('\n📝 步骤4: 扫描本地素材文件'); const localMaterials = scanLocalMaterials(UPLOADS_DIR); logs.push(`✅ 从本地找到 ${localMaterials.length} 个素材文件`); // 步骤5: 匹配预览图和素材文件 logs.push('\n📝 步骤5: 匹配预览图和素材文件'); const matches = matchPreviewsWithMaterials(s3Previews, localMaterials); const withPreview = matches.filter(m => m.previewFile).length; const withoutPreview = matches.length - withPreview; logs.push(`✅ 总计 ${matches.length} 个素材 (${withPreview} 个有预览图, ${withoutPreview} 个无预览图)`); // 步骤6: 恢复素材数据到数据库 logs.push('\n📝 步骤6: 恢复素材数据到数据库'); let successCount = 0; let failCount = 0; for (const match of matches) { try { const productName = extractProductName(match.materialFile); const localPath = path.join(UPLOADS_DIR, match.materialFile); const dockerPath = path.join('/mnt/PoolOne/apps/youyidian-3d/uploads', match.materialFile); const stats = fs.statSync(localPath); await prisma.material.create({ data: { previewUrl: match.previewFile ? `https://s3.1080p.hk/jsxiangche/${match.previewFile}` : '', productModel: productName.substring(0, 50), categoryName: DEFAULT_CATEGORY, sceneName: DEFAULT_SCENE, materialType: '3D场景', renderer: 'CR', authorName: ADMIN_USERNAME, zipFileName: match.materialFile, zipFilePath: dockerPath, zipFileSize: BigInt(stats.size), downloadCount: 0, likeCount: 0, userId: adminUser.id, }, }); successCount++; if (successCount % 10 === 0) { logs.push(`✅ 已恢复: ${successCount}/${matches.length}`); } } catch (error: any) { failCount++; logs.push(`❌ 恢复失败: ${match.materialFile} - ${error.message}`); } } logs.push(`\n📊 恢复完成统计:`); logs.push(` ✅ 成功: ${successCount} 个`); logs.push(` ❌ 失败: ${failCount} 个`); logs.push(` 📁 总计: ${matches.length} 个文件`); logs.push(`\n🎉 数据恢复完成!`); logs.push(`\n📌 默认信息:`); logs.push(` 管理员账号: ${ADMIN_USERNAME}`); logs.push(` 管理员密码: ${ADMIN_PASSWORD}`); logs.push(` 默认分类: ${DEFAULT_CATEGORY}`); logs.push(` 默认场景: ${DEFAULT_SCENE}`); logs.push(`\n⚠️ 提示: 请登录后台修改素材的SPU、分类、场景等信息`); return NextResponse.json({ success: true, message: logs.join('\n'), stats: { successCount, failCount, total: matches.length, }, }); } catch (error: any) { return NextResponse.json( { success: false, message: error.message, }, { status: 500 } ); } }