Pārlūkot izejas kodu

fix(s0): validate saved base-data references

YY968XX 3 mēneši atpakaļ
vecāks
revīzija
27fb56681f

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.91",
+	"version": "2.4.92",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 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.58</AssemblyVersion>
-    <FileVersion>1.0.58</FileVersion>
-    <Version>1.0.58</Version>
+    <AssemblyVersion>1.0.59</AssemblyVersion>
+    <FileVersion>1.0.59</FileVersion>
+    <Version>1.0.59</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 9 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Const/AdoS0ErrorCodes.cs

@@ -16,6 +16,15 @@ public static class AdoS0ErrorCodes
     public const string DetailRequired = "S01008";
     public const string DuplicateDetailItem = "S01009";
     public const string DataVersionConflict = "S01010";
+
+    /// <summary>
+    /// 保存端软引用校验失败:引用值在目标主表不存在。
+    /// 返回 400 BadRequest。区别于 <see cref="DeleteBlocked"/>(删除时被下游引用的 409)。
+    /// 备注:矩阵规约曾预设码值为 S01007,但 S01007 已被 InvalidStatusOperation 占用,
+    /// 实施时改用下一空位 S01011。
+    /// </summary>
+    public const string ReferenceNotFound = "S01011";
+
     public const string InternalServerError = "S01999";
 
     public const string CustomerCodeExists = "S02001";

+ 33 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Manufacturing/AdoS0MfgProductionLinesController.cs

@@ -15,10 +15,36 @@ namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Manufacturing;
 public class AdoS0MfgProductionLinesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0LineMaster> _rep;
+    private readonly AdoS0ReferenceChecker _refChecker;
 
-    public AdoS0MfgProductionLinesController(SqlSugarRepository<AdoS0LineMaster> rep)
+    public AdoS0MfgProductionLinesController(SqlSugarRepository<AdoS0LineMaster> rep, AdoS0ReferenceChecker refChecker)
     {
         _rep = rep;
+        _refChecker = refChecker;
+    }
+
+    /// <summary>
+    /// B1-9:LineMaster 6 个 Location 变体的统一 existence check。
+    /// 全部复用 AdoS0ReferenceChecker.LocationExistsAsync,空值放行。
+    /// </summary>
+    private async Task<IActionResult?> ValidateLocationReferencesAsync(AdoS0LineMasterUpsertDto dto)
+    {
+        var checks = new (string? Value, string Label)[]
+        {
+            (dto.Location, "主库位"),
+            (dto.VLocation, "虚拟库位"),
+            (dto.Location2, "辅助库位2"),
+            (dto.Location3, "辅助库位3"),
+            (dto.PickingLocation, "拣料库位"),
+            (dto.MidLocation, "中间库位"),
+        };
+        foreach (var (value, label) in checks)
+        {
+            if (!await _refChecker.LocationExistsAsync(value))
+                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                    $"{label}编码 '{value}' 不存在于库位主数据");
+        }
+        return null;
     }
 
     [HttpGet]
@@ -85,6 +111,9 @@ public class AdoS0MfgProductionLinesController : ControllerBase
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0LineMasterUpsertDto dto)
     {
+        var locErr = await ValidateLocationReferencesAsync(dto);
+        if (locErr != null) return locErr;
+
         var now = DateTime.Now;
         var entity = new AdoS0LineMaster
         {
@@ -115,6 +144,9 @@ public class AdoS0MfgProductionLinesController : ControllerBase
     [HttpPut("{id:long}")]
     public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0LineMasterUpsertDto dto)
     {
+        var locErr = await ValidateLocationReferencesAsync(dto);
+        if (locErr != null) return locErr;
+
         var entity = await _rep.GetByIdAsync(id);
         if (entity == null) return NotFound();
 

+ 11 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Manufacturing/AdoS0MfgWorkCentersController.cs

@@ -12,10 +12,12 @@ namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Manufacturing;
 public class AdoS0MfgWorkCentersController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0WorkCtrMaster> _rep;
+    private readonly AdoS0ReferenceChecker _refChecker;
 
-    public AdoS0MfgWorkCentersController(SqlSugarRepository<AdoS0WorkCtrMaster> rep)
+    public AdoS0MfgWorkCentersController(SqlSugarRepository<AdoS0WorkCtrMaster> rep, AdoS0ReferenceChecker refChecker)
     {
         _rep = rep;
+        _refChecker = refChecker;
     }
 
     [HttpGet]
@@ -69,6 +71,10 @@ public class AdoS0MfgWorkCentersController : ControllerBase
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0WorkCtrMasterUpsertDto dto)
     {
+        if (!await _refChecker.DepartmentExistsAsync(dto.Department))
+            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                $"部门编码 '{dto.Department}' 不存在于部门主数据");
+
         var now = DateTime.Now;
         var entity = new AdoS0WorkCtrMaster
         {
@@ -89,6 +95,10 @@ public class AdoS0MfgWorkCentersController : ControllerBase
     [HttpPut("{id:long}")]
     public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0WorkCtrMasterUpsertDto dto)
     {
+        if (!await _refChecker.DepartmentExistsAsync(dto.Department))
+            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                $"部门编码 '{dto.Department}' 不存在于部门主数据");
+
         var entity = await _rep.GetByIdAsync(id);
         if (entity == null) return NotFound();
         entity.WorkCtr = dto.WorkCtr.Trim();

+ 12 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0EmployeesController.cs

@@ -15,13 +15,16 @@ public class AdoS0EmployeesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0EmployeeMaster> _rep;
     private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
+    private readonly AdoS0ReferenceChecker _refChecker;
 
     public AdoS0EmployeesController(
         SqlSugarRepository<AdoS0EmployeeMaster> rep,
-        SqlSugarRepository<AdoS0DepartmentMaster> deptRep)
+        SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
+        AdoS0ReferenceChecker refChecker)
     {
         _rep = rep;
         _deptRep = deptRep;
+        _refChecker = refChecker;
     }
 
     [HttpGet]
@@ -65,6 +68,10 @@ 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}' 不存在于部门主数据");
+
         var entity = MapToEntity(dto);
         entity.CreateTime = DateTime.Now;
         await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
@@ -80,6 +87,10 @@ 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))
+            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                $"部门编码 '{dto.Department}' 不存在于部门主数据");
+
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;

+ 12 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LocationShelvesController.cs

@@ -15,13 +15,16 @@ public class AdoS0LocationShelvesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0LocationShelfMaster> _rep;
     private readonly SqlSugarRepository<AdoS0LocationMaster> _locRep;
+    private readonly AdoS0ReferenceChecker _refChecker;
 
     public AdoS0LocationShelvesController(
         SqlSugarRepository<AdoS0LocationShelfMaster> rep,
-        SqlSugarRepository<AdoS0LocationMaster> locRep)
+        SqlSugarRepository<AdoS0LocationMaster> locRep,
+        AdoS0ReferenceChecker refChecker)
     {
         _rep = rep;
         _locRep = locRep;
+        _refChecker = refChecker;
     }
 
     [HttpGet]
@@ -65,6 +68,10 @@ 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}' 不存在于库位主数据");
+
         var entity = new AdoS0LocationShelfMaster
         {
             CompanyRefId = dto.CompanyRefId,
@@ -91,6 +98,10 @@ 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))
+            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                $"库位编码 '{dto.Location}' 不存在于库位主数据");
+
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;

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

@@ -53,6 +53,33 @@ public sealed class AdoS0ReferenceChecker
             .AnyAsync();
     }
 
+    /// <summary>
+    /// 保存端:库位(LocationMaster)存在性检查。
+    /// 用于货架等"从属于 LocationMaster 聚合"的子实体写入保护。
+    /// 空值视为"未填"直接放行;作用域(Company/Factory/Domain)一致性归 B2。
+    /// </summary>
+    public async Task<bool> LocationExistsAsync(string? location)
+    {
+        if (string.IsNullOrWhiteSpace(location)) return true;
+        return await _db.Queryable<AdoS0LocationMaster>()
+            .Where(x => x.Location == location)
+            .AnyAsync();
+    }
+
+    /// <summary>
+    /// 保存端:部门(DepartmentMaster)存在性检查。
+    /// 空值视为"未填"直接放行;作用域(DomainCode/Factory)一致性归 B2。
+    /// 注意:历史数据中存在大量 Department 未对齐到 DepartmentMaster 的情况,
+    /// 本方法只拦截新写入,不影响历史记录读取。
+    /// </summary>
+    public async Task<bool> DepartmentExistsAsync(string? department)
+    {
+        if (string.IsNullOrWhiteSpace(department)) return true;
+        return await _db.Queryable<AdoS0DepartmentMaster>()
+            .Where(x => x.Department == department)
+            .AnyAsync();
+    }
+
     /// <summary>
     /// 保存端:工作中心(WorkCtrMaster)存在性检查。
     /// 注意跨命名:业务键是 WorkCtrMaster.WorkCtr(非 WorkCenterCode)。