using Admin.NET.Core; using Microsoft.Extensions.DependencyInjection; using SqlSugar; namespace Admin.NET.Plugin.AiDOP.Infrastructure; /// /// 将 Ai-DOP 种子菜单 Id 补写到 sys_tenant_menu / sys_role_menu。 /// 解决:SysTenantMenuSeedData 带 IgnoreUpdateSeed,库已存在时新增菜单不会自动进租户/角色;多租户下仅补默认租户会导致其它租户侧栏与菜单管理不可见。 /// public static class AidopMenuLinkSync { /// 与框架种子中首条「系统管理员」角色 Id 一致。 private const long SysAdminRoleId = 1300000000101L; private const long LegacyMaterialSubstitutionMenuId = 1329003000005L; private const long DeprecatedMaterialSubstitutionMenuId = 1329002000004L; private const long DeprecatedS8DashboardChildMenuId = 1329008000001L; private const long S8DirMenuId = 1321000009000L; public static void EnsureLinked(IServiceProvider services) { using var scope = services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); // Database.json 常关闭 EnableInitSeed/EnableIncreSeed,新增种子菜单不会入库;先补插 S2 三级结构再挂租户/角色。 EnsureS2ManufacturingCollaborationSeedMenus(db); NormalizeMaterialSubstitutionMenu(db); NormalizeS1OrderWorkOrderParents(db); NormalizeS2ManufacturingParents(db); NormalizeS2ScheduleExceptionMenu(db); NormalizeS2OperationPlanLeafMenus(db); NormalizeS2WorkOrderProgressKanbanMenu(db); EnsureLinkagePlanMenu(db); RemoveDeprecatedS8DashboardChildMenu(db); NormalizeS8MenuParents(db); var seedMenus = new global::Admin.NET.Plugin.AiDOP.SysMenuSeedData().HasData().ToList(); var seedMenuIds = seedMenus.Select(m => m.Id).ToHashSet(); var existingMenuIds = db.Queryable() .Where(m => seedMenuIds.Contains(m.Id)) .Select(m => m.Id) .ToList() .ToHashSet(); if (existingMenuIds.Count == 0) return; var tenantIds = db.Queryable().Select(t => t.Id).ToList(); if (tenantIds.Count == 0) return; var tenantMenuPairs = db.Queryable() .Where(tm => tenantIds.Contains(tm.TenantId) && seedMenuIds.Contains(tm.MenuId)) .Select(tm => new { tm.TenantId, tm.MenuId }) .ToList() .Select(x => (x.TenantId, x.MenuId)) .ToHashSet(); var tenantRows = new List(); foreach (var tid in tenantIds) { foreach (var menu in seedMenus) { if (!existingMenuIds.Contains(menu.Id)) continue; if (tenantMenuPairs.Contains((tid, menu.Id))) continue; tenantRows.Add(new SysTenantMenu { Id = CommonUtil.GetFixedHashCode("" + tid + menu.Id, 1300000000000), TenantId = tid, MenuId = menu.Id }); } } if (tenantRows.Count > 0) db.Insertable(tenantRows).ExecuteCommand(); // 已为任一 Ai-DOP 种子菜单授权的角色,补全新增子菜单;并始终包含默认租户系统管理员角色。 var roleIdsWithAnyAidop = db.Queryable() .Where(rm => seedMenuIds.Contains(rm.MenuId)) .Select(rm => rm.RoleId) .ToList() .Distinct() .ToHashSet(); roleIdsWithAnyAidop.Add(SysAdminRoleId); var roleMenuPairs = db.Queryable() .Where(rm => roleIdsWithAnyAidop.Contains(rm.RoleId) && seedMenuIds.Contains(rm.MenuId)) .Select(rm => new { rm.RoleId, rm.MenuId }) .ToList() .Select(x => (x.RoleId, x.MenuId)) .ToHashSet(); // SysRoleMenu 主键须全局唯一。使用 menu.Id + (roleId % 1300000000000) 时,不同 (MenuId, RoleId) 会算出相同 Id(如 1322000000208), // 批量插入失败,新菜单无法进入 sys_role_menu,侧栏不刷新。与 SysTenantMenu 一致,用固定哈希避免碰撞。 var roleRows = new List(); var pendingRoleMenuIds = new HashSet(); foreach (var roleId in roleIdsWithAnyAidop) { foreach (var menu in seedMenus) { if (!existingMenuIds.Contains(menu.Id)) continue; if (roleMenuPairs.Contains((roleId, menu.Id))) continue; var rmId = CommonUtil.GetFixedHashCode($"{roleId}:{menu.Id}", 1300000000000); while (!pendingRoleMenuIds.Add(rmId)) rmId++; roleRows.Add(new SysRoleMenu { Id = rmId, RoleId = roleId, MenuId = menu.Id }); } } if (roleRows.Count > 0) db.Insertable(roleRows).ExecuteCommand(); } /// /// S2「制造协同」二级目录与三级子菜单:Database.json 关闭种子时不会入库,导致侧栏无「工单工序排程」等项。 /// 从 取完整行一次性补插(含目录 Redirect)。 /// private static void EnsureS2ManufacturingCollaborationSeedMenus(ISqlSugarClient db) { const long aidopRootId = 1320990000101L; var fullSeed = new global::Admin.NET.Plugin.AiDOP.SysMenuSeedData().HasData().ToList(); var byId = fullSeed.GroupBy(m => m.Id).ToDictionary(g => g.Key, g => g.First()); // 父链:Ai-DOP 根 → S2 一级目录 → 三个二级目录 → 三级菜单(同批插入时父行尚未落库,用 cumulativeIds 判定) var orderedIds = new long[] { aidopRootId, 1321000003000L, 1322000000006L, 1322000000007L, 1322000000008L, 1322000000108L, 1329002100001L, 1329002100011L, 1329002100012L, 1329002100013L, 1329002100014L, 1329002100015L, 1329002100021L, }; var existing = db.Queryable().Where(m => orderedIds.Contains(m.Id)).Select(m => m.Id).ToList().ToHashSet(); var cumulativeIds = new HashSet(existing); var toInsert = new List(); foreach (var id in orderedIds) { if (cumulativeIds.Contains(id)) continue; if (!byId.TryGetValue(id, out var row)) continue; if (row.Pid != 0 && !cumulativeIds.Contains(row.Pid)) continue; toInsert.Add(row); cumulativeIds.Add(id); } if (toInsert.Count > 0) db.Insertable(toInsert).ExecuteCommand(); } /// /// 计划联动看板(Id=1322000000107):Database.json 常关闭 EnableIncreSeed,种子不会入库,故在启动时补插/纠偏。 /// 父级为产销协同看板目录 1322000000005。 /// private static void EnsureLinkagePlanMenu(ISqlSugarClient db) { const long menuId = 1322000000107L; const long salesKanBanDirId = 1322000000005L; if (!db.Queryable().Any(m => m.Id == salesKanBanDirId)) return; var ct = DateTime.Parse("2022-02-10 00:00:00"); if (!db.Queryable().Any(m => m.Id == menuId)) { db.Insertable(new SysMenu { Id = menuId, Pid = salesKanBanDirId, Title = "计划联动看板", Path = "/aidop/s1/SalesKanBan/linkage-plan", Name = "aidopS1LinkagePlan", Component = "/aidop/business/linkagePlanList", Icon = "ele-DataBoard", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 30, Status = StatusEnum.Enable, Remark = "S1 计划联动看板(LinkagePlan)" }).ExecuteCommand(); return; } var row = db.Queryable().First(m => m.Id == menuId); if (row.Pid != salesKanBanDirId || row.Path != "/aidop/s1/SalesKanBan/linkage-plan" || row.Component != "/aidop/business/linkagePlanList") { row.Pid = salesKanBanDirId; row.Path = "/aidop/s1/SalesKanBan/linkage-plan"; row.Name = "aidopS1LinkagePlan"; row.Component = "/aidop/business/linkagePlanList"; row.Title = "计划联动看板"; row.Icon = "ele-DataBoard"; row.OrderNo = 30; row.Type = MenuTypeEnum.Menu; db.Updateable(row) .UpdateColumns(m => new { m.Pid, m.Path, m.Name, m.Component, m.Title, m.Icon, m.OrderNo, m.Type }) .ExecuteCommand(); } } private static void NormalizeMaterialSubstitutionMenu(ISqlSugarClient db) { var deprecatedMenuId = DeprecatedMaterialSubstitutionMenuId; var legacyMenuId = LegacyMaterialSubstitutionMenuId; var legacyMenu = db.Queryable() .First(m => m.Id == legacyMenuId); if (legacyMenu != null) { legacyMenu.Title = "物料替代"; legacyMenu.Path = "/aidop/s0/manufacturing/material-substitution"; legacyMenu.Name = "aidopS0MfgMaterialSubstitution"; legacyMenu.Component = "/aidop/s0/manufacturing/MaterialSubstitutionList"; legacyMenu.Remark = "S0 物料替代"; db.Updateable(legacyMenu) .UpdateColumns(m => new { m.Title, m.Path, m.Name, m.Component, m.Remark }) .ExecuteCommand(); } var deprecatedExists = db.Queryable().Any(m => m.Id == deprecatedMenuId); if (!deprecatedExists) return; db.Deleteable().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand(); db.Deleteable().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand(); db.Deleteable().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand(); db.Deleteable().Where(x => x.Id == deprecatedMenuId).ExecuteCommand(); } private static void RemoveDeprecatedS8DashboardChildMenu(ISqlSugarClient db) { const long deprecatedMenuId = DeprecatedS8DashboardChildMenuId; var deprecatedExists = db.Queryable().Any(m => m.Id == deprecatedMenuId); if (!deprecatedExists) return; db.Deleteable().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand(); db.Deleteable().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand(); db.Deleteable().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand(); db.Deleteable().Where(x => x.Id == deprecatedMenuId).ExecuteCommand(); } private static void NormalizeS8MenuParents(ISqlSugarClient db) { var s8MenuIds = new long[] { 1329008000002L, 1329008000003L, 1329008000004L, 1329008000010L, 1329008000011L, 1329008000012L, 1329008000013L, 1329008000014L, 1329008000015L, 1329008000016L, 1329008000020L }; db.Updateable() .SetColumns(x => x.Pid == S8DirMenuId) .Where(x => s8MenuIds.Contains(x.Id) && x.Pid != S8DirMenuId) .ExecuteCommand(); } /// /// 将 S1「订单管理」「工单管理」升级为目录并修正 path/name; /// 「产销协同看板」单独菜单路径为 /aidop/s1/SalesKanBan; /// 「工单下达」父级须为工单管理目录 1322000000004(历史错误曾挂到 0005)。 /// 「工单工序排产」1322000000108 已迁至 S2「生产排程」下,见 。 /// private static void NormalizeS1OrderWorkOrderParents(ISqlSugarClient db) { const long orderMgmtId = 1322000000003L; const long workOrderMgmtId = 1322000000004L; const long salesKanBanMenuId = 1322000000005L; const long workOrderDispatchMenuId = 1322000000106L; var orderMgmt = db.Queryable().First(m => m.Id == orderMgmtId); if (orderMgmt != null) { orderMgmt.Type = MenuTypeEnum.Dir; orderMgmt.Component = "Layout"; orderMgmt.Path = "/aidop/s1/order-mgmt"; orderMgmt.Name = "aidopS1OrderMgmt"; orderMgmt.Icon = "ele-Folder"; db.Updateable(orderMgmt) .UpdateColumns(m => new { m.Type, m.Component, m.Path, m.Name, m.Icon }) .ExecuteCommand(); } var workOrderMgmt = db.Queryable().First(m => m.Id == workOrderMgmtId); if (workOrderMgmt != null) { workOrderMgmt.Type = MenuTypeEnum.Dir; workOrderMgmt.Component = "Layout"; workOrderMgmt.Path = "/aidop/s1/workorder-mgmt"; workOrderMgmt.Name = "aidopS1WorkOrderMgmt"; workOrderMgmt.Icon = "ele-Folder"; db.Updateable(workOrderMgmt) .UpdateColumns(m => new { m.Type, m.Component, m.Path, m.Name, m.Icon }) .ExecuteCommand(); } var salesKanBan = db.Queryable().First(m => m.Id == salesKanBanMenuId); if (salesKanBan != null) { salesKanBan.Type = MenuTypeEnum.Menu; salesKanBan.Component = "/aidop/kanban/s1"; salesKanBan.Path = "/aidop/s1/SalesKanBan"; salesKanBan.Name = "aidopS1003"; salesKanBan.Icon = "ele-DataAnalysis"; db.Updateable(salesKanBan) .UpdateColumns(m => new { m.Type, m.Component, m.Path, m.Name, m.Icon }) .ExecuteCommand(); } var dispatch = db.Queryable().First(m => m.Id == workOrderDispatchMenuId); if (dispatch != null) { var needFix = dispatch.Pid != workOrderMgmtId || dispatch.Path != "/aidop/s1/workorder-mgmt/dispatch"; if (needFix) { dispatch.Pid = workOrderMgmtId; dispatch.Path = "/aidop/s1/workorder-mgmt/dispatch"; db.Updateable(dispatch) .UpdateColumns(m => new { m.Pid, m.Path }) .ExecuteCommand(); } } } /// /// S2「生产排程 / 作业计划 / 制造协同看板」升级为目录;「工单工序排产」父级为生产排程 1322000000006,path 为 S2 路由。 /// private static void NormalizeS2ManufacturingParents(ISqlSugarClient db) { const long productionSchedulingId = 1322000000006L; const long operationPlanId = 1322000000007L; const long collaborationKanbanId = 1322000000008L; const long workOrderSchedulingMenuId = 1322000000108L; void EnsureDir(long id, string path, string name) { var m = db.Queryable().First(x => x.Id == id); if (m == null) return; m.Type = MenuTypeEnum.Dir; m.Component = "Layout"; m.Path = path; m.Name = name; m.Icon = "ele-Folder"; db.Updateable(m) .UpdateColumns(x => new { x.Type, x.Component, x.Path, x.Name, x.Icon }) .ExecuteCommand(); } EnsureDir(productionSchedulingId, "/aidop/s2/production-scheduling", "aidopS2ProductionScheduling"); EnsureDir(operationPlanId, "/aidop/s2/operation-plan", "aidopS2OperationPlan"); EnsureDir(collaborationKanbanId, "/aidop/s2/collaboration-kanban", "aidopS2CollaborationKanban"); var scheduling = db.Queryable().First(m => m.Id == workOrderSchedulingMenuId); if (scheduling == null) return; const string s2SchedulingPath = "/aidop/s2/production-scheduling/work-order-scheduling"; var needFix = scheduling.Pid != productionSchedulingId || scheduling.Path != s2SchedulingPath || scheduling.Name != "aidopS2WorkOrderScheduling"; if (!needFix) return; scheduling.Pid = productionSchedulingId; scheduling.Path = s2SchedulingPath; scheduling.Name = "aidopS2WorkOrderScheduling"; db.Updateable(scheduling) .UpdateColumns(m => new { m.Pid, m.Path, m.Name }) .ExecuteCommand(); } /// /// S2「排产异常记录」由占位页 /aidop/planning/index 改为真实列表页 /aidop/production/scheduleExceptionList。 /// private static void NormalizeS2ScheduleExceptionMenu(ISqlSugarClient db) { const long menuId = 1329002100001L; const string component = "/aidop/production/scheduleExceptionList"; var m = db.Queryable().First(x => x.Id == menuId); if (m == null) return; if (m.Component == component) return; m.Component = component; m.Remark = "S2 排产异常记录"; db.Updateable(m) .UpdateColumns(x => new { x.Component, x.Remark }) .ExecuteCommand(); } /// /// S2「作业计划」下三页由占位改为真实组件(库中已落旧 /aidop/planning/index 时纠偏)。 /// private static void NormalizeS2OperationPlanLeafMenus(ISqlSugarClient db) { (long Id, string Component, string Title)[] map = { (1329002100011L, "/aidop/production/executableDailyPlanList", "可执行日计划"), (1329002100012L, "/aidop/production/shopCalendarWorkCtrList", "产线工作日历管理"), (1329002100013L, "/aidop/production/qualityLineRestDetailList", "产线休息时间管理"), (1329002100014L, "/aidop/production/holidayMasterList", "产线节假日管理"), (1329002100015L, "/aidop/production/lineOvertimeList", "产线加班管理"), }; foreach (var (id, component, title) in map) { var m = db.Queryable().First(x => x.Id == id); if (m == null) continue; if (m.Component == component && m.Title == title) continue; m.Component = component; m.Title = title; db.Updateable(m) .UpdateColumns(x => new { x.Component, x.Title }) .ExecuteCommand(); } } /// /// S2「工单执行进度看板」由占位 /aidop/kanban/s2 改为列表页 /aidop/production/workOrderProgressDashboardList。 /// private static void NormalizeS2WorkOrderProgressKanbanMenu(ISqlSugarClient db) { const long menuId = 1329002100021L; const string component = "/aidop/production/workOrderProgressDashboardList"; var m = db.Queryable().First(x => x.Id == menuId); if (m == null) return; if (m.Component == component) return; m.Component = component; m.Remark = "S2 工单执行进度看板(列表)"; db.Updateable(m) .UpdateColumns(x => new { x.Component, x.Remark }) .ExecuteCommand(); } }