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();
}
}