305 lines
9.0 KiB
JavaScript
305 lines
9.0 KiB
JavaScript
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>([^<]+)<\/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);
|
||
|