// prisma/mysql/seed.ts import { PrismaClient, MenuType } from '@prisma/mysql/client'; import type { Prisma } from '@prisma/mysql/client'; const prisma = new PrismaClient(); // ============================================================================= // MENU SEEDS DATA // ============================================================================= interface SeedMenu { legacyId: number; legacyParentId: number | null; title: string; type: MenuType; name: string; path: string; icon: string | null; component_key?: string | null; order: number; meta?: Prisma.JsonValue; } const MENU_SEEDS: SeedMenu[] = [ // ====================== // 营销中心 (Marketing) // ====================== { legacyId: 1, legacyParentId: null, title: '营销中心', type: 'DIRECTORY', name: 'Marketing Management', path: '/marketing', icon: 'i-carbon:application-web', order: 100, meta: { title: '营销中心', i18n: 'route.general.root', icon: 'i-carbon:application-web', }, }, { legacyId: 2, legacyParentId: 1, title: '视频管理', type: 'MENU', name: 'videoList', path: '/marketing/video', icon: 'i-carbon:video', order: 1, meta: { title: '视频管理', i18n: 'route.general.video.root', icon: 'i-carbon:video', }, }, { legacyId: 3, legacyParentId: 2, title: '视频列表', type: 'SUBMENU', name: 'videoList', path: '/marketing/videoList', icon: null, component_key: '@/views/marketing_center/video_mgnt/list.vue', order: 1, meta: { title: '视频列表', i18n: 'route.general.video.list', sidebar: false, breadcrumb: false, cache: ['videoCreate', 'videoEdit'], }, }, { legacyId: 4, legacyParentId: 2, title: '新增视频', type: 'SUBMENU', name: 'videoCreate', path: '/marketing/video/detail', icon: null, component_key: '@/views/marketing_center/video_mgnt/detail.vue', order: 2, meta: { title: '新增视频', i18n: 'route.general.video.create', sidebar: false, activeMenu: '/marketing/videoList', cache: true, noCache: 'videoList', }, }, { legacyId: 5, legacyParentId: 2, title: '编辑视频', type: 'SUBMENU', name: 'videoEdit', path: '/marketing/video/detail/:id', icon: null, component_key: '@/views/marketing_center/video_mgnt/detail.vue', order: 3, meta: { title: '编辑视频', i18n: 'route.general.video.edit', sidebar: false, activeMenu: '/marketing/videoList', cache: true, noCache: 'videoList', }, }, { legacyId: 6, legacyParentId: 1, title: '分类管理', type: 'MENU', name: 'categoryList', path: '/marketing/category', icon: 'i-carbon:category', order: 2, meta: { title: '分类管理', i18n: 'route.general.category.root', icon: 'i-carbon:category', }, }, { legacyId: 7, legacyParentId: 6, title: '分类列表', type: 'SUBMENU', name: 'categoryList', path: '/marketing/categoryList', icon: null, component_key: '@/views/marketing_center/category_mgnt/list.vue', order: 1, meta: { title: '分类列表', i18n: 'route.general.category.list', sidebar: false, breadcrumb: false, cache: ['categoryCreate', 'categoryEdit'], }, }, { legacyId: 8, legacyParentId: 6, title: '新增分类', type: 'SUBMENU', name: 'categoryCreate', path: '/marketing/category/detail', icon: null, component_key: '@/views/marketing_center/category_mgnt/detail.vue', order: 2, meta: { title: '新增分类', i18n: 'route.general.category.create', sidebar: false, activeMenu: '/marketing/categoryList', cache: true, noCache: 'categoryList', }, }, { legacyId: 9, legacyParentId: 6, title: '编辑分类', type: 'SUBMENU', name: 'categoryEdit', path: '/marketing/category/detail/:id', icon: null, component_key: '@/views/marketing_center/category_mgnt/detail.vue', order: 3, meta: { title: '编辑分类', i18n: 'route.general.category.edit', sidebar: false, activeMenu: '/marketing/categoryList', cache: true, noCache: 'categoryList', }, }, { legacyId: 10, legacyParentId: 1, title: '标签管理', type: 'MENU', name: 'tagList', path: '/marketing/tag', icon: 'i-carbon:tag', order: 3, meta: { title: '标签管理', i18n: 'route.general.tag.root', icon: 'i-carbon:tag', }, }, { legacyId: 11, legacyParentId: 10, title: '标签列表', type: 'SUBMENU', name: 'tagList', path: '/marketing/tagList', icon: null, component_key: '@/views/marketing_center/tag_mgnt/list.vue', order: 1, meta: { title: '标签列表', i18n: 'route.general.tag.list', sidebar: false, breadcrumb: false, cache: ['tagCreate', 'tagEdit'], }, }, { legacyId: 12, legacyParentId: 10, title: '新增标签', type: 'SUBMENU', name: 'tagCreate', path: '/marketing/tag/detail', icon: null, component_key: '@/views/marketing_center/tag_mgnt/detail.vue', order: 2, meta: { title: '新增标签', i18n: 'route.general.tag.create', sidebar: false, activeMenu: '/marketing/tagList', cache: true, noCache: 'tagList', }, }, { legacyId: 13, legacyParentId: 10, title: '编辑标签', type: 'SUBMENU', name: 'tagEdit', path: '/marketing/tag/detail/:id', icon: null, component_key: '@/views/marketing_center/tag_mgnt/detail.vue', order: 3, meta: { title: '编辑标签', i18n: 'route.general.tag.edit', sidebar: false, activeMenu: '/marketing/tagList', cache: true, noCache: 'tagList', }, }, { legacyId: 14, legacyParentId: 1, title: '广告管理', type: 'MENU', name: 'adsList', path: '/marketing/ads', icon: 'i-carbon:image-copy', order: 4, meta: { title: '广告管理', i18n: 'route.general.ads.root', icon: 'i-carbon:image-copy', }, }, { legacyId: 15, legacyParentId: 14, title: '广告列表', type: 'SUBMENU', name: 'adsList', path: '/marketing/adsList', icon: null, component_key: '@/views/marketing_center/ads_mgnt/list.vue', order: 1, meta: { title: '广告列表', i18n: 'route.general.ads.list', sidebar: false, breadcrumb: false, cache: ['adsCreate', 'adsEdit'], }, }, { legacyId: 16, legacyParentId: 14, title: '新增广告', type: 'SUBMENU', name: 'adsCreate', path: '/marketing/ads/detail', icon: null, component_key: '@/views/marketing_center/ads_mgnt/detail.vue', order: 2, meta: { title: '新增广告', i18n: 'route.general.ads.create', sidebar: false, activeMenu: '/marketing/adsList', cache: true, noCache: 'adsList', }, }, { legacyId: 17, legacyParentId: 14, title: '编辑广告', type: 'SUBMENU', name: 'adsEdit', path: '/marketing/ads/detail/:id', icon: null, component_key: '@/views/marketing_center/ads_mgnt/detail.vue', order: 3, meta: { title: '编辑广告', i18n: 'route.general.ads.edit', sidebar: false, activeMenu: '/marketing/adsList', cache: true, noCache: 'adsList', }, }, { legacyId: 18, legacyParentId: 1, title: '参数管理', type: 'MENU', name: 'paramList', path: '/marketing/param', icon: 'i-carbon:settings', order: 5, meta: { title: '参数管理', i18n: 'route.general.param.root', icon: 'i-carbon:settings', }, }, { legacyId: 19, legacyParentId: 18, title: '参数列表', type: 'SUBMENU', name: 'paramList', path: '/marketing/paramList', icon: null, component_key: '@/views/marketing_center/param_mgnt/list.vue', order: 1, meta: { title: '参数列表', i18n: 'route.general.param.list', sidebar: false, breadcrumb: false, cache: ['paramCreate', 'paramEdit'], }, }, { legacyId: 20, legacyParentId: 18, title: '新增参数', type: 'SUBMENU', name: 'paramCreate', path: '/marketing/param/detail', icon: null, component_key: '@/views/marketing_center/param_mgnt/detail.vue', order: 2, meta: { title: '新增参数', i18n: 'route.general.param.create', sidebar: false, activeMenu: '/marketing/paramList', cache: true, noCache: 'paramList', }, }, { legacyId: 21, legacyParentId: 18, title: '编辑参数', type: 'SUBMENU', name: 'paramEdit', path: '/marketing/param/detail/:id', icon: null, component_key: '@/views/marketing_center/param_mgnt/detail.vue', order: 3, meta: { title: '编辑参数', i18n: 'route.general.param.edit', sidebar: false, activeMenu: '/marketing/paramList', cache: true, noCache: 'paramList', }, }, { legacyId: 22, legacyParentId: 1, title: '渠道管理', type: 'MENU', name: 'channelList', path: '/marketing/channel', icon: 'i-carbon:network-3', order: 6, meta: { title: '渠道管理', i18n: 'route.general.channel.root', icon: 'i-carbon:network-3', }, }, { legacyId: 23, legacyParentId: 22, title: '渠道列表', type: 'SUBMENU', name: 'channelList', path: '/marketing/channelList', icon: null, component_key: '@/views/marketing_center/channel_mgnt/list.vue', order: 1, meta: { title: '渠道列表', i18n: 'route.general.channel.list', sidebar: false, breadcrumb: false, cache: ['channelCreate', 'channelEdit'], }, }, { legacyId: 24, legacyParentId: 22, title: '新增渠道', type: 'SUBMENU', name: 'channelCreate', path: '/marketing/channel/detail', icon: null, component_key: '@/views/marketing_center/channel_mgnt/detail.vue', order: 2, meta: { title: '新增渠道', i18n: 'route.general.channel.create', sidebar: false, activeMenu: '/marketing/channelList', cache: true, noCache: 'channelList', }, }, { legacyId: 25, legacyParentId: 22, title: '编辑渠道', type: 'SUBMENU', name: 'channelEdit', path: '/marketing/channel/detail/:id', icon: null, component_key: '@/views/marketing_center/channel_mgnt/detail.vue', order: 3, meta: { title: '编辑渠道', i18n: 'route.general.channel.edit', sidebar: false, activeMenu: '/marketing/channelList', cache: true, noCache: 'channelList', }, }, // ====================== // 数据中心 (Data Center) // ====================== { legacyId: 26, legacyParentId: null, title: '数据中心', type: 'DIRECTORY', name: 'Data Center', path: '/datacenter', icon: 'i-carbon:data-vis-4', order: 200, meta: { title: '数据中心', i18n: 'route.general.root', icon: 'i-carbon:data-vis-4', }, }, { legacyId: 27, legacyParentId: 26, title: 'APP访问记录', type: 'MENU', name: 'appAccessList', path: '/datacenter/appAccessRecord', icon: 'i-carbon:mobile', order: 1, meta: { title: 'APP访问记录', i18n: 'route.general.appAccessRecord.root', icon: 'i-carbon:mobile', }, }, { legacyId: 28, legacyParentId: 27, title: 'APP访问记录', type: 'SUBMENU', name: 'appAccessList', path: '/datacenter/appAccessList', icon: null, component_key: '@/views/data_center/appAccess_records/list.vue', order: 1, meta: { title: 'APP访问记录', i18n: 'route.general.appAccessRecord.list', sidebar: false, breadcrumb: false, cache: ['appAccessRecordCreate', 'appAccessRecordEdit'], }, }, { legacyId: 29, legacyParentId: 26, title: '广告点击记录', type: 'MENU', name: 'adsAccessList', path: '/datacenter/adsAccessRecord', icon: 'i-carbon:touch-1', order: 2, meta: { title: '广告点击记录', i18n: 'route.general.adsAccessRecord.root', icon: 'i-carbon:touch-1', }, }, { legacyId: 30, legacyParentId: 29, title: '广告点击记录', type: 'SUBMENU', name: 'adsAccessList', path: '/datacenter/adsAccessList', icon: null, component_key: '@/views/data_center/adsAccess_records/list.vue', order: 1, meta: { title: '广告点击记录', i18n: 'route.general.category.list', sidebar: false, breadcrumb: false, cache: ['adsAccessRecordCreate', 'adsAccessRecordEdit'], }, }, // ====================== // 统计中心 (Stats Center) // ====================== { legacyId: 31, legacyParentId: null, title: '统计中心', type: 'DIRECTORY', name: 'Statistics Management', path: '/stats', icon: 'i-carbon:chart-line', order: 300, meta: { title: '统计中心', i18n: 'route.general.root', icon: 'i-carbon:chart-line', }, }, { legacyId: 32, legacyParentId: 31, title: '每日统计', type: 'MENU', name: 'dailyStats', path: '/stats/dailyStats', icon: 'i-carbon:calendar', order: 1, meta: { title: '每日统计', i18n: 'route.general.dailyStats.root', icon: 'i-carbon:calendar', }, }, { legacyId: 33, legacyParentId: 32, title: '每日统计', type: 'SUBMENU', name: 'dailyStatsList', path: '/stats/dailyStatsList', icon: null, component_key: '@/views/stats_center/daily_stats/list.vue', order: 1, meta: { title: '每日统计', i18n: 'route.general.video.list', sidebar: false, breadcrumb: false, cache: ['videoCreate', 'videoEdit'], }, }, { legacyId: 34, legacyParentId: 31, title: '广告统计', type: 'MENU', name: 'adsStatsList', path: '/stats/adsStats', icon: 'i-carbon:chart-bar', order: 2, meta: { title: '广告统计', i18n: 'route.general.adsStats.root', icon: 'i-carbon:chart-bar', }, }, { legacyId: 35, legacyParentId: 34, title: '广告统计', type: 'SUBMENU', name: 'adsStatsList', path: '/stats/adsStatsList', icon: null, component_key: '@/views/stats_center/ads_stats/list.vue', order: 1, meta: { title: '广告统计', i18n: 'route.general.adsStats.list', sidebar: false, breadcrumb: false, cache: ['adsStatsCreate', 'adsStatsEdit'], }, }, { legacyId: 36, legacyParentId: 31, title: '广告汇总', type: 'MENU', name: 'adsStatsSummary', path: '/stats/adsStatsSummary', icon: 'i-carbon:chart-pie', component_key: '@/views/stats_center/ads_stats_summary/list.vue', order: 3, meta: { title: '广告汇总', i18n: 'route.general.adsStatsSummary.root', icon: 'i-carbon:chart-pie', }, }, { legacyId: 37, legacyParentId: 36, title: '广告汇总', type: 'SUBMENU', name: 'adsStatsSummaryList', path: '/stats/adsStatsSummaryList', icon: null, component_key: '@/views/stats_center/ads_stats_summary/list.vue', order: 1, meta: { title: '广告汇总', i18n: 'route.general.adsStatsSummary.list', sidebar: false, breadcrumb: false, cache: ['adsStatsSummaryCreate', 'adsStatsSummaryEdit'], }, }, { legacyId: 38, legacyParentId: 31, title: '渠道统计', type: 'MENU', name: 'channelStatsList', path: '/stats/channelStats', icon: 'i-carbon:flow', order: 4, meta: { title: '渠道统计', i18n: 'route.general.channelStats.root', icon: 'i-carbon:flow', }, }, { legacyId: 39, legacyParentId: 38, title: '渠道统计', type: 'SUBMENU', name: 'channelStatsList', path: '/stats/channelStatsList', icon: null, component_key: '@/views/stats_center/channel_stats/list.vue', order: 1, meta: { title: '渠道统计', i18n: 'route.general.channel.list', sidebar: false, breadcrumb: false, cache: ['channelCreate', 'channelEdit'], }, }, { legacyId: 40, legacyParentId: 31, title: '渠道汇总', type: 'MENU', name: 'channelStatsSummaryList', path: '/stats/channelStatsSummary', icon: 'i-carbon:report', order: 5, meta: { title: '渠道汇总', i18n: 'route.general.channelStatsSummary.root', icon: 'i-carbon:report', }, }, { legacyId: 41, legacyParentId: 40, title: '渠道汇总', type: 'SUBMENU', name: 'channelStatsSummaryList', path: '/stats/channelStatsSummaryList', icon: null, component_key: '@/views/stats_center/channel_stats_summary/list.vue', order: 1, meta: { title: '参数列表', i18n: 'route.general.param.list', sidebar: false, breadcrumb: false, cache: ['channelStatsSummaryCreate', 'channelStatsSummaryEdit'], }, }, // ====================== // 系统管理 (System) // ====================== { legacyId: 42, legacyParentId: null, title: '账号管理', type: 'DIRECTORY', name: 'Sytem Management', path: '/system', icon: 'i-carbon:user-admin', order: 400, meta: { title: '账号管理', i18n: 'route.general.root', icon: 'i-carbon:user-admin', }, }, { legacyId: 43, legacyParentId: 42, title: '账号列表', type: 'MENU', name: 'userList', path: '/system/users', icon: 'i-carbon:user-multiple', component_key: '@/views/system_mgnt/users/list.vue', order: 1, meta: { title: '账号列表', i18n: 'route.general.manager.root', icon: 'i-carbon:user-multiple', }, }, { legacyId: 44, legacyParentId: 42, title: '角色列表', type: 'MENU', name: 'roleList', path: '/system/roles', icon: 'i-carbon:user-role', order: 2, meta: { title: '角色列表', i18n: 'route.general.role.root', icon: 'i-carbon:user-role', }, }, { legacyId: 45, legacyParentId: 44, title: '角色列表', type: 'SUBMENU', name: 'roleList', path: '/system/roleList', icon: null, component_key: '@/views/system_mgnt/roles/list.vue', order: 1, meta: { title: '角色列表', i18n: 'route.general.role.list', sidebar: false, breadcrumb: false, cache: ['roleCreate', 'roleEdit'], }, }, { legacyId: 46, legacyParentId: 44, title: '新增角色', type: 'SUBMENU', name: 'roleCreate', path: '/system/roles/detail', icon: null, component_key: '@/views/system_mgnt/roles/detail.vue', order: 2, meta: { title: '新增角色', i18n: 'route.general.role.create', sidebar: false, activeMenu: '/system/roleList', cache: true, noCache: 'roleList', }, }, { legacyId: 47, legacyParentId: 44, title: '编辑角色', type: 'SUBMENU', name: 'roleEdit', path: '/system/roles/detail/:id', icon: null, component_key: '@/views/system_mgnt/roles/detail.vue', order: 3, meta: { title: '编辑角色', i18n: 'route.general.role.edit', sidebar: false, activeMenu: '/system/roleList', cache: true, noCache: 'roleList', }, }, { legacyId: 48, legacyParentId: 42, title: '权限列表', type: 'MENU', name: 'menus', path: '/system/menus', icon: 'i-carbon:list-boxes', order: 3, meta: { title: '权限列表', i18n: 'route.general.menu.root', icon: 'i-carbon:list-boxes', }, }, { legacyId: 49, legacyParentId: 48, title: '菜单列表', type: 'SUBMENU', name: 'menuList', path: '/system/menuList', icon: null, component_key: '@/views/system_mgnt/menus/list.vue', order: 1, meta: { title: '菜单列表', i18n: 'route.general.menu.list', sidebar: false, breadcrumb: false, cache: ['menuCreate', 'menuEdit'], }, }, { legacyId: 50, legacyParentId: 48, title: '新增菜单', type: 'SUBMENU', name: 'menuCreate', path: '/system/menus/detail', icon: null, component_key: '@/views/system_mgnt/menus/detail.vue', order: 2, meta: { title: '新增菜单', i18n: 'route.general.menu.create', sidebar: false, activeMenu: '/system/menuList', cache: true, noCache: 'menuList', }, }, { legacyId: 51, legacyParentId: 48, title: '编辑菜单', type: 'SUBMENU', name: 'menuEdit', path: '/system/menus/detail/:id', icon: null, component_key: '@/views/system_mgnt/menus/detail.vue', order: 3, meta: { title: '编辑菜单', i18n: 'route.general.menu.edit', sidebar: false, activeMenu: '/system/menuList', cache: true, noCache: 'menuList', }, }, ]; // ============================================================================= // HELPER FUNCTIONS // ============================================================================= /** * Get default action permissions based on menu type */ function getDefaultPermissions(type: MenuType) { switch (type) { case 'DIRECTORY': return { canView: 1, canCreate: 0, canUpdate: 0, canDelete: 0 }; case 'MENU': case 'SUBMENU': return { canView: 1, canCreate: 1, canUpdate: 1, canDelete: 1 }; case 'BUTTON': return { canView: 0, canCreate: 1, canUpdate: 0, canDelete: 0 }; default: return { canView: 0, canCreate: 0, canUpdate: 0, canDelete: 0 }; } } // ============================================================================= // SEED FUNCTIONS // ============================================================================= /** * Seed users and roles */ async function seedUsers() { console.log('📝 Seeding users and roles...'); // Upsert role by unique name const role = await prisma.role.upsert({ where: { name: '管理员' }, update: { remark: '管理员专用' }, create: { name: '管理员', remark: '管理员专用', }, }); // Upsert admin user by unique username const adminUser = await prisma.user.upsert({ where: { username: 'admin' }, // Do not overwrite password on existing user update: { remark: '默认拥有所有菜单权限,不需要配置角色' }, create: { username: 'admin', password: '$2b$12$iS0UJ1YqSal0N3uwin/OvOABUINAclcZGjHNyGFC7mlwRYTFjGQ26', remark: '默认拥有所有菜单权限,不需要配置角色', }, }); // Link user and role if not already linked const existingLink = await prisma.userRole.findFirst({ where: { userId: adminUser.id, roleId: role.id }, }); if (!existingLink) { await prisma.userRole.create({ data: { userId: adminUser.id, roleId: role.id, }, }); } console.log('✅ Users and roles seeded successfully'); console.log(` - Admin user: ${adminUser.username}`); console.log(` - Role: ${role.name}`); } /** * Seed menus with multi-pass insertion to handle parent-child relationships */ async function seedMenus() { console.log('📝 Seeding menus...'); const legacyIdToNewId = new Map(); const inserted = new Set(); // Multi-pass insertion: insert records whose parent is either null or already inserted while (inserted.size < MENU_SEEDS.length) { let progress = false; for (const seed of MENU_SEEDS) { if (inserted.has(seed.legacyId)) { continue; } // If has a parent, but parent not inserted yet → skip this round if ( seed.legacyParentId !== null && !legacyIdToNewId.has(seed.legacyParentId) ) { continue; } const parentId = seed.legacyParentId === null ? null : legacyIdToNewId.get(seed.legacyParentId)!; const permissions = getDefaultPermissions(seed.type); const created = await prisma.menu.upsert({ // Use unique index on frontendAuth to identify menu where: { frontendAuth: seed.path }, update: { parentId, title: seed.title, status: true, type: seed.type, order: seed.order, // keep frontendAuth as path path: seed.path, name: seed.name, icon: seed.icon, redirect: null, component_key: seed.component_key ?? null, // meta undefined means do not overwrite; if provided, update ...(seed.meta !== undefined ? { meta: seed.meta as Prisma.InputJsonValue } : {}), ...permissions, }, create: { parentId, title: seed.title, status: true, type: seed.type, order: seed.order, // Use path as frontendAuth for RBAC tracking frontendAuth: seed.path, path: seed.path, name: seed.name, icon: seed.icon, redirect: null, component_key: seed.component_key ?? null, meta: seed.meta ?? undefined, // Set default permissions based on type ...permissions, }, }); legacyIdToNewId.set(seed.legacyId, created.id); inserted.add(seed.legacyId); progress = true; console.log( ` ✓ Menu [${seed.type.padEnd(9)}] ${seed.name.padEnd(25)} → ID: ${created.id}`, ); } if (!progress) { // Nothing could be inserted in this pass → some parentId is invalid or cyclic const pending = MENU_SEEDS.filter((s) => !inserted.has(s.legacyId)).map( (s) => ({ legacyId: s.legacyId, legacyParentId: s.legacyParentId, name: s.name, }), ); console.error('❌ Could not resolve parents for menus:', pending); throw new Error( 'Menu seeding aborted: unresolved parent relationships. Check legacyParentId values.', ); } } console.log('✅ Menus seeded successfully'); console.log(` - Total menus: ${inserted.size}`); } // ============================================================================= // MAIN EXECUTION // ============================================================================= async function main() { console.log('🌱 Starting database seeding...\n'); try { // Seed in order: users first, then menus await seedUsers(); console.log(''); await seedMenus(); console.log('\n🎉 Database seeding completed successfully!'); } catch (error) { console.error('\n❌ Error during seeding:', error); throw error; } } main() .then(async () => { await prisma.$disconnect(); }) .catch(async (e) => { console.error(e); await prisma.$disconnect(); process.exit(1); });