seed.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. // prisma/mysql/seed.ts
  2. import { PrismaClient, MenuType } from '@prisma/mysql/client';
  3. import type { Prisma } from '@prisma/mysql/client';
  4. const prisma = new PrismaClient();
  5. // =============================================================================
  6. // MENU SEEDS DATA
  7. // =============================================================================
  8. interface SeedMenu {
  9. legacyId: number;
  10. legacyParentId: number | null;
  11. title: string;
  12. type: MenuType;
  13. name: string;
  14. path: string;
  15. icon: string | null;
  16. component_key?: string | null;
  17. order: number;
  18. meta?: Prisma.JsonValue;
  19. }
  20. const MENU_SEEDS: SeedMenu[] = [
  21. // ======================
  22. // 营销中心 (Marketing)
  23. // ======================
  24. {
  25. legacyId: 1,
  26. legacyParentId: null,
  27. title: '营销中心',
  28. type: 'DIRECTORY',
  29. name: 'marketingRoot',
  30. path: '/marketing',
  31. icon: 'i-ri:megaphone-line',
  32. order: 100,
  33. meta: {
  34. title: '营销中心',
  35. i18n: 'route.marketing.root',
  36. icon: 'i-ri:megaphone-line',
  37. },
  38. },
  39. {
  40. legacyId: 2,
  41. legacyParentId: 1,
  42. title: '视频管理',
  43. type: 'MENU',
  44. name: 'marketingVideos',
  45. path: '/marketing/videos',
  46. icon: 'i-ri:video-line',
  47. order: 1,
  48. meta: {
  49. title: '视频管理',
  50. i18n: 'route.marketing.videos',
  51. icon: 'i-ri:video-line',
  52. },
  53. },
  54. {
  55. legacyId: 3,
  56. legacyParentId: 1,
  57. title: '分类管理',
  58. type: 'MENU',
  59. name: 'marketingCategories',
  60. path: '/marketing/categories',
  61. icon: 'i-ri:folder-3-line',
  62. order: 2,
  63. meta: {
  64. title: '分类管理',
  65. i18n: 'route.marketing.categories',
  66. icon: 'i-ri:folder-3-line',
  67. },
  68. },
  69. {
  70. legacyId: 4,
  71. legacyParentId: 1,
  72. title: '标签管理',
  73. type: 'MENU',
  74. name: 'marketingTags',
  75. path: '/marketing/tags',
  76. icon: 'i-ri:price-tag-3-line',
  77. order: 3,
  78. meta: {
  79. title: '标签管理',
  80. i18n: 'route.marketing.tags',
  81. icon: 'i-ri:price-tag-3-line',
  82. },
  83. },
  84. {
  85. legacyId: 5,
  86. legacyParentId: 1,
  87. title: '广告管理',
  88. type: 'MENU',
  89. name: 'marketingAds',
  90. path: '/marketing/ads',
  91. icon: 'i-ri:advertisement-line',
  92. order: 4,
  93. meta: {
  94. title: '广告管理',
  95. i18n: 'route.marketing.ads',
  96. icon: 'i-ri:advertisement-line',
  97. },
  98. },
  99. {
  100. legacyId: 6,
  101. legacyParentId: 1,
  102. title: '参数管理',
  103. type: 'MENU',
  104. name: 'marketingParams',
  105. path: '/marketing/params',
  106. icon: 'i-ri:settings-3-line',
  107. order: 5,
  108. meta: {
  109. title: '参数管理',
  110. i18n: 'route.marketing.params',
  111. icon: 'i-ri:settings-3-line',
  112. },
  113. },
  114. {
  115. legacyId: 7,
  116. legacyParentId: 1,
  117. title: '渠道管理',
  118. type: 'MENU',
  119. name: 'marketingChannels',
  120. path: '/marketing/channels',
  121. icon: 'i-ri:settings-3-line',
  122. order: 6,
  123. meta: {
  124. title: '渠道管理',
  125. i18n: 'route.marketing.channels',
  126. icon: 'i-ri:settings-3-line',
  127. },
  128. },
  129. // ======================
  130. // 数据中心 (Data Center)
  131. // ======================
  132. {
  133. legacyId: 8,
  134. legacyParentId: null,
  135. title: '数据中心',
  136. type: 'DIRECTORY',
  137. name: 'dataCenterRoot',
  138. path: '/data',
  139. icon: 'i-ri:database-2-line',
  140. order: 200,
  141. meta: {
  142. title: '数据中心',
  143. i18n: 'route.data.root',
  144. icon: 'i-ri:database-2-line',
  145. },
  146. },
  147. {
  148. legacyId: 9,
  149. legacyParentId: 8,
  150. title: 'APP访问记录',
  151. type: 'MENU',
  152. name: 'dataAppAccessLogs',
  153. path: '/data/app-access-logs',
  154. icon: 'i-ri:smartphone-line',
  155. order: 1,
  156. meta: {
  157. title: 'APP访问记录',
  158. i18n: 'route.data.appAccessLogs',
  159. icon: 'i-ri:smartphone-line',
  160. },
  161. },
  162. {
  163. legacyId: 10,
  164. legacyParentId: 8,
  165. title: '广告点击记录',
  166. type: 'MENU',
  167. name: 'dataAdsClickLogs',
  168. path: '/data/ads-click-logs',
  169. icon: 'i-ri:cursor-line',
  170. order: 2,
  171. meta: {
  172. title: '广告点击记录',
  173. i18n: 'route.data.adsClickLogs',
  174. icon: 'i-ri:cursor-line',
  175. },
  176. },
  177. // ======================
  178. // 统计中心 (Stats Center)
  179. // ======================
  180. {
  181. legacyId: 11,
  182. legacyParentId: null,
  183. title: '统计中心',
  184. type: 'DIRECTORY',
  185. name: 'statsCenterRoot',
  186. path: '/stats',
  187. icon: 'i-ri:line-chart-line',
  188. order: 300,
  189. meta: {
  190. title: '统计中心',
  191. i18n: 'route.stats.root',
  192. icon: 'i-ri:line-chart-line',
  193. },
  194. },
  195. {
  196. legacyId: 12,
  197. legacyParentId: 11,
  198. title: '每日统计',
  199. type: 'MENU',
  200. name: 'statsDaily',
  201. path: '/stats/daily',
  202. icon: 'i-ri:calendar-line',
  203. order: 1,
  204. meta: {
  205. title: '每日统计',
  206. i18n: 'route.stats.daily',
  207. icon: 'i-ri:calendar-line',
  208. },
  209. },
  210. {
  211. legacyId: 13,
  212. legacyParentId: 11,
  213. title: '广告统计',
  214. type: 'MENU',
  215. name: 'statsAds',
  216. path: '/stats/ads',
  217. icon: 'i-ri:bar-chart-box-line',
  218. order: 2,
  219. meta: {
  220. title: '广告统计',
  221. i18n: 'route.stats.ads',
  222. icon: 'i-ri:bar-chart-box-line',
  223. },
  224. },
  225. {
  226. legacyId: 14,
  227. legacyParentId: 11,
  228. title: '广告汇总',
  229. type: 'MENU',
  230. name: 'statsAdsSummary',
  231. path: '/stats/ads-summary',
  232. icon: 'i-ri:pie-chart-line',
  233. order: 3,
  234. meta: {
  235. title: '广告汇总',
  236. i18n: 'route.stats.adsSummary',
  237. icon: 'i-ri:pie-chart-line',
  238. },
  239. },
  240. {
  241. legacyId: 15,
  242. legacyParentId: 11,
  243. title: '网站统计',
  244. type: 'MENU',
  245. name: 'statsSite',
  246. path: '/stats/site',
  247. icon: 'i-ri:global-line',
  248. order: 4,
  249. meta: {
  250. title: '网站统计',
  251. i18n: 'route.stats.site',
  252. icon: 'i-ri:global-line',
  253. },
  254. },
  255. // ======================
  256. // 系统管理 (System)
  257. // ======================
  258. {
  259. legacyId: 16,
  260. legacyParentId: null,
  261. title: '系统管理',
  262. type: 'DIRECTORY',
  263. name: 'systemRoot',
  264. path: '/system',
  265. icon: 'i-ri:settings-4-line',
  266. order: 400,
  267. meta: {
  268. title: '系统管理',
  269. i18n: 'route.system.root',
  270. icon: 'i-ri:settings-4-line',
  271. },
  272. },
  273. {
  274. legacyId: 17,
  275. legacyParentId: 16,
  276. title: '系统用户',
  277. type: 'MENU',
  278. name: 'systemUsers',
  279. path: '/system/users',
  280. icon: 'i-ri:user-line',
  281. component_key: '@/views/system/users/list.vue',
  282. order: 1,
  283. meta: {
  284. title: '系统用户',
  285. i18n: 'route.system.users',
  286. icon: 'i-ri:user-line',
  287. },
  288. },
  289. {
  290. legacyId: 18,
  291. legacyParentId: 16,
  292. title: '角色列表',
  293. type: 'MENU',
  294. name: 'systemRoles',
  295. path: '/system/roles',
  296. icon: 'i-ri:shield-user-line',
  297. order: 20,
  298. meta: {
  299. title: '角色列表',
  300. i18n: 'route.system.roles',
  301. icon: 'i-ri:shield-user-line',
  302. },
  303. },
  304. {
  305. legacyId: 19,
  306. legacyParentId: 16,
  307. title: '菜单管理',
  308. type: 'MENU',
  309. name: 'systemMenus',
  310. path: '/system/menus',
  311. icon: 'i-ri:menu-line',
  312. order: 30,
  313. meta: {
  314. title: '菜单管理',
  315. i18n: 'route.system.menus',
  316. icon: 'i-ri:menu-line',
  317. },
  318. },
  319. {
  320. legacyId: 20,
  321. legacyParentId: 16,
  322. title: '操作日志',
  323. type: 'MENU',
  324. name: 'systemOperationLogs',
  325. path: '/system/operation-logs',
  326. icon: 'i-ri:time-line',
  327. order: 40,
  328. meta: {
  329. title: '操作日志',
  330. i18n: 'route.system.operationLogs',
  331. icon: 'i-ri:time-line',
  332. },
  333. },
  334. // ======================
  335. // SUBMENUs under Roles
  336. // ======================
  337. {
  338. legacyId: 21,
  339. legacyParentId: 18,
  340. title: '角色列表',
  341. type: 'SUBMENU',
  342. name: 'systemRoleList',
  343. path: '/system/roles/list',
  344. icon: 'i-ri:shield-user-line',
  345. component_key: '@/views/system/roles/list.vue',
  346. order: 1,
  347. meta: {
  348. title: '角色列表',
  349. i18n: 'route.system.role.list',
  350. sidebar: false,
  351. breadcrumb: false,
  352. cache: ['systemRoleCreate', 'systemRoleEdit'],
  353. },
  354. },
  355. {
  356. legacyId: 22,
  357. legacyParentId: 18,
  358. title: '新增角色',
  359. type: 'SUBMENU',
  360. name: 'systemRoleCreate',
  361. path: '/system/roles/create',
  362. icon: 'i-ri:add-circle-line',
  363. component_key: '@/views/system/roles/detail.vue',
  364. order: 2,
  365. meta: {
  366. title: '新增角色',
  367. i18n: 'route.system.role.create',
  368. sidebar: false,
  369. cache: true,
  370. activeMenu: '/system/roles',
  371. noCache: 'systemRoleList',
  372. },
  373. },
  374. {
  375. legacyId: 23,
  376. legacyParentId: 18,
  377. title: '编辑角色',
  378. type: 'SUBMENU',
  379. name: 'systemRoleEdit',
  380. path: '/system/roles/:id/edit',
  381. icon: 'i-ri:pencil-line',
  382. component_key: '@/views/system/roles/detail.vue',
  383. order: 3,
  384. meta: {
  385. title: '编辑角色',
  386. i18n: 'route.system.role.edit',
  387. sidebar: false,
  388. cache: true,
  389. activeMenu: '/system/roles',
  390. noCache: 'systemRoleList',
  391. },
  392. },
  393. // ======================
  394. // SUBMENUs under Menus
  395. // ======================
  396. {
  397. legacyId: 24,
  398. legacyParentId: 19,
  399. title: '菜单列表',
  400. type: 'SUBMENU',
  401. name: 'systemMenuList',
  402. path: '/system/menus/list',
  403. icon: 'i-ri:menu-line',
  404. component_key: '@/views/system/menus/list.vue',
  405. order: 1,
  406. meta: {
  407. title: '菜单列表',
  408. i18n: 'route.system.menu.list',
  409. sidebar: false,
  410. breadcrumb: false,
  411. cache: ['systemMenuCreate', 'systemMenuEdit'],
  412. },
  413. },
  414. {
  415. legacyId: 25,
  416. legacyParentId: 19,
  417. title: '新增菜单',
  418. type: 'SUBMENU',
  419. name: 'systemMenuCreate',
  420. path: '/system/menus/create',
  421. icon: 'i-ri:add-circle-line',
  422. component_key: '@/views/system/menus/detail.vue',
  423. order: 2,
  424. meta: {
  425. title: '新增菜单',
  426. i18n: 'route.system.menu.create',
  427. sidebar: false,
  428. cache: true,
  429. activeMenu: '/system/menus',
  430. noCache: 'systemMenuList',
  431. },
  432. },
  433. {
  434. legacyId: 26,
  435. legacyParentId: 19,
  436. title: '编辑菜单',
  437. type: 'SUBMENU',
  438. name: 'systemMenuEdit',
  439. path: '/system/menus/:id/edit',
  440. icon: 'i-ri:pencil-line',
  441. component_key: '@/views/system/menus/detail.vue',
  442. order: 3,
  443. meta: {
  444. title: '编辑菜单',
  445. i18n: 'route.system.menu.edit',
  446. sidebar: false,
  447. cache: true,
  448. activeMenu: '/system/menus',
  449. noCache: 'systemMenuList',
  450. },
  451. },
  452. ];
  453. // =============================================================================
  454. // HELPER FUNCTIONS
  455. // =============================================================================
  456. /**
  457. * Get default action permissions based on menu type
  458. */
  459. function getDefaultPermissions(type: MenuType) {
  460. switch (type) {
  461. case 'DIRECTORY':
  462. return { canView: 1, canCreate: 0, canUpdate: 0, canDelete: 0 };
  463. case 'MENU':
  464. case 'SUBMENU':
  465. return { canView: 1, canCreate: 1, canUpdate: 1, canDelete: 1 };
  466. case 'BUTTON':
  467. return { canView: 0, canCreate: 1, canUpdate: 0, canDelete: 0 };
  468. default:
  469. return { canView: 0, canCreate: 0, canUpdate: 0, canDelete: 0 };
  470. }
  471. }
  472. // =============================================================================
  473. // SEED FUNCTIONS
  474. // =============================================================================
  475. /**
  476. * Seed users and roles
  477. */
  478. async function seedUsers() {
  479. console.log('📝 Seeding users and roles...');
  480. const role = await prisma.role.create({
  481. data: {
  482. name: '管理员',
  483. remark: '管理员专用',
  484. },
  485. });
  486. const adminUser = await prisma.user.create({
  487. data: {
  488. username: 'admin',
  489. password: '$2b$12$iS0UJ1YqSal0N3uwin/OvOABUINAclcZGjHNyGFC7mlwRYTFjGQ26',
  490. remark: '默认拥有所有菜单权限,不需要配置角色',
  491. },
  492. });
  493. await prisma.userRole.create({
  494. data: {
  495. userId: adminUser.id,
  496. roleId: role.id,
  497. },
  498. });
  499. console.log('✅ Users and roles seeded successfully');
  500. console.log(` - Admin user: ${adminUser.username}`);
  501. console.log(` - Role: ${role.name}`);
  502. }
  503. /**
  504. * Seed menus with multi-pass insertion to handle parent-child relationships
  505. */
  506. async function seedMenus() {
  507. console.log('📝 Seeding menus...');
  508. const legacyIdToNewId = new Map<number, number>();
  509. const inserted = new Set<number>();
  510. // Multi-pass insertion: insert records whose parent is either null or already inserted
  511. while (inserted.size < MENU_SEEDS.length) {
  512. let progress = false;
  513. for (const seed of MENU_SEEDS) {
  514. if (inserted.has(seed.legacyId)) {
  515. continue;
  516. }
  517. // If has a parent, but parent not inserted yet → skip this round
  518. if (
  519. seed.legacyParentId !== null &&
  520. !legacyIdToNewId.has(seed.legacyParentId)
  521. ) {
  522. continue;
  523. }
  524. const parentId =
  525. seed.legacyParentId === null
  526. ? null
  527. : legacyIdToNewId.get(seed.legacyParentId)!;
  528. const permissions = getDefaultPermissions(seed.type);
  529. const created = await prisma.menu.create({
  530. data: {
  531. parentId,
  532. title: seed.title,
  533. status: true,
  534. type: seed.type,
  535. order: seed.order,
  536. // Use path as frontendAuth for RBAC tracking
  537. frontendAuth: seed.path,
  538. path: seed.path,
  539. name: seed.name,
  540. icon: seed.icon,
  541. redirect: null,
  542. component_key: seed.component_key ?? null,
  543. meta: seed.meta ?? undefined,
  544. // Set default permissions based on type
  545. ...permissions,
  546. },
  547. });
  548. legacyIdToNewId.set(seed.legacyId, created.id);
  549. inserted.add(seed.legacyId);
  550. progress = true;
  551. console.log(
  552. ` ✓ Menu [${seed.type.padEnd(9)}] ${seed.name.padEnd(25)} → ID: ${created.id}`,
  553. );
  554. }
  555. if (!progress) {
  556. // Nothing could be inserted in this pass → some parentId is invalid or cyclic
  557. const pending = MENU_SEEDS.filter((s) => !inserted.has(s.legacyId)).map(
  558. (s) => ({
  559. legacyId: s.legacyId,
  560. legacyParentId: s.legacyParentId,
  561. name: s.name,
  562. }),
  563. );
  564. console.error('❌ Could not resolve parents for menus:', pending);
  565. throw new Error(
  566. 'Menu seeding aborted: unresolved parent relationships. Check legacyParentId values.',
  567. );
  568. }
  569. }
  570. console.log('✅ Menus seeded successfully');
  571. console.log(` - Total menus: ${inserted.size}`);
  572. }
  573. // =============================================================================
  574. // MAIN EXECUTION
  575. // =============================================================================
  576. async function main() {
  577. console.log('🌱 Starting database seeding...\n');
  578. try {
  579. // Seed in order: users first, then menus
  580. await seedUsers();
  581. console.log('');
  582. await seedMenus();
  583. console.log('\n🎉 Database seeding completed successfully!');
  584. } catch (error) {
  585. console.error('\n❌ Error during seeding:', error);
  586. throw error;
  587. }
  588. }
  589. main()
  590. .then(async () => {
  591. await prisma.$disconnect();
  592. })
  593. .catch(async (e) => {
  594. console.error(e);
  595. await prisma.$disconnect();
  596. process.exit(1);
  597. });