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

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

数据库变更:
- 新增departments表
- User表新增department字段
2025-10-26 14:06:19 +08:00

255 lines
7.9 KiB
TypeScript
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.

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';
// 简单的密码hash函数(临时使用)
function hashPassword(password: string): string {
return crypto.createHash('sha256').update(password).digest('hex');
}
// 配置
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 ADMIN_EMAIL = 'admin';
const ADMIN_USERNAME = 'admin';
const ADMIN_PASSWORD = 'admin123456';
const DEFAULT_CATEGORY = '默认店铺';
const DEFAULT_SCENE = '默认场景';
const DEFAULT_RENDERER = 'CR';
const DEFAULT_MATERIAL_TYPE = '3DMAX';
// 解析S3 XML
function parseS3XML(xml: string): string[] {
const keys: string[] = [];
const keyRegex = /<Key>([^<]+)<\/Key>/g;
let match;
while ((match = keyRegex.exec(xml)) !== null) {
keys.push(match[1]);
}
return keys;
}
// 获取S3预览图列表
async function getS3PreviewFiles(): Promise<string[]> {
return new Promise((resolve, reject) => {
https.get(S3_BUCKET_URL, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
const allKeys = parseS3XML(data);
const previewFiles = allKeys.filter(key => key.includes('preview-'));
resolve(previewFiles);
});
}).on('error', reject);
});
}
// 获取本地素材文件
function getLocalMaterialFiles(uploadsDir: string): string[] {
try {
const files = fs.readdirSync(uploadsDir);
return files.filter(f =>
f.startsWith('zip-') && (f.endsWith('.zip') || f.endsWith('.rar') || f.endsWith('.7z'))
);
} catch (error: any) {
console.error('读取本地文件失败:', error.message);
return [];
}
}
// 提取时间戳
function extractTimestamp(filename: string): number | null {
const match = filename.match(/-(\d{13})-/);
return match ? parseInt(match[1]) : null;
}
// 提取原始名称
function extractOriginalName(filename: string): string {
const parts = filename.split('-');
if (parts.length >= 3) {
const namePart = parts.slice(2).join('-');
return namePart.replace(/\.(zip|rar|7z)$/i, '');
}
return filename;
}
// 匹配文件
interface Match {
previewFile: string | null;
materialFile: string;
timestamp: number;
}
function matchFiles(previewFiles: string[], materialFiles: string[]): Match[] {
const matches: Match[] = [];
for (const materialFile of materialFiles) {
const materialTimestamp = extractTimestamp(materialFile);
if (!materialTimestamp) continue;
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
});
}
return matches;
}
export async function POST(req: NextRequest) {
try {
const output: string[] = [];
output.push('🚀 开始数据恢复...\n');
// 创建管理员用户
output.push('📝 步骤1: 创建管理员用户');
let admin;
try {
const hashedPassword = hashPassword(ADMIN_PASSWORD);
admin = await prisma.user.create({
data: {
email: ADMIN_EMAIL,
username: ADMIN_USERNAME,
password: hashedPassword,
role: 'ADMIN',
isActive: true,
canDownload: true
}
});
output.push(`✅ 创建管理员用户: ${ADMIN_USERNAME}`);
} catch (error: any) {
if (error.code === 'P2002') {
admin = await prisma.user.findUnique({
where: { email: ADMIN_EMAIL }
});
output.push(`ℹ️ 管理员用户已存在`);
} else {
throw error;
}
}
if (!admin) {
throw new Error('无法创建或找到管理员用户');
}
// 创建默认分类
output.push('\n📝 步骤2: 创建默认分类和场景');
try {
await prisma.category.create({ data: { name: DEFAULT_CATEGORY } });
output.push(`✅ 创建默认分类: ${DEFAULT_CATEGORY}`);
} catch (error: any) {
if (error.code === 'P2002') {
output.push(`ℹ️ 默认分类已存在`);
}
}
// 创建默认场景
try {
await prisma.scene.create({ data: { name: DEFAULT_SCENE } });
output.push(`✅ 创建默认场景: ${DEFAULT_SCENE}`);
} catch (error: any) {
if (error.code === 'P2002') {
output.push(`ℹ️ 默认场景已存在`);
}
}
// 获取S3预览图
output.push('\n📝 步骤3: 获取S3预览图列表');
const previewFiles = await getS3PreviewFiles();
output.push(`✅ 从S3找到 ${previewFiles.length} 个预览图`);
// 获取本地素材文件
output.push('\n📝 步骤4: 扫描本地素材文件');
const materialFiles = getLocalMaterialFiles(UPLOADS_DIR);
output.push(`✅ 从本地找到 ${materialFiles.length} 个素材文件`);
// 匹配文件
output.push('\n📝 步骤5: 匹配预览图和素材文件');
const matches = matchFiles(previewFiles, materialFiles);
const withPreview = matches.filter(m => m.previewFile).length;
const withoutPreview = matches.length - withPreview;
output.push(`✅ 总计 ${matches.length} 个素材 (${withPreview} 个有预览图, ${withoutPreview} 个无预览图)`);
// 恢复素材数据
output.push('\n📝 步骤6: 恢复素材数据到数据库');
const HOST_UPLOADS_DIR = '/mnt/PoolOne/apps/youyidian-3d/uploads';
let successCount = 0;
let failCount = 0;
for (const match of matches) {
try {
const originalName = extractOriginalName(match.materialFile);
const containerPath = path.join(UPLOADS_DIR, match.materialFile);
const hostPath = path.join(HOST_UPLOADS_DIR, match.materialFile);
const stats = fs.statSync(containerPath);
await prisma.material.create({
data: {
previewUrl: match.previewFile ? `${S3_PUBLIC_URL}${match.previewFile}` : '',
productModel: originalName.substring(0, 50),
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: admin.id
}
});
successCount++;
if (successCount % 10 === 0) {
output.push(`✅ 已恢复: ${successCount}/${matches.length}`);
}
} catch (error: any) {
failCount++;
output.push(`❌ 恢复失败: ${match.materialFile} - ${error.message}`);
}
}
output.push(`\n📊 恢复完成统计:`);
output.push(` ✅ 成功: ${successCount} 个`);
output.push(` ❌ 失败: ${failCount} 个`);
output.push(` 📁 总计: ${matches.length} 个文件`);
output.push(`\n🎉 数据恢复完成!`);
output.push(`\n📌 默认信息:`);
output.push(` 管理员账号: ${ADMIN_USERNAME}`);
output.push(` 管理员密码: ${ADMIN_PASSWORD}`);
output.push(` 默认分类: ${DEFAULT_CATEGORY}`);
output.push(` 默认场景: ${DEFAULT_SCENE}`);
output.push(`\n⚠️ 提示: 请登录后台修改素材的SPU、分类、场景等信息`);
return NextResponse.json({
success: true,
message: output.join('\n'),
stats: { successCount, failCount, total: matches.length }
});
} catch (error: any) {
return NextResponse.json({
success: false,
message: error.message
}, { status: 500 });
}
}