Selaa lähdekoodia

fix(s0): validate shelf and employee reference scope

YY968XX 5 kuukautta sitten
vanhempi
commit
92f1f209

+ 47 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0EmployeesController.cs

@@ -1,6 +1,7 @@
 using Admin.NET.Plugin.AiDOP.Dto.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
+using Microsoft.Extensions.Logging;
 
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
@@ -16,15 +17,18 @@ public class AdoS0EmployeesController : ControllerBase
     private readonly SqlSugarRepository<AdoS0EmployeeMaster> _rep;
     private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
     private readonly AdoS0ReferenceChecker _refChecker;
+    private readonly ILogger<AdoS0EmployeesController> _logger;
 
     public AdoS0EmployeesController(
         SqlSugarRepository<AdoS0EmployeeMaster> rep,
         SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
-        AdoS0ReferenceChecker refChecker)
+        AdoS0ReferenceChecker refChecker,
+        ILogger<AdoS0EmployeesController> logger)
     {
         _rep = rep;
         _deptRep = deptRep;
         _refChecker = refChecker;
+        _logger = logger;
     }
 
     [HttpGet]
@@ -68,9 +72,17 @@ public class AdoS0EmployeesController : ControllerBase
         if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.Employee == dto.Employee))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "员工编码已存在");
 
-        if (!await _refChecker.DepartmentExistsAsync(dto.Department))
-            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
-                $"部门编码 '{dto.Department}' 不存在于部门主数据");
+        // B2:Create 一律严格作用域校验,禁止降级(参 AdoS0MfgRoutingOpDetailsController)。
+        var deptResult = await _refChecker.DepartmentExistsInScopeAsync(dto.Department, dto.CompanyRefId, dto.FactoryRefId);
+        switch (deptResult)
+        {
+            case MaterialScopeCheck.NotFound:
+                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                    $"部门编码 '{dto.Department}' 不存在于部门主数据");
+            case MaterialScopeCheck.ScopeMiss:
+                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.InvalidReferenceScope,
+                    $"部门编码 '{dto.Department}' 不属于当前公司/工厂 (CompanyRefId={dto.CompanyRefId}, FactoryRefId={dto.FactoryRefId})");
+        }
 
         var entity = MapToEntity(dto);
         entity.CreateTime = DateTime.Now;
@@ -87,10 +99,40 @@ public class AdoS0EmployeesController : ControllerBase
         if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Employee == dto.Employee))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "员工编码已存在");
 
-        if (!await _refChecker.DepartmentExistsAsync(dto.Department))
+        // B2 + 历史兼容降级(D-03 严格条件,参 AdoS0MfgRoutingOpDetailsController)
+        var deptResult = await _refChecker.DepartmentExistsInScopeAsync(dto.Department, dto.CompanyRefId, dto.FactoryRefId);
+        if (deptResult == MaterialScopeCheck.NotFound)
             return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
                 $"部门编码 '{dto.Department}' 不存在于部门主数据");
 
+        if (deptResult == MaterialScopeCheck.ScopeMiss)
+        {
+            // 降级条件(全部满足才允许):
+            //   1. 是 Update(已满足)
+            //   2. DB 原记录 scope 为 0/0
+            //   3. 本次未修改 Department 引用值
+            var originScopeEmpty = entity.CompanyRefId == 0 && entity.FactoryRefId == 0;
+            var departmentUnchanged = string.Equals(entity.Department, dto.Department?.Trim(), StringComparison.Ordinal);
+
+            if (originScopeEmpty && departmentUnchanged)
+            {
+                _logger.LogWarning(
+                    "[B2 Downgrade] Table={TableName} PrimaryKey={PrimaryKey} ReferenceField={ReferenceField} " +
+                    "OldValue={OldValue} NewValue={NewValue} CompanyRefId={CompanyRefId} FactoryRefId={FactoryRefId} " +
+                    "DowngradeReason={DowngradeReason}",
+                    "EmployeeMaster", entity.Id, "Department",
+                    entity.Department, dto.Department,
+                    dto.CompanyRefId, dto.FactoryRefId,
+                    "LegacyRecord_OriginScopeZero_DepartmentUnchanged");
+                // 放行,继续执行写入
+            }
+            else
+            {
+                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.InvalidReferenceScope,
+                    $"部门编码 '{dto.Department}' 不属于当前公司/工厂 (CompanyRefId={dto.CompanyRefId}, FactoryRefId={dto.FactoryRefId})");
+            }
+        }
+
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;

+ 47 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LocationShelvesController.cs

@@ -1,6 +1,7 @@
 using Admin.NET.Plugin.AiDOP.Dto.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
+using Microsoft.Extensions.Logging;
 
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
@@ -16,15 +17,18 @@ public class AdoS0LocationShelvesController : ControllerBase
     private readonly SqlSugarRepository<AdoS0LocationShelfMaster> _rep;
     private readonly SqlSugarRepository<AdoS0LocationMaster> _locRep;
     private readonly AdoS0ReferenceChecker _refChecker;
+    private readonly ILogger<AdoS0LocationShelvesController> _logger;
 
     public AdoS0LocationShelvesController(
         SqlSugarRepository<AdoS0LocationShelfMaster> rep,
         SqlSugarRepository<AdoS0LocationMaster> locRep,
-        AdoS0ReferenceChecker refChecker)
+        AdoS0ReferenceChecker refChecker,
+        ILogger<AdoS0LocationShelvesController> logger)
     {
         _rep = rep;
         _locRep = locRep;
         _refChecker = refChecker;
+        _logger = logger;
     }
 
     [HttpGet]
@@ -68,9 +72,17 @@ public class AdoS0LocationShelvesController : ControllerBase
         if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.Location == dto.Location && x.InvShelf == dto.InvShelf))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "货架编码已存在");
 
-        if (!await _refChecker.LocationExistsAsync(dto.Location))
-            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
-                $"库位编码 '{dto.Location}' 不存在于库位主数据");
+        // B2:Create 一律严格作用域校验,禁止降级(参 AdoS0MfgRoutingOpDetailsController)。
+        var locResult = await _refChecker.LocationExistsInScopeAsync(dto.Location, dto.CompanyRefId, dto.FactoryRefId);
+        switch (locResult)
+        {
+            case MaterialScopeCheck.NotFound:
+                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                    $"库位编码 '{dto.Location}' 不存在于库位主数据");
+            case MaterialScopeCheck.ScopeMiss:
+                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.InvalidReferenceScope,
+                    $"库位编码 '{dto.Location}' 不属于当前公司/工厂 (CompanyRefId={dto.CompanyRefId}, FactoryRefId={dto.FactoryRefId})");
+        }
 
         var entity = new AdoS0LocationShelfMaster
         {
@@ -98,10 +110,40 @@ public class AdoS0LocationShelvesController : ControllerBase
         if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Location == dto.Location && x.InvShelf == dto.InvShelf))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "货架编码已存在");
 
-        if (!await _refChecker.LocationExistsAsync(dto.Location))
+        // B2 + 历史兼容降级(D-03 严格条件,参 AdoS0MfgRoutingOpDetailsController)
+        var locResult = await _refChecker.LocationExistsInScopeAsync(dto.Location, dto.CompanyRefId, dto.FactoryRefId);
+        if (locResult == MaterialScopeCheck.NotFound)
             return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
                 $"库位编码 '{dto.Location}' 不存在于库位主数据");
 
+        if (locResult == MaterialScopeCheck.ScopeMiss)
+        {
+            // 降级条件(全部满足才允许):
+            //   1. 是 Update(已满足)
+            //   2. DB 原记录 scope 为 0/0
+            //   3. 本次未修改 Location 引用值
+            var originScopeEmpty = entity.CompanyRefId == 0 && entity.FactoryRefId == 0;
+            var locationUnchanged = string.Equals(entity.Location, dto.Location?.Trim(), StringComparison.Ordinal);
+
+            if (originScopeEmpty && locationUnchanged)
+            {
+                _logger.LogWarning(
+                    "[B2 Downgrade] Table={TableName} PrimaryKey={PrimaryKey} ReferenceField={ReferenceField} " +
+                    "OldValue={OldValue} NewValue={NewValue} CompanyRefId={CompanyRefId} FactoryRefId={FactoryRefId} " +
+                    "DowngradeReason={DowngradeReason}",
+                    "LocationShelfMaster", entity.Id, "Location",
+                    entity.Location, dto.Location,
+                    dto.CompanyRefId, dto.FactoryRefId,
+                    "LegacyRecord_OriginScopeZero_LocationUnchanged");
+                // 放行,继续执行写入
+            }
+            else
+            {
+                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.InvalidReferenceScope,
+                    $"库位编码 '{dto.Location}' 不属于当前公司/工厂 (CompanyRefId={dto.CompanyRefId}, FactoryRefId={dto.FactoryRefId})");
+            }
+        }
+
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;

+ 46 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AdoS0ReferenceChecker.cs

@@ -107,6 +107,31 @@ public sealed class AdoS0ReferenceChecker
             .AnyAsync();
     }
 
+    /// <summary>
+    /// B2 保存端:库位(LocationMaster)存在 + 作用域一致性检查。
+    /// 语义与 <see cref="MaterialExistsInScopeAsync"/> 对齐:
+    ///   null/空 → Match(交给必填校验)
+    ///   主表无 location → NotFound
+    ///   存在且 Company/Factory 完全匹配 → Match(含双方同为 0/0 的历史)
+    ///   存在但 scope 不匹配 → ScopeMiss
+    /// </summary>
+    public async Task<MaterialScopeCheck> LocationExistsInScopeAsync(string? location, long companyRefId, long factoryRefId)
+    {
+        if (string.IsNullOrWhiteSpace(location)) return MaterialScopeCheck.Match;
+
+        var match = await _db.Queryable<AdoS0LocationMaster>()
+            .Where(x => x.Location == location)
+            .Select(x => new { x.CompanyRefId, x.FactoryRefId })
+            .ToListAsync();
+
+        if (match.Count == 0) return MaterialScopeCheck.NotFound;
+
+        if (match.Any(m => m.CompanyRefId == companyRefId && m.FactoryRefId == factoryRefId))
+            return MaterialScopeCheck.Match;
+
+        return MaterialScopeCheck.ScopeMiss;
+    }
+
     /// <summary>
     /// 保存端:部门(DepartmentMaster)存在性检查。
     /// 空值视为"未填"直接放行;作用域(DomainCode/Factory)一致性归 B2。
@@ -121,6 +146,27 @@ public sealed class AdoS0ReferenceChecker
             .AnyAsync();
     }
 
+    /// <summary>
+    /// B2 保存端:部门(DepartmentMaster)存在 + 作用域一致性检查。
+    /// 语义与 <see cref="MaterialExistsInScopeAsync"/> 对齐;只拦截新写入,不影响历史读取。
+    /// </summary>
+    public async Task<MaterialScopeCheck> DepartmentExistsInScopeAsync(string? department, long companyRefId, long factoryRefId)
+    {
+        if (string.IsNullOrWhiteSpace(department)) return MaterialScopeCheck.Match;
+
+        var match = await _db.Queryable<AdoS0DepartmentMaster>()
+            .Where(x => x.Department == department)
+            .Select(x => new { x.CompanyRefId, x.FactoryRefId })
+            .ToListAsync();
+
+        if (match.Count == 0) return MaterialScopeCheck.NotFound;
+
+        if (match.Any(m => m.CompanyRefId == companyRefId && m.FactoryRefId == factoryRefId))
+            return MaterialScopeCheck.Match;
+
+        return MaterialScopeCheck.ScopeMiss;
+    }
+
     /// <summary>
     /// 保存端:工作中心(WorkCtrMaster)存在性检查。
     /// 注意跨命名:业务键是 WorkCtrMaster.WorkCtr(非 WorkCenterCode)。