// prisma/mongo/seed-admin.ts import { PrismaClient, MenuType, AdType } from '@prisma/mongo/client'; import type { Prisma } from '@prisma/mongo/client'; const prisma = new PrismaClient(); type ImgSource = 'LOCAL_ONLY' | 'S3_ONLY' | 'S3_AND_LOCAL' | 'PROVIDER'; async function seedSysConfig(): Promise { const nowSec = Math.floor(Date.now() / 1000); // Equivalent to: // db.sysConfig.updateOne({ _id: -1 }, { $set: {...} }, { upsert: true }) await prisma.$runCommandRaw({ update: 'sysConfig', updates: [ { q: { _id: -1 }, u: { $set: { appConfig: { videoCdn: { image: 'https://vm.rvakc.xyz/res/decode', video: 'https://vm.rvakc.xyz/api/web/media/m3u8/', }, adsCdn: { s3: 'https://s3.ap-east-1.amazonaws.com/mybucket-imgs', local: 'https://man.boxt3yk.com/images', }, }, imageConfig: { s3Enabled: false, storageStrategy: 'LOCAL_ONLY' as ImgSource, local: { rootPath: '/usr/local/app/box-project/box-images', baseUrl: 'https://man.boxt3yk.com/images', }, limitsMb: { image: 10, video: 100 }, s3: { accessKeyId: 'AKIA6GSNGR5PISMIKCJ4', secretAccessKey: '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', bucket: 'mybucket-imgs', region: 'ap-east-1', endpointUrl: 'https://s3.ap-east-1.amazonaws.com', imageBaseUrl: 'https://s3.ap-east-1.amazonaws.com/mybucket-imgs', }, }, provider: { providerCode: 'PARTNER', apiUrl: 'https://vm.rvakc.xyz/api/web/mediafile/search', itemsLimit: 300, }, updatedAt: nowSec, }, }, upsert: true, multi: false, }, ], }); } // ============================================================================= // MENU SEEDS DATA // ============================================================================= interface SeedMenu { legacyId: number; legacyParentId: number | null; title: string; type: MenuType; name: string; path: string; icon: string | null; componentKey?: 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: 'videoListMenu', 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, componentKey: '@/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, componentKey: '@/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, componentKey: '@/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: 'categoryListMenu', 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, componentKey: '@/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, componentKey: '@/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, componentKey: '@/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: 'tagListMenu', 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, componentKey: '@/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, componentKey: '@/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, componentKey: '@/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: 'adsListMenu', 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, componentKey: '@/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, componentKey: '@/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, componentKey: '@/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: 'paramListMenu', path: '/setting/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: '/setting/paramList', icon: null, componentKey: '@/views/setting_mgnt/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: '/setting/param/detail', icon: null, componentKey: '@/views/setting_mgnt/param_mgnt/detail.vue', order: 2, meta: { title: '新增参数', i18n: 'route.general.param.create', sidebar: false, activeMenu: '/setting/paramList', cache: true, noCache: 'paramList', }, }, { legacyId: 21, legacyParentId: 18, title: '编辑参数', type: 'SUBMENU', name: 'paramEdit', path: '/setting/param/detail/:id', icon: null, componentKey: '@/views/setting_mgnt/param_mgnt/detail.vue', order: 3, meta: { title: '编辑参数', i18n: 'route.general.param.edit', sidebar: false, activeMenu: '/setting/paramList', cache: true, noCache: 'paramList', }, }, { legacyId: 22, legacyParentId: 1, title: '渠道管理', type: 'MENU', name: 'channelListMenu', 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, componentKey: '@/views/setting_mgnt/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, componentKey: '@/views/setting_mgnt/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, componentKey: '@/views/setting_mgnt/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: 'appAccessListMenu', 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, componentKey: '@/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: 'adsAccessListMenu', 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, componentKey: '@/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, componentKey: '@/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: 'adsStatsListMenu', 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, componentKey: '@/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', componentKey: '@/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, componentKey: '@/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: 'channelStatsListMenu', 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, componentKey: '@/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: 'channelStatsSummaryListMenu', 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, componentKey: '@/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', componentKey: '@/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: 'roleListMenu', 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, componentKey: '@/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, componentKey: '@/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, componentKey: '@/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, componentKey: '@/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, componentKey: '@/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, componentKey: '@/views/system_mgnt/menus/detail.vue', order: 3, meta: { title: '编辑菜单', i18n: 'route.general.menu.edit', sidebar: false, activeMenu: '/system/menuList', cache: true, noCache: 'menuList', }, }, { legacyId: 52, legacyParentId: 42, title: 'Redis监控', type: 'MENU', name: 'redisMonitor', path: '/system/redisMonitor', icon: 'i-carbon:list-boxes', order: 4, meta: { title: 'Redis监控', i18n: 'route.general.redisMonitor.list', icon: 'i-carbon:list-boxes', }, }, { legacyId: 53, legacyParentId: 52, title: 'Redis列表', type: 'SUBMENU', name: 'redisList', path: '/system/redisList', icon: null, componentKey: '@/views/system_mgnt/redisMonitor/list.vue', order: 1, meta: { title: 'Redis列表', i18n: 'route.general.redisMonitor.list', sidebar: false, breadcrumb: false, cache: ['redisCreate', 'redisEdit'], }, }, ]; // ============================================================================= // 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...'); await prisma.$transaction([ prisma.sysUserRole.deleteMany(), prisma.sysUser.deleteMany(), prisma.sysRole.deleteMany(), ]); // Upsert role by unique name const roleCreateData: Prisma.SysRoleUncheckedCreateInput = { id: '6946c613ea4266475e73d074', name: '管理员', status: true, remark: '管理员专用', }; const role = await prisma.sysRole.upsert({ where: { name: '管理员' }, update: { remark: '管理员专用', status: true }, create: roleCreateData, }); // Upsert admin user by unique username const adminUserCreateData: Prisma.SysUserUncheckedCreateInput = { username: 'admin', password: '$2b$12$iS0UJ1YqSal0N3uwin/OvOABUINAclcZGjHNyGFC7mlwRYTFjGQ26', remark: '默认拥有所有菜单权限,不需要配置角色', }; const adminUser = await prisma.sysUser.upsert({ where: { username: 'admin' }, // Do not overwrite password on existing user update: { remark: '默认拥有所有菜单权限,不需要配置角色' }, create: adminUserCreateData, }); // Link user and role if not already linked const existingLink = await prisma.sysUserRole.findFirst({ where: { userId: adminUser.id, roleId: role.id }, }); if (!existingLink) { const userRoleCreateData: Prisma.SysUserRoleUncheckedCreateInput = { userId: adminUser.id, roleId: role.id, }; await prisma.sysUserRole.create({ data: userRoleCreateData, }); } 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...'); await prisma.$runCommandRaw({ delete: 'sys_api_permission', deletes: [{ q: {}, limit: 0 }], }); await prisma.$runCommandRaw({ delete: 'sys_menu', deletes: [{ q: {}, limit: 0 }], }); 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 menuCreateData: Prisma.SysMenuUncheckedCreateInput = { parentId, title: seed.title, status: true, type: seed.type, order: seed.order, frontendAuth: seed.path, path: seed.path, name: seed.name, icon: seed.icon, redirect: null, componentKey: seed.componentKey ?? null, meta: seed.meta ?? undefined, ...permissions, }; const created = await prisma.sysMenu.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, componentKey: seed.componentKey ?? null, // meta undefined means do not overwrite; if provided, update ...(seed.meta !== undefined ? { meta: seed.meta as Prisma.InputJsonValue } : {}), ...permissions, }, create: { ...menuCreateData, }, }); 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() { const adsModules = [ { adType: AdType.STARTUP, adsModule: '启动页', moduleDesc: '启动页(10:21)', seq: 1, }, { adType: AdType.CAROUSEL, adsModule: '轮播', moduleDesc: '轮播(2:1)', seq: 2, }, { adType: AdType.POPUP_ICON, adsModule: '弹窗-图标', moduleDesc: '弹窗-图标(1:1)', seq: 3, }, { adType: AdType.POPUP_IMAGE, adsModule: '弹窗-图片', moduleDesc: '弹窗-图片(2:3)', seq: 4, }, { adType: AdType.POPUP_OFFICIAL, adsModule: '弹窗-官方', moduleDesc: '弹窗-官方(2:3)', seq: 5, }, { adType: AdType.WATERFALL_ICON, adsModule: '瀑布流-图标', moduleDesc: '瀑布流-图标(1:1)', seq: 6, }, { adType: AdType.WATERFALL_TEXT, adsModule: '瀑布流-文字', moduleDesc: '瀑布流-文字', seq: 7, }, { adType: AdType.WATERFALL_VIDEO, adsModule: '瀑布流-视频', moduleDesc: '瀑布流-视频(8:5)', seq: 8, }, { adType: AdType.FLOATING_BOTTOM, adsModule: '悬浮-底部', moduleDesc: '悬浮-底部(1:1)', seq: 9, }, { adType: AdType.FLOATING_EDGE, adsModule: '悬浮-边缘', moduleDesc: '悬浮-边缘(1:1)', seq: 10, }, { adType: AdType.BANNER, adsModule: 'banner', moduleDesc: 'banner(4:1)', seq: 11, }, { adType: AdType.PREROLL, adsModule: '片头', moduleDesc: '片头(8:5)', seq: 12, }, { adType: AdType.PAUSE, adsModule: '暂停', moduleDesc: '暂停(2:1)', seq: 13, }, ]; console.log('🌱 Starting database seeding...\n'); try { console.log('cleaning ads modules...'); await prisma.adsModule.deleteMany(); console.log('seeding ads modules...'); for (const module of adsModules) { await prisma.adsModule.create({ data: module, }); } // Seed in order: users first, then menus console.log('seeding users...'); await seedUsers(); console.log(''); console.log('seeding menus...'); await seedMenus(); console.log(''); console.log('seeding system config...'); await seedSysConfig(); 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); });