-- =========================================================================== -- UAT 租户占位菜单收口(S0 / S5 / S6 / S7) -- -- 背景:一批菜单已在 SysMenu 全局登记,但 Component 仍指向通用占位页 -- `/aidop/planning/index`(AidopDemoShell + el-empty「后续在此接入 Ai-DOP -- 业务页面」),对应前端业务目录与组件均不存在,属「纯菜单占位、零实现」。 -- UAT 租户(SysOrg.Code = 'UATTEST_CHL')的测试账号能正常点进这些空壳页。 -- -- 目标:只让该 UAT 租户看不到这些占位功能。 -- - 不删除、不禁用、不隐藏任何 SysMenu 全局记录 -- - 不影响其余 9 个租户 -- - 不改前端,不硬编码 tenantId -- -- 机制:菜单可见性 = SysMenu(Type≠Btn ∧ Status=Enable) -- ∩ SysTenantMenu[登录租户] -- ∩ SysRoleMenu[用户角色集](含祖先补全) -- 其中 SysTenantMenu 是唯一的「租户维度」硬白名单 -- (SysMenuService.GetSugarQueryableAndTenantId → InnerJoinIF), -- 对该租户内所有账号类型生效(含 SysAdmin 888),且会同时收窄 -- 「该租户内新建角色时的可选菜单范围」 -- (SysRoleMenuService.GrantRoleMenu 只落 SysTenantMenu 允许集合的交集)。 -- 因此从 SysTenantMenu 移除,正是「租户级隐藏」的语义。 -- -- ⚠️ 为什么还要配一张抑制表: -- AidopMenuLinkSync.EnsureLinked(Plugin.AiDOP/Startup.cs:79,每次进程启动 -- 无条件执行)会把 Ai-DOP 种子菜单回填到**全部租户**的 SysTenantMenu,判据 -- 只有「这行还在吗」,且主键是 (tenantId, menuId) 的确定性哈希 —— 单纯 DELETE -- 会在下一次 restart_aidop.sh 后以同一主键原样复活。ado_tenant_menu_suppression -- 给回填逻辑一个「这是有意取消的」显式信号,并由启动逻辑强制执行,重启自愈。 -- -- 范围:12 个占位叶子(S0×1 + S5×4 + S6×5 + S7×2) -- + 4 个「子项全为占位」的父目录 = 16 对 (TenantId, MenuId)。 -- S0 的父节点「S0 运营建模」下另有「数据建模」真实子树(58 个已实现页面), -- 故父节点**不摘**。 -- S8 经审计**无**任何 planning 占位菜单,本脚本不涉及 S8。 -- S9 / S4 / M11-M16 的占位菜单**本轮明确不处理**。 -- -- 幂等:CREATE TABLE IF NOT EXISTS / INSERT ... WHERE NOT EXISTS / DELETE, -- 可重复执行,重跑影响行数为 0。 -- 校验:1.0.424.verify.sql(AutoVersionUpdate 自动逐条真值断言) -- 对账/回滚:UAT-PLACEHOLDER-MENU-HIDE.ops.sql(人工执行,不会被自动运行) -- =========================================================================== -- 租户由稳定业务键解析,不写死数字 Id(与 1.0.420.sql 同口径) SET @t_uat := (SELECT t.Id FROM SysTenant t JOIN SysOrg o ON o.Id = t.OrgId WHERE o.Code = 'UATTEST_CHL' AND t.Status = 1 LIMIT 1); -- --------------------------------------------------------------------------- -- 00 · 抑制清单表 -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS `ado_tenant_menu_suppression` ( `Id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `TenantId` bigint NOT NULL COMMENT '租户 Id(SysTenant.Id)', `MenuId` bigint NOT NULL COMMENT '菜单 Id(SysMenu.Id)', `Reason` varchar(256) NULL COMMENT '抑制原因(为何该租户不应看到该菜单)', `CreatedAt` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`Id`), UNIQUE KEY `uk_tenant_menu_suppression` (`TenantId`, `MenuId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户级菜单抑制清单'; -- --------------------------------------------------------------------------- -- 01 · 占位叶子(15 条) -- 双重业务键:Path 命中 + Component 必须仍是占位页。 -- 若某条日后被换成真实组件,这里自然不再命中,不会误抑制真实功能。 -- --------------------------------------------------------------------------- INSERT INTO ado_tenant_menu_suppression (TenantId, MenuId, Reason, CreatedAt) SELECT @t_uat, m.Id, CONCAT('UAT 收口:未实现占位页 ', m.Path), NOW() FROM SysMenu m WHERE @t_uat IS NOT NULL AND m.Component = '/aidop/planning/index' AND m.Path IN ( -- S0 业务建模(FUNC-S0-065) -- 该条 SysMenu.Status 已为 2/停用、当前本就不渲染;纳入抑制是为了口径一致 + -- 防御「日后有人把 Status 改回 1 时它在 UAT 租户重新冒出来」。 '/aidop/s0/002', -- S5 物料仓储看板(FUNC-S5-014 ~ 017) '/aidop/s5/warehouse-kanban/receiving-task', '/aidop/s5/warehouse-kanban/issuing-task', '/aidop/s5/warehouse-kanban/turnover-report', '/aidop/s5/warehouse-kanban/age-analysis', -- S6 生产记录管理(FUNC-S6-001) '/aidop/s6/production-record/order-list', -- S6 生产执行看板(FUNC-S6-005 ~ 008) '/aidop/s6/execution-kanban/work-order', '/aidop/s6/execution-kanban/workshop-efficiency', '/aidop/s6/execution-kanban/equipment-oee', '/aidop/s6/execution-kanban/labor-hours', -- S7 成品仓储看板(FUNC-S7-005 / 006) '/aidop/s7/finished-warehouse-kanban/shipment-task', '/aidop/s7/finished-warehouse-kanban/sluggish' ) AND NOT EXISTS (SELECT 1 FROM ado_tenant_menu_suppression s WHERE s.TenantId = @t_uat AND s.MenuId = m.Id); -- --------------------------------------------------------------------------- -- 02 · 纯占位父目录(5 条) -- 菜单树不会自动裁掉空父节点:后端 ToTree 无 children.Any() 判断, -- ExpandMenuIdsWithAncestorsAsync 只正向补祖先;前端 vertical.vue 对 -- children.length===0 的目录走 v-else 渲染成**可点击的死链 el-menu-item**。 -- 所以父目录必须显式一并移除。 -- -- 安全断言:只在「该目录下所有非按钮子菜单都是占位」时才登记。 -- 任何一个真实子菜单存在 → 整条不命中 → 绝不会摘掉还有真实功能的目录。 -- --------------------------------------------------------------------------- INSERT INTO ado_tenant_menu_suppression (TenantId, MenuId, Reason, CreatedAt) SELECT @t_uat, m.Id, CONCAT('UAT 收口:子项全为占位,避免留下空壳目录 ', m.Path), NOW() FROM SysMenu m WHERE @t_uat IS NOT NULL AND m.Path IN ( '/aidop/s5/warehouse-kanban', -- 物料仓储看板 '/aidop/s6/production-record', -- 生产记录管理 '/aidop/s6/execution-kanban', -- 生产执行看板 '/aidop/s7/finished-warehouse-kanban' -- 成品仓储看板 ) -- 必须确有子菜单(防止把一个孤立节点当目录摘掉) AND EXISTS (SELECT 1 FROM SysMenu c WHERE c.Pid = m.Id AND c.Type <> 3) -- 且非按钮子菜单必须 100% 是占位页 AND NOT EXISTS ( SELECT 1 FROM SysMenu c WHERE c.Pid = m.Id AND c.Type <> 3 AND (c.Component IS NULL OR c.Component <> '/aidop/planning/index') ) AND NOT EXISTS (SELECT 1 FROM ado_tenant_menu_suppression s WHERE s.TenantId = @t_uat AND s.MenuId = m.Id); -- --------------------------------------------------------------------------- -- 03 · 按抑制清单移除该租户的菜单授权 -- 只删 SysTenantMenu,且只删 @t_uat 这一个租户的行。 -- **不动 SysRoleMenu** —— 角色授权保留,撤销抑制后可精确还原原可见性。 -- **不动 SysMenu** —— 全局菜单定义零改动。 -- --------------------------------------------------------------------------- DELETE tm FROM SysTenantMenu tm JOIN ado_tenant_menu_suppression s ON s.TenantId = tm.TenantId AND s.MenuId = tm.MenuId WHERE tm.TenantId = @t_uat;