AdoS0ReferenceChecker.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
  2. using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
  3. using Admin.NET.Plugin.AiDOP.Entity.S0.Supply;
  4. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  5. using Microsoft.Extensions.Logging;
  6. using SqlSugar;
  7. namespace Admin.NET.Plugin.AiDOP.Infrastructure;
  8. /// <summary>
  9. /// S0 删除前下游引用检查。
  10. /// 每个主数据主表在 DELETE 前调用对应方法,若存在下游引用则拒绝删除。
  11. /// 返回 null 表示无引用可安全删除;非空表示 (引用数, 下游表语义描述)。
  12. /// </summary>
  13. public sealed class AdoS0ReferenceChecker
  14. {
  15. private readonly ISqlSugarClient _db;
  16. private readonly ILogger<AdoS0ReferenceChecker> _logger;
  17. public AdoS0ReferenceChecker(ISqlSugarClient db, ILogger<AdoS0ReferenceChecker> logger)
  18. {
  19. _db = db;
  20. _logger = logger;
  21. }
  22. /// <summary>
  23. /// 容错包装:对已知 schema 不对齐表(如 srm_purchase)的查询失败时不阻断删除,只记日志。
  24. /// 独立问题由 BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH 单独修。
  25. /// </summary>
  26. private async Task<int> SafeCountAsync<T>(Func<ISugarQueryable<T>> queryFactory, string tag) where T : class, new()
  27. {
  28. try
  29. {
  30. return await queryFactory().CountAsync();
  31. }
  32. catch (Exception ex)
  33. {
  34. _logger.LogWarning(ex, "[AdoS0ReferenceChecker] skip {Tag} check due to schema mismatch", tag);
  35. return 0;
  36. }
  37. }
  38. /// <summary>
  39. /// 保存端:物料(ItemMaster)存在性检查。
  40. /// 仅校验"目标主表存在该 itemNum",不做 factoryRefId/domain 作用域校验(B2 归口)。
  41. /// 空值视为"未填"直接放行,由调用方先做必填校验。
  42. /// </summary>
  43. public async Task<bool> MaterialExistsAsync(string? itemNum)
  44. {
  45. if (string.IsNullOrWhiteSpace(itemNum)) return true;
  46. return await _db.Queryable<AdoS0ItemMaster>()
  47. .Where(x => x.ItemNum == itemNum)
  48. .AnyAsync();
  49. }
  50. /// <summary>
  51. /// 保存端:库位(LocationMaster)存在性检查。
  52. /// 用于货架等"从属于 LocationMaster 聚合"的子实体写入保护。
  53. /// 空值视为"未填"直接放行;作用域(Company/Factory/Domain)一致性归 B2。
  54. /// </summary>
  55. public async Task<bool> LocationExistsAsync(string? location)
  56. {
  57. if (string.IsNullOrWhiteSpace(location)) return true;
  58. return await _db.Queryable<AdoS0LocationMaster>()
  59. .Where(x => x.Location == location)
  60. .AnyAsync();
  61. }
  62. /// <summary>
  63. /// 保存端:部门(DepartmentMaster)存在性检查。
  64. /// 空值视为"未填"直接放行;作用域(DomainCode/Factory)一致性归 B2。
  65. /// 注意:历史数据中存在大量 Department 未对齐到 DepartmentMaster 的情况,
  66. /// 本方法只拦截新写入,不影响历史记录读取。
  67. /// </summary>
  68. public async Task<bool> DepartmentExistsAsync(string? department)
  69. {
  70. if (string.IsNullOrWhiteSpace(department)) return true;
  71. return await _db.Queryable<AdoS0DepartmentMaster>()
  72. .Where(x => x.Department == department)
  73. .AnyAsync();
  74. }
  75. /// <summary>
  76. /// 保存端:工作中心(WorkCtrMaster)存在性检查。
  77. /// 注意跨命名:业务键是 WorkCtrMaster.WorkCtr(非 WorkCenterCode)。
  78. /// 空值视为"未填"直接放行;作用域(Domain/Factory)一致性归 B2。
  79. /// </summary>
  80. public async Task<bool> WorkCenterExistsAsync(string? workCenterCode)
  81. {
  82. if (string.IsNullOrWhiteSpace(workCenterCode)) return true;
  83. return await _db.Queryable<AdoS0WorkCtrMaster>()
  84. .Where(x => x.WorkCtr == workCenterCode)
  85. .AnyAsync();
  86. }
  87. /// <summary>
  88. /// 物料(ItemMaster)删除前引用检查。
  89. /// 覆盖下游:工艺路线明细 / 物料替代关系 / 货源清单(SRM 采购)。
  90. /// </summary>
  91. public async Task<(int Count, string Table)?> MaterialReferencesAsync(string? itemNum)
  92. {
  93. if (string.IsNullOrWhiteSpace(itemNum)) return null;
  94. var routing = await _db.Queryable<AdoS0MfgRoutingOpDetail>()
  95. .Where(x => x.MaterialCode == itemNum)
  96. .CountAsync();
  97. if (routing > 0) return (routing, "工艺路线明细 (RoutingOpDetail)");
  98. var sub = await _db.Queryable<AdoS0ItemSubstituteDetail>()
  99. .Where(x => x.ItemNum == itemNum || x.SubstituteItem == itemNum)
  100. .CountAsync();
  101. if (sub > 0) return (sub, "物料替代关系 (ItemSubstituteDetail)");
  102. // 不查 SRM:srm_purchase.icitem_id 是上游系统(金蝶等)的物料 ID,
  103. // 与本地 AdoS0ItemMaster.Id 不在同一坐标系,无法直接 JOIN;
  104. // SRM 表也没有物料编码 (ItemNum) 字符串列。
  105. // 物料防线已由本地 RoutingOpDetail / ItemSubstituteDetail 覆盖。
  106. // 详见 BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH。
  107. return null;
  108. }
  109. /// <summary>
  110. /// 供应商(SuppMaster)删除前引用检查。
  111. /// 覆盖下游:货源清单(SRM 采购)。
  112. /// </summary>
  113. public async Task<(int Count, string Table)?> SupplierReferencesAsync(string? supp)
  114. {
  115. if (string.IsNullOrWhiteSpace(supp)) return null;
  116. // 只用 SupplierNumber(真实列)。原 OR 分支的 x.Supplier 是 [SugarColumn(IsIgnore = true)]
  117. // 展示字段,进 Where 会抛异常 → SafeCount 兜底 0,实际保护失效(BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH)。
  118. var srm = await SafeCountAsync(
  119. () => _db.Queryable<AdoS0SrmPurchase>().Where(x => x.SupplierNumber == supp),
  120. "SrmPurchase.SupplierNumber");
  121. if (srm > 0) return (srm, "货源清单 (SrmPurchase)");
  122. return null;
  123. }
  124. /// <summary>
  125. /// 库位(LocationMaster)删除前引用检查。
  126. /// 覆盖下游:货架 / 物料默认库位。
  127. /// </summary>
  128. public async Task<(int Count, string Table)?> LocationReferencesAsync(string? location)
  129. {
  130. if (string.IsNullOrWhiteSpace(location)) return null;
  131. var shelf = await _db.Queryable<AdoS0LocationShelfMaster>()
  132. .Where(x => x.Location == location)
  133. .CountAsync();
  134. if (shelf > 0) return (shelf, "货架 (LocationShelfMaster)");
  135. var item = await _db.Queryable<AdoS0ItemMaster>()
  136. .Where(x => x.Location == location)
  137. .CountAsync();
  138. if (item > 0) return (item, "物料默认库位 (ItemMaster.Location)");
  139. return null;
  140. }
  141. /// <summary>
  142. /// 部门(DepartmentMaster)删除前引用检查。
  143. /// 覆盖下游:员工所属部门 / 工作中心所属部门。
  144. /// </summary>
  145. public async Task<(int Count, string Table)?> DepartmentReferencesAsync(string? department)
  146. {
  147. if (string.IsNullOrWhiteSpace(department)) return null;
  148. var emp = await _db.Queryable<AdoS0EmployeeMaster>()
  149. .Where(x => x.Department == department)
  150. .CountAsync();
  151. if (emp > 0) return (emp, "员工 (EmployeeMaster)");
  152. var wc = await _db.Queryable<AdoS0WorkCtrMaster>()
  153. .Where(x => x.Department == department)
  154. .CountAsync();
  155. if (wc > 0) return (wc, "工作中心 (WorkCtrMaster)");
  156. return null;
  157. }
  158. }