gongxiangp2/restore-data.js
chenpei d17f6df2c7 feat: 部门管理和文件上传优化
新功能:
1. 部门管理功能
   - 管理员可在系统设置中添加/删除部门
   - 用户注册时从下拉框选择部门
   - 新增Department数据库表和API

2. 文件上传优化
   - 支持直接上传源文件(.max/.psd/.ai/.cad/.dwg/.c4d)
   - 自动识别文件类型并设置素材类型
   - 压缩包需手动选择类型
   - 优化拖拽上传体验

数据库变更:
- 新增departments表
- User表新增department字段
2025-10-26 14:06:19 +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 = 'https://s3.1080p.hk/jsxiangche/?max-keys=2000';
const S3_PUBLIC_URL = 'https://s3.1080p.hk/jsxiangche/';
// 默认配置
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);