flnxi-Wu/scripts/tools/restore-data.js
WuYongJie f646b71813
Some checks failed
CI Check / check (push) Has been cancelled
chore: 初始化本地仓库并同步最近改动
2025-11-18 17:50:16 +08:00

305 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);