AidopMenuLinkSync.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. using Admin.NET.Core;
  2. using Microsoft.Extensions.DependencyInjection;
  3. using SqlSugar;
  4. namespace Admin.NET.Plugin.AiDOP.Infrastructure;
  5. /// <summary>
  6. /// 将 Ai-DOP 种子菜单 Id 补写到 <c>sys_tenant_menu</c> / <c>sys_role_menu</c>。
  7. /// 解决:<c>SysTenantMenuSeedData</c> 带 IgnoreUpdateSeed,库已存在时新增菜单不会自动进租户/角色;多租户下仅补默认租户会导致其它租户侧栏与菜单管理不可见。
  8. /// </summary>
  9. public static class AidopMenuLinkSync
  10. {
  11. /// <summary>与框架种子中首条「系统管理员」角色 Id 一致。</summary>
  12. private const long SysAdminRoleId = 1300000000101L;
  13. private const long LegacyMaterialSubstitutionMenuId = 1329003000005L;
  14. private const long DeprecatedMaterialSubstitutionMenuId = 1329002000004L;
  15. private const long DeprecatedS8DashboardChildMenuId = 1329008000001L;
  16. private const long S8DirMenuId = 1321000009000L;
  17. public static void EnsureLinked(IServiceProvider services)
  18. {
  19. using var scope = services.CreateScope();
  20. var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
  21. // Database.json 常关闭 EnableInitSeed/EnableIncreSeed,新增种子菜单不会入库;先补插 S2 三级结构再挂租户/角色。
  22. EnsureS2ManufacturingCollaborationSeedMenus(db);
  23. NormalizeMaterialSubstitutionMenu(db);
  24. NormalizeS1OrderWorkOrderParents(db);
  25. NormalizeS2ManufacturingParents(db);
  26. NormalizeS2ScheduleExceptionMenu(db);
  27. NormalizeS2OperationPlanLeafMenus(db);
  28. NormalizeS2WorkOrderProgressKanbanMenu(db);
  29. EnsureLinkagePlanMenu(db);
  30. RemoveDeprecatedS8DashboardChildMenu(db);
  31. NormalizeS8MenuParents(db);
  32. var seedMenus = new global::Admin.NET.Plugin.AiDOP.SysMenuSeedData().HasData().ToList();
  33. var seedMenuIds = seedMenus.Select(m => m.Id).ToHashSet();
  34. var existingMenuIds = db.Queryable<SysMenu>()
  35. .Where(m => seedMenuIds.Contains(m.Id))
  36. .Select(m => m.Id)
  37. .ToList()
  38. .ToHashSet();
  39. if (existingMenuIds.Count == 0)
  40. return;
  41. var tenantIds = db.Queryable<SysTenant>().Select(t => t.Id).ToList();
  42. if (tenantIds.Count == 0)
  43. return;
  44. var tenantMenuPairs = db.Queryable<SysTenantMenu>()
  45. .Where(tm => tenantIds.Contains(tm.TenantId) && seedMenuIds.Contains(tm.MenuId))
  46. .Select(tm => new { tm.TenantId, tm.MenuId })
  47. .ToList()
  48. .Select(x => (x.TenantId, x.MenuId))
  49. .ToHashSet();
  50. var tenantRows = new List<SysTenantMenu>();
  51. foreach (var tid in tenantIds)
  52. {
  53. foreach (var menu in seedMenus)
  54. {
  55. if (!existingMenuIds.Contains(menu.Id))
  56. continue;
  57. if (tenantMenuPairs.Contains((tid, menu.Id)))
  58. continue;
  59. tenantRows.Add(new SysTenantMenu
  60. {
  61. Id = CommonUtil.GetFixedHashCode("" + tid + menu.Id, 1300000000000),
  62. TenantId = tid,
  63. MenuId = menu.Id
  64. });
  65. }
  66. }
  67. if (tenantRows.Count > 0)
  68. db.Insertable(tenantRows).ExecuteCommand();
  69. // 已为任一 Ai-DOP 种子菜单授权的角色,补全新增子菜单;并始终包含默认租户系统管理员角色。
  70. var roleIdsWithAnyAidop = db.Queryable<SysRoleMenu>()
  71. .Where(rm => seedMenuIds.Contains(rm.MenuId))
  72. .Select(rm => rm.RoleId)
  73. .ToList()
  74. .Distinct()
  75. .ToHashSet();
  76. roleIdsWithAnyAidop.Add(SysAdminRoleId);
  77. var roleMenuPairs = db.Queryable<SysRoleMenu>()
  78. .Where(rm => roleIdsWithAnyAidop.Contains(rm.RoleId) && seedMenuIds.Contains(rm.MenuId))
  79. .Select(rm => new { rm.RoleId, rm.MenuId })
  80. .ToList()
  81. .Select(x => (x.RoleId, x.MenuId))
  82. .ToHashSet();
  83. // SysRoleMenu 主键须全局唯一。使用 menu.Id + (roleId % 1300000000000) 时,不同 (MenuId, RoleId) 会算出相同 Id(如 1322000000208),
  84. // 批量插入失败,新菜单无法进入 sys_role_menu,侧栏不刷新。与 SysTenantMenu 一致,用固定哈希避免碰撞。
  85. var roleRows = new List<SysRoleMenu>();
  86. var pendingRoleMenuIds = new HashSet<long>();
  87. foreach (var roleId in roleIdsWithAnyAidop)
  88. {
  89. foreach (var menu in seedMenus)
  90. {
  91. if (!existingMenuIds.Contains(menu.Id))
  92. continue;
  93. if (roleMenuPairs.Contains((roleId, menu.Id)))
  94. continue;
  95. var rmId = CommonUtil.GetFixedHashCode($"{roleId}:{menu.Id}", 1300000000000);
  96. while (!pendingRoleMenuIds.Add(rmId))
  97. rmId++;
  98. roleRows.Add(new SysRoleMenu
  99. {
  100. Id = rmId,
  101. RoleId = roleId,
  102. MenuId = menu.Id
  103. });
  104. }
  105. }
  106. if (roleRows.Count > 0)
  107. db.Insertable(roleRows).ExecuteCommand();
  108. }
  109. /// <summary>
  110. /// S2「制造协同」二级目录与三级子菜单:<c>Database.json</c> 关闭种子时不会入库,导致侧栏无「工单工序排程」等项。
  111. /// 从 <see cref="SysMenuSeedData.HasData"/> 取完整行一次性补插(含目录 Redirect)。
  112. /// </summary>
  113. private static void EnsureS2ManufacturingCollaborationSeedMenus(ISqlSugarClient db)
  114. {
  115. const long aidopRootId = 1320990000101L;
  116. var fullSeed = new global::Admin.NET.Plugin.AiDOP.SysMenuSeedData().HasData().ToList();
  117. var byId = fullSeed.GroupBy(m => m.Id).ToDictionary(g => g.Key, g => g.First());
  118. // 父链:Ai-DOP 根 → S2 一级目录 → 三个二级目录 → 三级菜单(同批插入时父行尚未落库,用 cumulativeIds 判定)
  119. var orderedIds = new long[]
  120. {
  121. aidopRootId,
  122. 1321000003000L,
  123. 1322000000006L,
  124. 1322000000007L,
  125. 1322000000008L,
  126. 1322000000108L,
  127. 1329002100001L,
  128. 1329002100011L,
  129. 1329002100012L,
  130. 1329002100013L,
  131. 1329002100014L,
  132. 1329002100015L,
  133. 1329002100021L,
  134. };
  135. var existing = db.Queryable<SysMenu>().Where(m => orderedIds.Contains(m.Id)).Select(m => m.Id).ToList().ToHashSet();
  136. var cumulativeIds = new HashSet<long>(existing);
  137. var toInsert = new List<SysMenu>();
  138. foreach (var id in orderedIds)
  139. {
  140. if (cumulativeIds.Contains(id))
  141. continue;
  142. if (!byId.TryGetValue(id, out var row))
  143. continue;
  144. if (row.Pid != 0 && !cumulativeIds.Contains(row.Pid))
  145. continue;
  146. toInsert.Add(row);
  147. cumulativeIds.Add(id);
  148. }
  149. if (toInsert.Count > 0)
  150. db.Insertable(toInsert).ExecuteCommand();
  151. }
  152. /// <summary>
  153. /// 计划联动看板(Id=1322000000107):<c>Database.json</c> 常关闭 <c>EnableIncreSeed</c>,种子不会入库,故在启动时补插/纠偏。
  154. /// 父级为产销协同看板目录 1322000000005。
  155. /// </summary>
  156. private static void EnsureLinkagePlanMenu(ISqlSugarClient db)
  157. {
  158. const long menuId = 1322000000107L;
  159. const long salesKanBanDirId = 1322000000005L;
  160. if (!db.Queryable<SysMenu>().Any(m => m.Id == salesKanBanDirId))
  161. return;
  162. var ct = DateTime.Parse("2022-02-10 00:00:00");
  163. if (!db.Queryable<SysMenu>().Any(m => m.Id == menuId))
  164. {
  165. db.Insertable(new SysMenu
  166. {
  167. Id = menuId,
  168. Pid = salesKanBanDirId,
  169. Title = "计划联动看板",
  170. Path = "/aidop/s1/SalesKanBan/linkage-plan",
  171. Name = "aidopS1LinkagePlan",
  172. Component = "/aidop/business/linkagePlanList",
  173. Icon = "ele-DataBoard",
  174. Type = MenuTypeEnum.Menu,
  175. CreateTime = ct,
  176. OrderNo = 30,
  177. Status = StatusEnum.Enable,
  178. Remark = "S1 计划联动看板(LinkagePlan)"
  179. }).ExecuteCommand();
  180. return;
  181. }
  182. var row = db.Queryable<SysMenu>().First(m => m.Id == menuId);
  183. if (row.Pid != salesKanBanDirId
  184. || row.Path != "/aidop/s1/SalesKanBan/linkage-plan"
  185. || row.Component != "/aidop/business/linkagePlanList")
  186. {
  187. row.Pid = salesKanBanDirId;
  188. row.Path = "/aidop/s1/SalesKanBan/linkage-plan";
  189. row.Name = "aidopS1LinkagePlan";
  190. row.Component = "/aidop/business/linkagePlanList";
  191. row.Title = "计划联动看板";
  192. row.Icon = "ele-DataBoard";
  193. row.OrderNo = 30;
  194. row.Type = MenuTypeEnum.Menu;
  195. db.Updateable(row)
  196. .UpdateColumns(m => new { m.Pid, m.Path, m.Name, m.Component, m.Title, m.Icon, m.OrderNo, m.Type })
  197. .ExecuteCommand();
  198. }
  199. }
  200. private static void NormalizeMaterialSubstitutionMenu(ISqlSugarClient db)
  201. {
  202. var deprecatedMenuId = DeprecatedMaterialSubstitutionMenuId;
  203. var legacyMenuId = LegacyMaterialSubstitutionMenuId;
  204. var legacyMenu = db.Queryable<SysMenu>()
  205. .First(m => m.Id == legacyMenuId);
  206. if (legacyMenu != null)
  207. {
  208. legacyMenu.Title = "物料替代";
  209. legacyMenu.Path = "/aidop/s0/manufacturing/material-substitution";
  210. legacyMenu.Name = "aidopS0MfgMaterialSubstitution";
  211. legacyMenu.Component = "/aidop/s0/manufacturing/MaterialSubstitutionList";
  212. legacyMenu.Remark = "S0 物料替代";
  213. db.Updateable(legacyMenu)
  214. .UpdateColumns(m => new { m.Title, m.Path, m.Name, m.Component, m.Remark })
  215. .ExecuteCommand();
  216. }
  217. var deprecatedExists = db.Queryable<SysMenu>().Any(m => m.Id == deprecatedMenuId);
  218. if (!deprecatedExists)
  219. return;
  220. db.Deleteable<SysUserMenu>().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand();
  221. db.Deleteable<SysRoleMenu>().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand();
  222. db.Deleteable<SysTenantMenu>().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand();
  223. db.Deleteable<SysMenu>().Where(x => x.Id == deprecatedMenuId).ExecuteCommand();
  224. }
  225. private static void RemoveDeprecatedS8DashboardChildMenu(ISqlSugarClient db)
  226. {
  227. const long deprecatedMenuId = DeprecatedS8DashboardChildMenuId;
  228. var deprecatedExists = db.Queryable<SysMenu>().Any(m => m.Id == deprecatedMenuId);
  229. if (!deprecatedExists)
  230. return;
  231. db.Deleteable<SysUserMenu>().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand();
  232. db.Deleteable<SysRoleMenu>().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand();
  233. db.Deleteable<SysTenantMenu>().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand();
  234. db.Deleteable<SysMenu>().Where(x => x.Id == deprecatedMenuId).ExecuteCommand();
  235. }
  236. private static void NormalizeS8MenuParents(ISqlSugarClient db)
  237. {
  238. var s8MenuIds = new long[]
  239. {
  240. 1329008000002L,
  241. 1329008000003L,
  242. 1329008000004L,
  243. 1329008000010L,
  244. 1329008000011L,
  245. 1329008000012L,
  246. 1329008000013L,
  247. 1329008000014L,
  248. 1329008000015L,
  249. 1329008000016L,
  250. 1329008000020L
  251. };
  252. db.Updateable<SysMenu>()
  253. .SetColumns(x => x.Pid == S8DirMenuId)
  254. .Where(x => s8MenuIds.Contains(x.Id) && x.Pid != S8DirMenuId)
  255. .ExecuteCommand();
  256. }
  257. /// <summary>
  258. /// 将 S1「订单管理」「工单管理」升级为目录并修正 path/name;
  259. /// 「产销协同看板」单独菜单路径为 /aidop/s1/SalesKanBan;
  260. /// 「工单下达」父级须为工单管理目录 1322000000004(历史错误曾挂到 0005)。
  261. /// 「工单工序排产」1322000000108 已迁至 S2「生产排程」下,见 <see cref="NormalizeS2ManufacturingParents"/>。
  262. /// </summary>
  263. private static void NormalizeS1OrderWorkOrderParents(ISqlSugarClient db)
  264. {
  265. const long orderMgmtId = 1322000000003L;
  266. const long workOrderMgmtId = 1322000000004L;
  267. const long salesKanBanMenuId = 1322000000005L;
  268. const long workOrderDispatchMenuId = 1322000000106L;
  269. var orderMgmt = db.Queryable<SysMenu>().First(m => m.Id == orderMgmtId);
  270. if (orderMgmt != null)
  271. {
  272. orderMgmt.Type = MenuTypeEnum.Dir;
  273. orderMgmt.Component = "Layout";
  274. orderMgmt.Path = "/aidop/s1/order-mgmt";
  275. orderMgmt.Name = "aidopS1OrderMgmt";
  276. orderMgmt.Icon = "ele-Folder";
  277. db.Updateable(orderMgmt)
  278. .UpdateColumns(m => new { m.Type, m.Component, m.Path, m.Name, m.Icon })
  279. .ExecuteCommand();
  280. }
  281. var workOrderMgmt = db.Queryable<SysMenu>().First(m => m.Id == workOrderMgmtId);
  282. if (workOrderMgmt != null)
  283. {
  284. workOrderMgmt.Type = MenuTypeEnum.Dir;
  285. workOrderMgmt.Component = "Layout";
  286. workOrderMgmt.Path = "/aidop/s1/workorder-mgmt";
  287. workOrderMgmt.Name = "aidopS1WorkOrderMgmt";
  288. workOrderMgmt.Icon = "ele-Folder";
  289. db.Updateable(workOrderMgmt)
  290. .UpdateColumns(m => new { m.Type, m.Component, m.Path, m.Name, m.Icon })
  291. .ExecuteCommand();
  292. }
  293. var salesKanBan = db.Queryable<SysMenu>().First(m => m.Id == salesKanBanMenuId);
  294. if (salesKanBan != null)
  295. {
  296. salesKanBan.Type = MenuTypeEnum.Menu;
  297. salesKanBan.Component = "/aidop/kanban/s1";
  298. salesKanBan.Path = "/aidop/s1/SalesKanBan";
  299. salesKanBan.Name = "aidopS1003";
  300. salesKanBan.Icon = "ele-DataAnalysis";
  301. db.Updateable(salesKanBan)
  302. .UpdateColumns(m => new { m.Type, m.Component, m.Path, m.Name, m.Icon })
  303. .ExecuteCommand();
  304. }
  305. var dispatch = db.Queryable<SysMenu>().First(m => m.Id == workOrderDispatchMenuId);
  306. if (dispatch != null)
  307. {
  308. var needFix = dispatch.Pid != workOrderMgmtId
  309. || dispatch.Path != "/aidop/s1/workorder-mgmt/dispatch";
  310. if (needFix)
  311. {
  312. dispatch.Pid = workOrderMgmtId;
  313. dispatch.Path = "/aidop/s1/workorder-mgmt/dispatch";
  314. db.Updateable(dispatch)
  315. .UpdateColumns(m => new { m.Pid, m.Path })
  316. .ExecuteCommand();
  317. }
  318. }
  319. }
  320. /// <summary>
  321. /// S2「生产排程 / 作业计划 / 制造协同看板」升级为目录;「工单工序排产」父级为生产排程 1322000000006,path 为 S2 路由。
  322. /// </summary>
  323. private static void NormalizeS2ManufacturingParents(ISqlSugarClient db)
  324. {
  325. const long productionSchedulingId = 1322000000006L;
  326. const long operationPlanId = 1322000000007L;
  327. const long collaborationKanbanId = 1322000000008L;
  328. const long workOrderSchedulingMenuId = 1322000000108L;
  329. void EnsureDir(long id, string path, string name)
  330. {
  331. var m = db.Queryable<SysMenu>().First(x => x.Id == id);
  332. if (m == null)
  333. return;
  334. m.Type = MenuTypeEnum.Dir;
  335. m.Component = "Layout";
  336. m.Path = path;
  337. m.Name = name;
  338. m.Icon = "ele-Folder";
  339. db.Updateable(m)
  340. .UpdateColumns(x => new { x.Type, x.Component, x.Path, x.Name, x.Icon })
  341. .ExecuteCommand();
  342. }
  343. EnsureDir(productionSchedulingId, "/aidop/s2/production-scheduling", "aidopS2ProductionScheduling");
  344. EnsureDir(operationPlanId, "/aidop/s2/operation-plan", "aidopS2OperationPlan");
  345. EnsureDir(collaborationKanbanId, "/aidop/s2/collaboration-kanban", "aidopS2CollaborationKanban");
  346. var scheduling = db.Queryable<SysMenu>().First(m => m.Id == workOrderSchedulingMenuId);
  347. if (scheduling == null)
  348. return;
  349. const string s2SchedulingPath = "/aidop/s2/production-scheduling/work-order-scheduling";
  350. var needFix = scheduling.Pid != productionSchedulingId
  351. || scheduling.Path != s2SchedulingPath
  352. || scheduling.Name != "aidopS2WorkOrderScheduling";
  353. if (!needFix)
  354. return;
  355. scheduling.Pid = productionSchedulingId;
  356. scheduling.Path = s2SchedulingPath;
  357. scheduling.Name = "aidopS2WorkOrderScheduling";
  358. db.Updateable(scheduling)
  359. .UpdateColumns(m => new { m.Pid, m.Path, m.Name })
  360. .ExecuteCommand();
  361. }
  362. /// <summary>
  363. /// S2「排产异常记录」由占位页 <c>/aidop/planning/index</c> 改为真实列表页 <c>/aidop/production/scheduleExceptionList</c>。
  364. /// </summary>
  365. private static void NormalizeS2ScheduleExceptionMenu(ISqlSugarClient db)
  366. {
  367. const long menuId = 1329002100001L;
  368. const string component = "/aidop/production/scheduleExceptionList";
  369. var m = db.Queryable<SysMenu>().First(x => x.Id == menuId);
  370. if (m == null)
  371. return;
  372. if (m.Component == component)
  373. return;
  374. m.Component = component;
  375. m.Remark = "S2 排产异常记录";
  376. db.Updateable(m)
  377. .UpdateColumns(x => new { x.Component, x.Remark })
  378. .ExecuteCommand();
  379. }
  380. /// <summary>
  381. /// S2「作业计划」下三页由占位改为真实组件(库中已落旧 <c>/aidop/planning/index</c> 时纠偏)。
  382. /// </summary>
  383. private static void NormalizeS2OperationPlanLeafMenus(ISqlSugarClient db)
  384. {
  385. (long Id, string Component, string Title)[] map =
  386. {
  387. (1329002100011L, "/aidop/production/executableDailyPlanList", "可执行日计划"),
  388. (1329002100012L, "/aidop/production/shopCalendarWorkCtrList", "产线工作日历管理"),
  389. (1329002100013L, "/aidop/production/qualityLineRestDetailList", "产线休息时间管理"),
  390. (1329002100014L, "/aidop/production/holidayMasterList", "产线节假日管理"),
  391. (1329002100015L, "/aidop/production/lineOvertimeList", "产线加班管理"),
  392. };
  393. foreach (var (id, component, title) in map)
  394. {
  395. var m = db.Queryable<SysMenu>().First(x => x.Id == id);
  396. if (m == null)
  397. continue;
  398. if (m.Component == component && m.Title == title)
  399. continue;
  400. m.Component = component;
  401. m.Title = title;
  402. db.Updateable(m)
  403. .UpdateColumns(x => new { x.Component, x.Title })
  404. .ExecuteCommand();
  405. }
  406. }
  407. /// <summary>
  408. /// S2「工单执行进度看板」由占位 <c>/aidop/kanban/s2</c> 改为列表页 <c>/aidop/production/workOrderProgressDashboardList</c>。
  409. /// </summary>
  410. private static void NormalizeS2WorkOrderProgressKanbanMenu(ISqlSugarClient db)
  411. {
  412. const long menuId = 1329002100021L;
  413. const string component = "/aidop/production/workOrderProgressDashboardList";
  414. var m = db.Queryable<SysMenu>().First(x => x.Id == menuId);
  415. if (m == null)
  416. return;
  417. if (m.Component == component)
  418. return;
  419. m.Component = component;
  420. m.Remark = "S2 工单执行进度看板(列表)";
  421. db.Updateable(m)
  422. .UpdateColumns(x => new { x.Component, x.Remark })
  423. .ExecuteCommand();
  424. }
  425. }