AdoS0QmsSpecAggregateGuard.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. using Admin.NET.Plugin.AiDOP.Dto.S0.Quality;
  2. namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Quality;
  3. /// <summary>
  4. /// 检验规范聚合(主单 + 检验明细)的必填契约守卫。
  5. ///
  6. /// 契约来源:UAT-S0-10 拍板(2026-09-02)。此前四张检规表在前端表单、后端 DTO、DB 列三层
  7. /// 都只约束「文件编号」,导致只填文件编号即可落库空壳检规。本守卫是该契约的唯一实现处,
  8. /// Create / Import 两条写入入口共用,避免再次出现「导入宽于表单」。
  9. ///
  10. /// 两条入口的规则不同,刻意分成两个方法:
  11. /// · 新增(Create / Import)——必填字段必须全部有值,且至少 1 条有内容的检验明细;
  12. /// · 编辑(Update)——「历史兼容、禁止劣化」:历史记录原本就缺的字段允许继续为空,
  13. /// 但原本有值的必填字段不允许被清空;原本 0 明细允许保持 0,原本有明细不允许删空。
  14. ///
  15. /// 【为什么四张检规表的 DTO 上一个 [Required] 都不留】
  16. /// Create 与 Update 共用同一个 UpsertDto。任何 [Required] 都会在 MVC 模型绑定阶段先于本守卫触发,
  17. /// 把 Update 直接挡成 400,从而绕过并破坏 ValidateUpdate(existing, incoming) 的历史兼容语义
  18. /// ——实测 qms_gcjygf 有 3 行 wjbh 为空,正是被遗留的 FileNumber[Required] 锁死不可编辑。
  19. /// 因此「文件编号」的新增必填也统一迁到本守卫,特性层不再承担任何必填职责。
  20. ///
  21. /// 【明细归一化】全空明细是 UI 占位行 / Excel 空行,不是业务明细,一律不落库。
  22. /// Create / Update / Import 三条入口都必须先经 Meaningful(...) 过滤,再用过滤结果做数量校验与写入,
  23. /// 保证「校验看到的」与「写进库的」是同一份数据。
  24. /// </summary>
  25. public static class AdoS0QmsSpecAggregateGuard
  26. {
  27. /// <summary>新增校验。返回 null 表示通过,否则为面向用户的错误文案。</summary>
  28. public static string? ValidateCreate(IReadOnlyList<(string Label, string? Value)> requiredFields, int meaningfulItemCount)
  29. {
  30. foreach (var (label, value) in requiredFields)
  31. {
  32. if (string.IsNullOrWhiteSpace(value)) return $"{label}不能为空";
  33. }
  34. return meaningfulItemCount > 0 ? null : "检验明细至少需要 1 条";
  35. }
  36. /// <summary>编辑校验(历史兼容、禁止劣化)。返回 null 表示通过,否则为面向用户的错误文案。</summary>
  37. public static string? ValidateUpdate(
  38. IReadOnlyList<(string Label, string? Existing, string? Incoming)> requiredFields,
  39. int existingItemCount,
  40. int incomingMeaningfulItemCount)
  41. {
  42. foreach (var (label, existing, incoming) in requiredFields)
  43. {
  44. // 原本为空 → 放行(不强制本次补齐);原本有值 → 不允许清空。
  45. if (!string.IsNullOrWhiteSpace(existing) && string.IsNullOrWhiteSpace(incoming))
  46. return $"{label}原本已有值,不允许清空";
  47. }
  48. if (existingItemCount > 0 && incomingMeaningfulItemCount == 0)
  49. return $"该检规原有 {existingItemCount} 条检验明细,不允许全部删除";
  50. return null;
  51. }
  52. // ==================== 明细归一化:过滤全空占位行 ====================
  53. // 唯一入口。控制器一律用 Meaningful(...) 的返回值同时做数量校验与写库,
  54. // 不得再各自 Where/Count 一遍,避免「校验一套、写入另一套」。
  55. public static List<AdoS0QmsRawInspectionSpecEntryDto> Meaningful(IEnumerable<AdoS0QmsRawInspectionSpecEntryDto> items)
  56. => items.Where(HasValue).ToList();
  57. public static List<AdoS0QmsProcessInspectionSpecEntryDto> Meaningful(IEnumerable<AdoS0QmsProcessInspectionSpecEntryDto> items)
  58. => items.Where(HasValue).ToList();
  59. public static List<AdoS0QmsFqcInspectionSpecEntryDto> Meaningful(IEnumerable<AdoS0QmsFqcInspectionSpecEntryDto> items)
  60. => items.Where(HasValue).ToList();
  61. public static List<AdoS0QmsOqcInspectionSpecEntryDto> Meaningful(IEnumerable<AdoS0QmsOqcInspectionSpecEntryDto> items)
  62. => items.Where(HasValue).ToList();
  63. // ==================== 明细「有内容」判据 ====================
  64. // 整行全空的明细不计数、不落库,否则前端 openCreate 自动补的那一行空明细
  65. // 会让「≥1 条」形同虚设,并持续制造新的空明细脏数据。
  66. //
  67. // 导入行(ImportRow)的重载与实体侧同语义,只是模板列少了图像分类/备注/附件等非模板列——
  68. // 判据仍集中在本类,导入侧不再自带一份定义。
  69. public static bool HasDetailValue(AdoS0QmsRawInspectionSpecImportRow r)
  70. => r.Seq.HasValue
  71. || AnyNotBlank(r.InspectionItem, r.InspectionStandard, r.InspectionMethod,
  72. r.SamplingScheme, r.UpperLimit, r.LowerLimit);
  73. public static bool HasDetailValue(AdoS0QmsProcessLikeInspectionSpecImportRow r)
  74. => AnyNotBlank(r.OperationCode, r.OperationName, r.InspectionItem, r.InspectionMethod,
  75. r.InspectionSpec, r.InspectionFrequency, r.UpperLimit, r.LowerLimit);
  76. public static bool HasValue(AdoS0QmsRawInspectionSpecEntryDto e)
  77. => e.Seq.HasValue
  78. || AnyNotBlank(e.InspectionItem, e.InspectionStandard, e.InspectionMethod, e.ImageCategory,
  79. e.SamplingScheme, e.Remark, e.Attachment, e.UpperLimit, e.LowerLimit);
  80. public static bool HasValue(AdoS0QmsProcessInspectionSpecEntryDto e)
  81. => e.PeelingForce.HasValue
  82. || AnyNotBlank(e.OperationCode, e.OperationName, e.InspectionItem, e.InspectionMethod, e.InspectionSpec,
  83. e.ImageCategory, e.InspectionFrequency, e.TechnicalStandard, e.UpperLimit, e.LowerLimit);
  84. public static bool HasValue(AdoS0QmsFqcInspectionSpecEntryDto e)
  85. => e.PeelingForce.HasValue
  86. || AnyNotBlank(e.OperationCode, e.OperationName, e.InspectionItem, e.InspectionMethod, e.InspectionSpec,
  87. e.ImageCategory, e.InspectionFrequency, e.TechnicalStandard, e.UpperLimit, e.LowerLimit);
  88. public static bool HasValue(AdoS0QmsOqcInspectionSpecEntryDto e)
  89. => e.PeelingForce.HasValue
  90. || AnyNotBlank(e.OperationCode, e.OperationName, e.InspectionItem, e.InspectionMethod, e.InspectionSpec,
  91. e.ImageCategory, e.InspectionFrequency, e.TechnicalStandard, e.UpperLimit, e.LowerLimit);
  92. private static bool AnyNotBlank(params string?[] values)
  93. {
  94. foreach (var v in values)
  95. {
  96. if (!string.IsNullOrWhiteSpace(v)) return true;
  97. }
  98. return false;
  99. }
  100. }