ソースを参照

fix(s0): align routing update material-change check with create contract

FUNC-S0-002 · 新建 Routing 行后更换物料一律 400

上一批把 Create 改为租户级物料存在性校验后,新建行带的是 SysOrg 雪花 scope;
Update 换物料时仍调 MaterialExistsInScopeAsync,要求 ItemMaster 的
company/factory 与本行组织归属数值全等,而两者分属无映射关系的两套 namespace
(雪花 vs legacy 1000/8010)→ 必判 ScopeMiss;D-03 降级又要求「原 scope 为 0/0
且物料未改」,对换物料结构上永不可达 → 新行换物料 100% 400。

改法:Update 按「物料值是否变化」分流,只改 materialChanged 这一支——
与 Create 同契约,租户内物料真实存在即可。租户隔离与物料真实性仍严格
(他租户物料 → 400 S01011)。物料值未变的分支(D-013 preserve + scope 校验
+ D-03 降级)逐字保留,未重构、未删除已冗余的判定。
共享的 AdoS0ReferenceChecker 未改动,只换本调用点,其它依赖严格 scope 的功能不受影响。

前端未改动。legacy↔SysOrg 映射治理(I-007)不在本轮范围。

chore: bump version server 1.0.464
YY968XX 1 ヶ月 前
親
コミット
23b246c71f

+ 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.463</AssemblyVersion>
-    <FileVersion>1.0.463</FileVersion>
-    <Version>1.0.463</Version>
+    <AssemblyVersion>1.0.464</AssemblyVersion>
+    <FileVersion>1.0.464</FileVersion>
+    <Version>1.0.464</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 61 - 35
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Manufacturing/AdoS0MfgRoutingOpDetailsController.cs

@@ -123,8 +123,7 @@ public class AdoS0MfgRoutingOpDetailsController : ControllerBase
         // 底层 checker 语义保持不变(不在 checker 里「查不到就放行」)。
         //
         // 各引用的 scope 依据(按 AdoS0ReferenceChecker 实现推导,非猜测):
-        //   MaterialCode   -> MaterialExistsInScopeAsync(tenantId, itemNum, CompanyRefId, FactoryRefId)
-        //                     故 scope 字段 = CompanyRefId + FactoryRefId
+        //   MaterialCode   -> 值未变时按 CompanyRefId + FactoryRefId 作用域(见下方 else 分支)
         //   WorkCenterCode -> WorkCenterExistsAsync(tenantId, workCenterCode)
         //                     只取 tenantId,UPDATE 中租户不可变 → 无额外 scope 字段
         //
@@ -134,8 +133,10 @@ public class AdoS0MfgRoutingOpDetailsController : ControllerBase
         static string? Norm(string? v) => string.IsNullOrWhiteSpace(v) ? null : v.Trim();
 
         var scopeUnchanged = entity.CompanyRefId == dto.CompanyRefId && entity.FactoryRefId == dto.FactoryRefId;
-        var materialPreserved = scopeUnchanged
-            && string.Equals(Norm(entity.MaterialCode), Norm(dto.MaterialCode), StringComparison.Ordinal);
+        // 「用户主动换了一个新物料」与「原值原样写回」是两种语义,必须分开判定。
+        var materialChanged =
+            !string.Equals(Norm(entity.MaterialCode), Norm(dto.MaterialCode), StringComparison.Ordinal);
+        var materialPreserved = scopeUnchanged && !materialChanged;
         var workCenterPreserved =
             string.Equals(Norm(entity.WorkCenterCode), Norm(dto.WorkCenterCode), StringComparison.Ordinal);
 
@@ -147,40 +148,65 @@ public class AdoS0MfgRoutingOpDetailsController : ControllerBase
                 entity.Id, entity.MaterialCode, entity.CompanyRefId, entity.FactoryRefId);
         }
 
-        // B1 + B2 + 历史兼容降级(D-03 严格条件)
-        var matResult = materialPreserved
-            ? MaterialScopeCheck.Match
-            : await _refChecker.MaterialExistsInScopeAsync(tenantId, dto.MaterialCode, dto.CompanyRefId, dto.FactoryRefId);
-        if (matResult == MaterialScopeCheck.NotFound)
-            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
-                $"物料编码 '{dto.MaterialCode}' 不存在于物料主数据");
-
-        if (matResult == MaterialScopeCheck.ScopeMiss)
+        if (materialChanged)
+        {
+            // 「选择一个新的真实物料」——与 Create 同一契约:**当前租户内物料真实存在即可**。
+            //
+            // 为什么不能继续用 MaterialExistsInScopeAsync:该方法要求 ItemMaster 的
+            // Company/FactoryRefId 与本行组织归属**数值完全相等**,但两者分属没有映射关系的
+            // 两套 namespace(本行 = SysOrg 雪花 id,ItemMaster 历史归属 = legacy 1000/8010),
+            // 跨 namespace 强比较恒不成立。上一批把 Create 改成租户级存在性后,新建行带的是
+            // SysOrg scope,再来换物料必判 ScopeMiss;而 D-03 降级要求「原 scope 为 0/0 且物料未改」,
+            // 对换物料这一动作**结构上永不可达** → 新行换物料 100% 400。
+            //
+            // 校验并未放宽:租户隔离与物料真实性依旧严格(他租户物料查不到 → 400 S01011),
+            // 移除的只是那条无语义基础的跨 namespace 数值比较。
+            // 共享的 AdoS0ReferenceChecker **未改动**,只是本调用点换用同类中已有的租户级方法;
+            // 其它仍依赖严格 scope 的功能不受影响。legacy↔SysOrg 映射治理(I-007)不在本轮范围。
+            if (!await _refChecker.MaterialExistsAsync(tenantId, dto.MaterialCode))
+                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                    $"物料编码 '{dto.MaterialCode}' 不存在于物料主数据");
+        }
+        else
         {
-            // 降级条件(全部满足才允许,对应 D-03 严格条件):
-            //   1. 是 Update(已满足)
-            //   2. DB 原记录 scope 为 0/null/0
-            //   3. 本次未修改 MaterialCode(dto 与 entity 相同)
-            //   4. B1 存在性通过(matResult != NotFound,已满足)
-            var originScopeEmpty = entity.CompanyRefId == 0 && entity.FactoryRefId == 0;
-            var materialUnchanged = string.Equals(entity.MaterialCode, dto.MaterialCode?.Trim(), StringComparison.Ordinal);
+            // 物料值未变(可能只改了组织归属或其它字段):历史兼容语义原样保留,本轮不动。
+            // B1 + B2 + 历史兼容降级(D-03 严格条件)
+            var matResult = materialPreserved
+                ? MaterialScopeCheck.Match
+                : await _refChecker.MaterialExistsInScopeAsync(tenantId, dto.MaterialCode, dto.CompanyRefId, dto.FactoryRefId);
+            if (matResult == MaterialScopeCheck.NotFound)
+                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                    $"物料编码 '{dto.MaterialCode}' 不存在于物料主数据");
 
-            if (originScopeEmpty && materialUnchanged)
-            {
-                _logger.LogWarning(
-                    "[B2 Downgrade] Table={TableName} PrimaryKey={PrimaryKey} ReferenceField={ReferenceField} " +
-                    "OldValue={OldValue} NewValue={NewValue} CompanyRefId={CompanyRefId} FactoryRefId={FactoryRefId} " +
-                    "DowngradeReason={DowngradeReason}",
-                    "RoutingOpDetail", entity.Id, "MaterialCode",
-                    entity.MaterialCode, dto.MaterialCode,
-                    dto.CompanyRefId, dto.FactoryRefId,
-                    "LegacyRecord_OriginScopeZero_MaterialUnchanged");
-                // 放行,继续执行写入
-            }
-            else
+            if (matResult == MaterialScopeCheck.ScopeMiss)
             {
-                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.InvalidReferenceScope,
-                    $"物料编码 '{dto.MaterialCode}' 不属于当前公司/工厂 (CompanyRefId={dto.CompanyRefId}, FactoryRefId={dto.FactoryRefId})");
+                // 降级条件(全部满足才允许,对应 D-03 严格条件):
+                //   1. 是 Update(已满足)
+                //   2. DB 原记录 scope 为 0/null/0
+                //   3. 本次未修改 MaterialCode(dto 与 entity 相同)
+                //   4. B1 存在性通过(matResult != NotFound,已满足)
+                // 注:进入本分支时条件 3 恒成立(外层已按 materialChanged 分流),
+                //     但**刻意保留该判定**,不趁本轮重构历史兼容代码。
+                var originScopeEmpty = entity.CompanyRefId == 0 && entity.FactoryRefId == 0;
+                var materialUnchanged = string.Equals(entity.MaterialCode, dto.MaterialCode?.Trim(), StringComparison.Ordinal);
+
+                if (originScopeEmpty && materialUnchanged)
+                {
+                    _logger.LogWarning(
+                        "[B2 Downgrade] Table={TableName} PrimaryKey={PrimaryKey} ReferenceField={ReferenceField} " +
+                        "OldValue={OldValue} NewValue={NewValue} CompanyRefId={CompanyRefId} FactoryRefId={FactoryRefId} " +
+                        "DowngradeReason={DowngradeReason}",
+                        "RoutingOpDetail", entity.Id, "MaterialCode",
+                        entity.MaterialCode, dto.MaterialCode,
+                        dto.CompanyRefId, dto.FactoryRefId,
+                        "LegacyRecord_OriginScopeZero_MaterialUnchanged");
+                    // 放行,继续执行写入
+                }
+                else
+                {
+                    return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.InvalidReferenceScope,
+                        $"物料编码 '{dto.MaterialCode}' 不属于当前公司/工厂 (CompanyRefId={dto.CompanyRefId}, FactoryRefId={dto.FactoryRefId})");
+                }
             }
         }