/** * 数据库种子数据脚本 * 用于填充测试数据和演示数据 */ import { PrismaClient, FeedbackStatus, FeedbackPriority, ActivityType } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; import { Pool } from 'pg'; import { hash } from 'bcryptjs'; import { STANDARD_FINISH_CATALOG } from '../src/lib/standard-finish-catalog'; import { buildStandardFinishMaterialData } from '../src/lib/standard-finish-sync'; import 'dotenv/config'; // Prisma 7 需要使用适配器 const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); const adapter = new PrismaPg(pool); const prisma = new PrismaClient({ adapter }); async function main() { console.log('🌱 开始填充数据库...'); // 清理现有数据 console.log('🧹 清理现有数据...'); await prisma.activityLog.deleteMany(); await prisma.feedback.deleteMany(); await prisma.shareLink.deleteMany(); await prisma.productHardware.deleteMany(); await prisma.panelMaterial.deleteMany(); await prisma.panel.deleteMany(); await prisma.assemblyStep.deleteMany(); await prisma.productSKU.deleteMany(); await prisma.product.deleteMany(); await prisma.productCategory.deleteMany(); await prisma.hardware.deleteMany(); await prisma.hardwareCategory.deleteMany(); await prisma.material.deleteMany(); await prisma.materialCategory.deleteMany(); await prisma.user.deleteMany(); await prisma.team.deleteMany(); await prisma.department.deleteMany(); // ============================================ // 创建饰面/花色标准库 // ============================================ console.log('🎨 创建饰面/花色标准分类...'); const materialCategories = await Promise.all( STANDARD_FINISH_CATALOG.map((category) => prisma.materialCategory.create({ data: { name: category.name, icon: category.icon, sortOrder: category.sortOrder, isActive: true, }, }), ), ); const categoryIdByKey = new Map( STANDARD_FINISH_CATALOG.map((category, index) => [category.key, materialCategories[index].id]), ); console.log('🎨 创建饰面/花色标准预设...'); for (const category of STANDARD_FINISH_CATALOG) { const categoryId = categoryIdByKey.get(category.key); if (!categoryId) { throw new Error(`缺少饰面分类 ID: ${category.key}`); } for (const material of category.materials) { await prisma.material.create({ data: buildStandardFinishMaterialData({ material, category, categoryId, }), }); } } // ============================================ // 创建五金件分类和预置五金件 // ============================================ console.log('🔩 创建五金件分类...'); const hardwareCategories = await Promise.all([ prisma.hardwareCategory.create({ data: { name: '合页', icon: '📐', sortOrder: 1, description: '各类门板合页' }, }), prisma.hardwareCategory.create({ data: { name: '螺丝', icon: '🔩', sortOrder: 2, description: '固定用螺丝' }, }), prisma.hardwareCategory.create({ data: { name: '导轨', icon: '🛤️', sortOrder: 3, description: '抽屉滑轨' }, }), prisma.hardwareCategory.create({ data: { name: '拉手', icon: '🚪', sortOrder: 4, description: '柜门拉手' }, }), prisma.hardwareCategory.create({ data: { name: '连接件', icon: '🔗', sortOrder: 5, description: '板材连接件' }, }), prisma.hardwareCategory.create({ data: { name: '支撑件', icon: '📏', sortOrder: 6, description: '层板支撑' }, }), ]); // 创建分类映射 const hwCatMap: Record = {}; hardwareCategories.forEach((cat) => { hwCatMap[cat.name] = cat.id; }); console.log('🔧 创建预置五金件...'); // 合页类 const hinges = [ { name: '直角合页 50mm', code: 'HW-HINGE-001', specs: { size: '50x50mm', material: '不锈钢', angle: 90 } }, { name: '直角合页 75mm', code: 'HW-HINGE-002', specs: { size: '75x75mm', material: '不锈钢', angle: 90 } }, { name: '阻尼合页', code: 'HW-HINGE-003', specs: { size: '40mm', material: '锌合金', damping: true } }, { name: '隐藏式合页', code: 'HW-HINGE-004', specs: { size: '35mm', material: '不锈钢', hidden: true } }, ]; for (const hw of hinges) { await prisma.hardware.create({ data: { name: hw.name, code: hw.code, categoryId: hwCatMap['合页'], specifications: hw.specs, modelUrl: `/hardware/models/hinges/${hw.code}.glb`, thumbnailUrl: `/hardware/thumbnails/hinges/${hw.code}.jpg`, dimensions: { width: 50, height: 50, depth: 5, unit: 'mm' }, isPreset: true, isActive: true, }, }); } // 螺丝类 const screws = [ { name: '十字沉头螺丝 M4x20', code: 'HW-SCREW-001', specs: { type: 'M4', length: 20, head: '沉头' } }, { name: '十字沉头螺丝 M4x30', code: 'HW-SCREW-002', specs: { type: 'M4', length: 30, head: '沉头' } }, { name: '内六角螺丝 M6x25', code: 'HW-SCREW-003', specs: { type: 'M6', length: 25, head: '内六角' } }, { name: '自攻螺丝 4x30', code: 'HW-SCREW-004', specs: { type: '4mm', length: 30, selfTapping: true } }, ]; for (const hw of screws) { await prisma.hardware.create({ data: { name: hw.name, code: hw.code, categoryId: hwCatMap['螺丝'], specifications: hw.specs, modelUrl: `/hardware/models/screws/${hw.code}.glb`, thumbnailUrl: `/hardware/thumbnails/screws/${hw.code}.jpg`, dimensions: { width: 8, height: hw.specs.length, depth: 8, unit: 'mm' }, isPreset: true, isActive: true, }, }); } // 导轨类 const rails = [ { name: '三节滑轨 300mm', code: 'HW-RAIL-001', specs: { length: 300, sections: 3, loadCapacity: '25kg' } }, { name: '三节滑轨 400mm', code: 'HW-RAIL-002', specs: { length: 400, sections: 3, loadCapacity: '30kg' } }, { name: '三节滑轨 500mm', code: 'HW-RAIL-003', specs: { length: 500, sections: 3, loadCapacity: '35kg' } }, { name: '阻尼缓冲滑轨 400mm', code: 'HW-RAIL-004', specs: { length: 400, sections: 3, damping: true } }, ]; for (const hw of rails) { await prisma.hardware.create({ data: { name: hw.name, code: hw.code, categoryId: hwCatMap['导轨'], specifications: hw.specs, modelUrl: `/hardware/models/rails/${hw.code}.glb`, thumbnailUrl: `/hardware/thumbnails/rails/${hw.code}.jpg`, dimensions: { width: 45, height: hw.specs.length, depth: 12, unit: 'mm' }, isPreset: true, isActive: true, }, }); } // 拉手类 const handles = [ { name: '简约拉手 96mm', code: 'HW-HANDLE-001', specs: { holeSpacing: 96, style: '简约' } }, { name: '简约拉手 128mm', code: 'HW-HANDLE-002', specs: { holeSpacing: 128, style: '简约' } }, { name: '隐形拉手 64mm', code: 'HW-HANDLE-003', specs: { holeSpacing: 64, style: '隐形' } }, { name: '圆形拉手', code: 'HW-HANDLE-004', specs: { diameter: 35, style: '圆形' } }, ]; for (const hw of handles) { await prisma.hardware.create({ data: { name: hw.name, code: hw.code, categoryId: hwCatMap['拉手'], specifications: hw.specs, modelUrl: `/hardware/models/handles/${hw.code}.glb`, thumbnailUrl: `/hardware/thumbnails/handles/${hw.code}.jpg`, dimensions: { width: 120, height: 25, depth: 30, unit: 'mm' }, isPreset: true, isActive: true, }, }); } // 连接件类 const connectors = [ { name: '三合一连接件', code: 'HW-CONN-001', specs: { type: '三合一', material: '锌合金' } }, { name: '偏心轮连接件', code: 'HW-CONN-002', specs: { type: '偏心轮', diameter: 15 } }, { name: '角码连接件', code: 'HW-CONN-003', specs: { type: '角码', angle: 90 } }, { name: '隐形连接件', code: 'HW-CONN-004', specs: { type: '隐形', hidden: true } }, ]; for (const hw of connectors) { await prisma.hardware.create({ data: { name: hw.name, code: hw.code, categoryId: hwCatMap['连接件'], specifications: hw.specs, modelUrl: `/hardware/models/connectors/${hw.code}.glb`, thumbnailUrl: `/hardware/thumbnails/connectors/${hw.code}.jpg`, dimensions: { width: 15, height: 15, depth: 15, unit: 'mm' }, isPreset: true, isActive: true, }, }); } // 支撑件类 const supports = [ { name: '层板托 5mm', code: 'HW-SUPP-001', specs: { pinDiameter: 5, material: '金属' } }, { name: '层板托 7mm', code: 'HW-SUPP-002', specs: { pinDiameter: 7, material: '金属' } }, { name: '隔板支撑', code: 'HW-SUPP-003', specs: { type: '隔板', adjustable: false } }, { name: '可调节层板托', code: 'HW-SUPP-004', specs: { type: '可调节', adjustable: true } }, ]; for (const hw of supports) { await prisma.hardware.create({ data: { name: hw.name, code: hw.code, categoryId: hwCatMap['支撑件'], specifications: hw.specs, modelUrl: `/hardware/models/supports/${hw.code}.glb`, thumbnailUrl: `/hardware/thumbnails/supports/${hw.code}.jpg`, dimensions: { width: 10, height: 20, depth: 10, unit: 'mm' }, isPreset: true, isActive: true, }, }); } // 创建产品分类 console.log('📁 创建产品分类...'); const categories = await Promise.all([ prisma.productCategory.create({ data: { name: '沙发', description: '各类沙发产品', sortOrder: 1 }, }), prisma.productCategory.create({ data: { name: '餐桌', description: '餐厅用桌', sortOrder: 2 }, }), prisma.productCategory.create({ data: { name: '书柜', description: '书房家具', sortOrder: 3 }, }), prisma.productCategory.create({ data: { name: '床', description: '卧室床类', sortOrder: 4 }, }), prisma.productCategory.create({ data: { name: '椅子', description: '各类座椅', sortOrder: 5 }, }), prisma.productCategory.create({ data: { name: '茶几', description: '客厅茶几', sortOrder: 6 }, }), prisma.productCategory.create({ data: { name: '电视柜', description: '客厅电视柜', sortOrder: 7 }, }), prisma.productCategory.create({ data: { name: '衣柜', description: '卧室衣柜', sortOrder: 8 }, }), ]); // 分类映射(方便后面使用) const categoryMap: Record = {}; categories.forEach(cat => { categoryMap[cat.name] = cat.id; }); // 创建部门结构 console.log('🏢 创建部门结构...'); const designDept = await prisma.department.create({ data: { name: '设计部', fullPath: '设计部', orderIndex: 1, }, }); const design3DDept = await prisma.department.create({ data: { name: '3D设计组', fullPath: '设计部/3D设计组', parentId: designDept.id, orderIndex: 1, }, }); const _productDept = await prisma.department.create({ data: { name: '产品部', fullPath: '产品部', orderIndex: 2, }, }); const _marketingDept = await prisma.department.create({ data: { name: '市场部', fullPath: '市场部', orderIndex: 3, }, }); // 创建小组 console.log('👥 创建小组...'); const modelingTeam = await prisma.team.create({ data: { name: '建模小组', description: '负责3D模型制作', departmentId: design3DDept.id, orderIndex: 1, }, }); const animationTeam = await prisma.team.create({ data: { name: '动画小组', description: '负责装配动画制作', departmentId: design3DDept.id, orderIndex: 2, }, }); // 创建用户 console.log('👤 创建用户...'); const hashedPassword = await hash('Admin123456!', 10); const _superAdmin = await prisma.user.create({ data: { email: 'admin@furniture3d.com', name: '系统管理员', password: hashedPassword, role: 'SUPER_ADMIN', isFirstLogin: false, status: 'ACTIVE', }, }); const deptManager = await prisma.user.create({ data: { email: 'design.manager@furniture3d.com', name: '张设计', password: hashedPassword, role: 'SUPERVISOR', departmentId: designDept.id, isFirstLogin: false, status: 'ACTIVE', }, }); // 更新部门负责人 await prisma.department.update({ where: { id: designDept.id }, data: { managerId: deptManager.id }, }); const teamLeader = await prisma.user.create({ data: { email: 'modeling.leader@furniture3d.com', name: '李组长', password: hashedPassword, role: 'SUPERVISOR', departmentId: design3DDept.id, teamId: modelingTeam.id, isFirstLogin: false, status: 'ACTIVE', }, }); // 更新小组组长 await prisma.team.update({ where: { id: modelingTeam.id }, data: { leaderId: teamLeader.id }, }); // 创建设计师 const designers = []; for (let i = 1; i <= 5; i++) { const designer = await prisma.user.create({ data: { email: `designer${i}@furniture3d.com`, name: `设计师${i}号`, password: hashedPassword, role: 'DESIGNER', departmentId: design3DDept.id, teamId: i <= 3 ? modelingTeam.id : animationTeam.id, isFirstLogin: false, status: 'ACTIVE', }, }); designers.push(designer); } // 创建产品 console.log('🛋️ 创建产品数据...'); const products = []; // ============================================ // 本地模型路径说明: // 所有模型存放在 public/uploads/models/ 目录下 // 前端访问路径为 /uploads/models/xxx.glb // ============================================ // 产品1:现代简约沙发 const sofa = await prisma.product.create({ data: { modelNumber: 'SF-001', name: '现代简约沙发', description: '北欧风格,简约设计,舒适坐感', categoryId: categoryMap['沙发'], modelUrl: '/uploads/models/demo/SheenChair.glb', // 本地模型:椅子(4.2MB) thumbnailUrl: '/uploads/models/demo/thumbnails/sofa.jpg', createdBy: designers[0].id, departmentId: design3DDept.id, visibility: 'PUBLIC', collaboratorUsers: { create: [ { userId: designers[1].id, role: 'editor', }, ], }, }, }); products.push(sofa); // 产品2:实木餐桌 const table = await prisma.product.create({ data: { modelNumber: 'TB-001', name: '实木餐桌', description: '橡木材质,可容纳6人', categoryId: categoryMap['餐桌'], modelUrl: '/uploads/models/demo/Horse.glb', // 本地模型:马(177KB) createdBy: designers[1].id, departmentId: design3DDept.id, visibility: 'DEPARTMENT', }, }); products.push(table); // 产品3:书柜 const bookshelf = await prisma.product.create({ data: { modelNumber: 'BS-001', name: '多层书柜', description: '5层设计,大容量储物', categoryId: categoryMap['书柜'], modelUrl: '/uploads/models/demo/Flamingo.glb', // 本地模型:火烈鸟(76KB) createdBy: designers[2].id, departmentId: design3DDept.id, visibility: 'TEAM', }, }); products.push(bookshelf); // 本地演示模型列表(循环使用) const demoModels = [ '/uploads/models/demo/SheenChair.glb', '/uploads/models/demo/Horse.glb', '/uploads/models/demo/Flamingo.glb', '/uploads/models/demo/Soldier.glb', '/uploads/models/demo/Parrot.glb', ]; // 创建更多产品(用于性能测试) const categoryNames = ['沙发', '餐桌', '书柜', '床', '衣柜']; for (let i = 4; i <= 20; i++) { const product = await prisma.product.create({ data: { modelNumber: `PRD-${String(i).padStart(3, '0')}`, name: `测试产品${i}`, description: `这是第${i}个测试产品`, categoryId: categoryMap[categoryNames[i % categoryNames.length]], modelUrl: demoModels[i % demoModels.length], // 循环使用本地模型 createdBy: designers[i % designers.length].id, departmentId: design3DDept.id, visibility: ['PUBLIC', 'DEPARTMENT', 'TEAM'][i % 3], }, }); products.push(product); } // 创建 SKU console.log('📦 创建 SKU 数据...'); const skus = []; // 沙发 SKU const sofaColors = ['米白色', '浅灰色', '深灰色']; const sofaSizes = ['双人座', '三人座']; for (const color of sofaColors) { for (const size of sofaSizes) { const sku = await prisma.productSKU.create({ data: { skuCode: `SF-001-${color.substring(0, 2)}-${size.substring(0, 2)}`, productId: sofa.id, ownerId: designers[0].id, attributes: { color, size, material: '布艺' }, price: size === '双人座' ? 2999 : 3999, costPrice: size === '双人座' ? 1500 : 2000, stock: Math.floor(Math.random() * 50) + 10, images: [ `https://example.com/sofa-${color}-${size}-1.jpg`, `https://example.com/sofa-${color}-${size}-2.jpg`, ], isActive: true, }, }); skus.push(sku); } } // 为其他产品创建 SKU for (let i = 1; i < Math.min(products.length, 10); i++) { const product = products[i]; for (let j = 1; j <= 3; j++) { const sku = await prisma.productSKU.create({ data: { skuCode: `${product.modelNumber}-V${j}`, productId: product.id, ownerId: designers[i % designers.length].id, attributes: { variant: `变体${j}` }, price: 1000 + Math.random() * 4000, costPrice: 500 + Math.random() * 2000, stock: Math.floor(Math.random() * 100), images: [`https://example.com/${product.modelNumber}-v${j}.jpg`], isActive: Math.random() > 0.2, }, }); skus.push(sku); } } // 创建装配步骤(使用演示模型的实际节点) console.log('🔧 创建装配步骤...'); await prisma.assemblyStep.createMany({ data: [ { productId: sofa.id, order: 1, title: '初始位置', description: '模型从初始位置开始', partIds: ['Scene'], transform: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }, duration: 1500, easing: 'power2.inOut', }, { productId: sofa.id, order: 2, title: '旋转展示', description: '360度旋转展示模型', partIds: ['Scene'], transform: { position: [0, 0, 0], rotation: [0, Math.PI * 2, 0], scale: [1, 1, 1] }, duration: 3000, easing: 'power2.inOut', }, { productId: sofa.id, order: 3, title: '放大查看', description: '放大查看细节', partIds: ['Scene'], transform: { position: [0, 0, 0], rotation: [0, Math.PI * 2, 0], scale: [1.5, 1.5, 1.5] }, duration: 2000, easing: 'power2.inOut', }, { productId: sofa.id, order: 4, title: '恢复原状', description: '恢复到初始状态', partIds: ['Scene'], transform: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }, duration: 2000, easing: 'power2.inOut', }, ], }); // 创建板件 console.log('📏 创建板件数据...'); await prisma.panel.createMany({ data: [ { productId: sofa.id, code: 'P001', name: '底座板', gltfNodeName: 'base', length: 200, width: 80, thickness: 5, }, { productId: sofa.id, code: 'P002', name: '靠背板', gltfNodeName: 'back', length: 200, width: 60, thickness: 3, }, { productId: sofa.id, code: 'P003', name: '左扶手', gltfNodeName: 'arm-left', length: 80, width: 20, thickness: 3, }, { productId: sofa.id, code: 'P004', name: '右扶手', gltfNodeName: 'arm-right', length: 80, width: 20, thickness: 3, }, ], }); // 创建分享链接 console.log('🔗 创建分享链接...'); await prisma.shareLink.createMany({ data: [ { productId: sofa.id, token: 'share-sofa-demo-001', expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天后过期 viewCount: Math.floor(Math.random() * 100), }, { productId: table.id, token: 'share-table-demo-001', expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7天后过期 viewCount: Math.floor(Math.random() * 50), }, ], }); // 创建反馈 console.log('💬 创建反馈数据...'); const feedbackCategories = ['模型错误', '尺寸错误', '材质错误', '装配步骤错误', '其他']; const feedbackStatuses: FeedbackStatus[] = ['PENDING', 'IN_PROGRESS', 'RESOLVED', 'CLOSED']; const feedbackPriorities: FeedbackPriority[] = ['LOW', 'MEDIUM', 'HIGH', 'URGENT']; for (let i = 0; i < 30; i++) { const feedbackData = { title: `反馈问题 ${i + 1}`, description: `这是第 ${i + 1} 个反馈问题的详细描述`, category: feedbackCategories[i % feedbackCategories.length], priority: feedbackPriorities[i % feedbackPriorities.length], status: feedbackStatuses[i % feedbackStatuses.length], productId: products[i % products.length].id, submitterId: designers[i % designers.length].id, assigneeId: i % 2 === 0 ? teamLeader.id : null, images: i % 3 === 0 ? [`https://example.com/feedback-${i}.jpg`] : [], resolution: i % 4 === 3 ? '问题已修复' : null, resolvedAt: i % 4 === 3 ? new Date() : null, ...(i % 4 === 0 ? { comments: [ { userId: teamLeader.id, content: 'I will handle this issue', createdAt: new Date().toISOString(), }, ], } : {}), }; // 只在需要时添加 comments 字段 if (i % 4 === 0) { feedbackData.comments = [ { userId: teamLeader.id, content: '我来处理这个问题', createdAt: new Date().toISOString(), }, ]; } await prisma.feedback.create({ data: feedbackData, }); } // 创建操作日志 console.log('📝 创建操作日志...'); const activityTypes: ActivityType[] = [ 'USER_LOGIN', 'PRODUCT_CREATE', 'PRODUCT_UPDATE', 'SKU_CREATE', 'SKU_UPDATE', 'FILE_UPLOAD', 'SHARE_LINK_CREATE', 'FEEDBACK_CREATE', ]; for (let i = 0; i < 100; i++) { const createdAt = new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000); // 过去30天内 await prisma.activityLog.create({ data: { userId: designers[i % designers.length].id, type: activityTypes[i % activityTypes.length], action: `执行了 ${activityTypes[i % activityTypes.length]} 操作`, targetType: i % 2 === 0 ? 'Product' : 'ProductSKU', targetId: i % 2 === 0 ? products[i % products.length].id : skus[i % skus.length]?.id, changes: { field: 'name', before: '旧值', after: '新值', }, metadata: { ip: `192.168.1.${Math.floor(Math.random() * 255)}`, userAgent: 'Mozilla/5.0', device: 'Desktop', }, createdAt, }, }); } console.log('✅ 数据填充完成!'); console.log('\n📊 数据统计:'); console.log(` - 材质分类:${await prisma.materialCategory.count()} 个`); console.log(` - 预置材质:${await prisma.material.count()} 个`); console.log(` - 五金件分类:${await prisma.hardwareCategory.count()} 个`); console.log(` - 预置五金件:${await prisma.hardware.count()} 个`); console.log(` - 部门:${await prisma.department.count()} 个`); console.log(` - 小组:${await prisma.team.count()} 个`); console.log(` - 用户:${await prisma.user.count()} 个`); console.log(` - 产品:${await prisma.product.count()} 个`); console.log(` - SKU:${await prisma.productSKU.count()} 个`); console.log(` - 装配步骤:${await prisma.assemblyStep.count()} 个`); console.log(` - 板件:${await prisma.panel.count()} 个`); console.log(` - 分享链接:${await prisma.shareLink.count()} 个`); console.log(` - 反馈:${await prisma.feedback.count()} 个`); console.log(` - 操作日志:${await prisma.activityLog.count()} 条`); console.log('\n🔑 测试账号:'); console.log(' - 超级管理员:admin@furniture3d.com / Admin123456!'); console.log(' - 部门管理员:design.manager@furniture3d.com / Admin123456!'); console.log(' - 组长:modeling.leader@furniture3d.com / Admin123456!'); console.log(' - 设计师:designer1@furniture3d.com / Admin123456!'); } main() .catch((e) => { console.error('❌ 数据填充失败:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); await pool.end(); });