Pārlūkot izejas kodu

fix(s0): protect contract review menu from link sync cleanup

Avoid deleting the contract review cycle menu when normalizing legacy material substitution links.
Only remove the deprecated material substitution menu when the existing menu identity matches legacy material substitution semantics.
Use a fresh menu read before semantic classification to avoid stale ORM state during startup.
YY968XX 1 mēnesi atpakaļ
vecāks
revīzija
84e036a49f

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.178</AssemblyVersion>
-    <FileVersion>1.0.178</FileVersion>
-    <Version>1.0.178</Version>
+    <AssemblyVersion>1.0.179</AssemblyVersion>
+    <FileVersion>1.0.179</FileVersion>
+    <Version>1.0.179</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 43 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AidopMenuLinkSync.cs

@@ -1,6 +1,7 @@
 using Admin.NET.Core;
 using Microsoft.Extensions.DependencyInjection;
 using SqlSugar;
+using System.Diagnostics;
 
 namespace Admin.NET.Plugin.AiDOP.Infrastructure;
 
@@ -367,16 +368,56 @@ public static class AidopMenuLinkSync
                 .ExecuteCommand();
         }
 
-        var deprecatedExists = db.Queryable<SysMenu>().Any(m => m.Id == deprecatedMenuId);
-        if (!deprecatedExists)
+        // Id 1329002000004 历史上是旧「物料替代」菜单,已迁移到 1329003000005;但该 Id 现被
+        // SysMenuSeedData「合同评审周期」(/aidop/s0/sales/contract-review-cycle) 合法复用。
+        // 旧逻辑仅凭 Id 无条件删除,会在每次启动抹掉合同评审周期菜单及其租户/角色授权。
+        // 改为语义判别:是合同评审周期则保护,明确是旧物料替代才清理,语义不明保守跳过。
+        // 用原生 SQL 读取该行,绕过 SqlSugar 查询缓存/全局过滤:直连 DB 写入的合同评审周期行
+        // 在 ORM 缓存路径下可能被读成迁移前的旧「物料替代」陈旧行,导致误判删除(启动期偶发)。
+        var deprecated = db.Ado
+            .SqlQuery<SysMenu>("SELECT * FROM SysMenu WHERE Id = @id", new SugarParameter("@id", deprecatedMenuId))
+            .FirstOrDefault();
+        if (deprecated == null)
+            return;
+
+        if (IsContractReviewCycleMenu(deprecated))
+        {
+            Trace.TraceInformation($"AidopMenuLinkSync: Id={deprecatedMenuId} 现为「合同评审周期」菜单,跳过旧物料替代清理(保护)。");
+            return;
+        }
+
+        if (!IsLegacyMaterialSubstitutionMenu(deprecated))
+        {
+            Trace.TraceWarning($"AidopMenuLinkSync: Id={deprecatedMenuId} 语义不明(Title={deprecated.Title}, Path={deprecated.Path}),保守跳过,不删除。");
             return;
+        }
 
+        // 仅当确认为旧「物料替代」菜单时,才执行历史清理。
         db.Deleteable<SysUserMenu>().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand();
         db.Deleteable<SysRoleMenu>().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand();
         db.Deleteable<SysTenantMenu>().Where(x => x.MenuId == deprecatedMenuId).ExecuteCommand();
         db.Deleteable<SysMenu>().Where(x => x.Id == deprecatedMenuId).ExecuteCommand();
     }
 
+    /// <summary>
+    /// 判断给定菜单是否为 S0「合同评审周期」菜单(Id 1329002000004 的当前合法占用者)。
+    /// </summary>
+    private static bool IsContractReviewCycleMenu(SysMenu m) =>
+        m.Title == "合同评审周期"
+        || m.Path == "/aidop/s0/sales/contract-review-cycle"
+        || m.Name == "aidopS0SalesContractReviewCycle"
+        || m.Component == "/aidop/s0/sales/ContractReviewCycleList";
+
+    /// <summary>
+    /// 判断给定菜单是否为旧「物料替代」菜单(迁移前曾占用 Id 1329002000004)。
+    /// 依据 Phase 0 已举证的旧字段语义:Title 含「物料替代」,或 Path/Name/Component 含 material-substitution / MaterialSubstitution。
+    /// </summary>
+    private static bool IsLegacyMaterialSubstitutionMenu(SysMenu m) =>
+        (m.Title?.Contains("物料替代") ?? false)
+        || (m.Path?.Contains("material-substitution") ?? false)
+        || (m.Name?.Contains("MaterialSubstitution") ?? false)
+        || (m.Component?.Contains("MaterialSubstitution") ?? false);
+
     private static void RemoveDeprecatedS8DashboardChildMenu(ISqlSugarClient db)
     {
         const long deprecatedMenuId = DeprecatedS8DashboardChildMenuId;