AdoS0ReferenceChecker.cs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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. /// B2 物料作用域校验结果。
  10. /// </summary>
  11. public enum MaterialScopeCheck
  12. {
  13. /// <summary>引用值存在且作用域一致。</summary>
  14. Match,
  15. /// <summary>引用值不存在于 ItemMaster(走 S01011)。</summary>
  16. NotFound,
  17. /// <summary>引用值存在但 Company/Factory 不匹配(走 S01012)。</summary>
  18. ScopeMiss
  19. }
  20. /// <summary>
  21. /// S0 删除前下游引用检查。
  22. /// 每个主数据主表在 DELETE 前调用对应方法,若存在下游引用则拒绝删除。
  23. /// 返回 null 表示无引用可安全删除;非空表示 (引用数, 下游表语义描述)。
  24. /// </summary>
  25. public sealed class AdoS0ReferenceChecker
  26. {
  27. private readonly ISqlSugarClient _db;
  28. private readonly ILogger<AdoS0ReferenceChecker> _logger;
  29. public AdoS0ReferenceChecker(ISqlSugarClient db, ILogger<AdoS0ReferenceChecker> logger)
  30. {
  31. _db = db;
  32. _logger = logger;
  33. }
  34. /// <summary>
  35. /// 容错包装:对已知 schema 不对齐表(如 srm_purchase)的查询失败时不阻断删除,只记日志。
  36. /// 独立问题由 BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH 单独修。
  37. /// </summary>
  38. private async Task<int> SafeCountAsync<T>(Func<ISugarQueryable<T>> queryFactory, string tag) where T : class, new()
  39. {
  40. try
  41. {
  42. return await queryFactory().CountAsync();
  43. }
  44. catch (Exception ex)
  45. {
  46. _logger.LogWarning(ex, "[AdoS0ReferenceChecker] skip {Tag} check due to schema mismatch", tag);
  47. return 0;
  48. }
  49. }
  50. /// <summary>
  51. /// 保存端:物料(ItemMaster)存在性检查。
  52. /// 仅校验"目标主表存在该 itemNum",不做 factoryRefId/domain 作用域校验(B2 归口)。
  53. /// 空值视为"未填"直接放行,由调用方先做必填校验。
  54. /// </summary>
  55. public async Task<bool> MaterialExistsAsync(string? itemNum)
  56. {
  57. if (string.IsNullOrWhiteSpace(itemNum)) return true;
  58. return await _db.Queryable<AdoS0ItemMaster>()
  59. .Where(x => x.ItemNum == itemNum)
  60. .AnyAsync();
  61. }
  62. /// <summary>
  63. /// B2 保存端:物料(ItemMaster)存在 + 作用域一致性检查。
  64. /// 返回:
  65. /// null — 引用值空,放行(交给必填校验)
  66. /// NotFound — 引用值不存在(应返 S01011)
  67. /// ScopeMiss — 引用值存在但 Company/Factory 不匹配(应返 S01012)
  68. /// Match — 存在且作用域一致,放行
  69. /// 0/0 边界:目标 ItemMaster 的 CompanyRefId=0 且 FactoryRefId=0 被视为"未 scope 化的历史数据",
  70. /// 仅当来源 Routing 同为 0/0 时才允许匹配;否则判 ScopeMiss(不做全局物料白名单)。
  71. /// </summary>
  72. public async Task<MaterialScopeCheck> MaterialExistsInScopeAsync(string? itemNum, long companyRefId, long factoryRefId)
  73. {
  74. if (string.IsNullOrWhiteSpace(itemNum)) return MaterialScopeCheck.Match;
  75. var match = await _db.Queryable<AdoS0ItemMaster>()
  76. .Where(x => x.ItemNum == itemNum)
  77. .Select(x => new { x.CompanyRefId, x.FactoryRefId })
  78. .ToListAsync();
  79. if (match.Count == 0) return MaterialScopeCheck.NotFound;
  80. // 存在 scope 完全一致的物料(含双方同为 0/0 的历史记录)
  81. if (match.Any(m => m.CompanyRefId == companyRefId && m.FactoryRefId == factoryRefId))
  82. return MaterialScopeCheck.Match;
  83. return MaterialScopeCheck.ScopeMiss;
  84. }
  85. /// <summary>
  86. /// 保存端:库位(LocationMaster)存在性检查。
  87. /// 用于货架等"从属于 LocationMaster 聚合"的子实体写入保护。
  88. /// 空值视为"未填"直接放行;作用域(Company/Factory/Domain)一致性归 B2。
  89. /// </summary>
  90. public async Task<bool> LocationExistsAsync(string? location)
  91. {
  92. if (string.IsNullOrWhiteSpace(location)) return true;
  93. return await _db.Queryable<AdoS0LocationMaster>()
  94. .Where(x => x.Location == location)
  95. .AnyAsync();
  96. }
  97. /// <summary>
  98. /// 保存端:部门(DepartmentMaster)存在性检查。
  99. /// 空值视为"未填"直接放行;作用域(DomainCode/Factory)一致性归 B2。
  100. /// 注意:历史数据中存在大量 Department 未对齐到 DepartmentMaster 的情况,
  101. /// 本方法只拦截新写入,不影响历史记录读取。
  102. /// </summary>
  103. public async Task<bool> DepartmentExistsAsync(string? department)
  104. {
  105. if (string.IsNullOrWhiteSpace(department)) return true;
  106. return await _db.Queryable<AdoS0DepartmentMaster>()
  107. .Where(x => x.Department == department)
  108. .AnyAsync();
  109. }
  110. /// <summary>
  111. /// 保存端:工作中心(WorkCtrMaster)存在性检查。
  112. /// 注意跨命名:业务键是 WorkCtrMaster.WorkCtr(非 WorkCenterCode)。
  113. /// 空值视为"未填"直接放行;作用域(Domain/Factory)一致性归 B2。
  114. /// </summary>
  115. public async Task<bool> WorkCenterExistsAsync(string? workCenterCode)
  116. {
  117. if (string.IsNullOrWhiteSpace(workCenterCode)) return true;
  118. return await _db.Queryable<AdoS0WorkCtrMaster>()
  119. .Where(x => x.WorkCtr == workCenterCode)
  120. .AnyAsync();
  121. }
  122. /// <summary>
  123. /// 物料(ItemMaster)删除前引用检查。
  124. /// 覆盖下游:工艺路线明细 / 物料替代关系 / 货源清单(SRM 采购)。
  125. /// </summary>
  126. public async Task<(int Count, string Table)?> MaterialReferencesAsync(string? itemNum)
  127. {
  128. if (string.IsNullOrWhiteSpace(itemNum)) return null;
  129. var routing = await _db.Queryable<AdoS0MfgRoutingOpDetail>()
  130. .Where(x => x.MaterialCode == itemNum)
  131. .CountAsync();
  132. if (routing > 0) return (routing, "工艺路线明细 (RoutingOpDetail)");
  133. var sub = await _db.Queryable<AdoS0ItemSubstituteDetail>()
  134. .Where(x => x.ItemNum == itemNum || x.SubstituteItem == itemNum)
  135. .CountAsync();
  136. if (sub > 0) return (sub, "物料替代关系 (ItemSubstituteDetail)");
  137. // 不查 SRM:srm_purchase.icitem_id 是上游系统(金蝶等)的物料 ID,
  138. // 与本地 AdoS0ItemMaster.Id 不在同一坐标系,无法直接 JOIN;
  139. // SRM 表也没有物料编码 (ItemNum) 字符串列。
  140. // 物料防线已由本地 RoutingOpDetail / ItemSubstituteDetail 覆盖。
  141. // 详见 BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH。
  142. return null;
  143. }
  144. /// <summary>
  145. /// 供应商(SuppMaster)删除前引用检查。
  146. /// 覆盖下游:货源清单(SRM 采购)。
  147. /// </summary>
  148. public async Task<(int Count, string Table)?> SupplierReferencesAsync(string? supp)
  149. {
  150. if (string.IsNullOrWhiteSpace(supp)) return null;
  151. // 只用 SupplierNumber(真实列)。原 OR 分支的 x.Supplier 是 [SugarColumn(IsIgnore = true)]
  152. // 展示字段,进 Where 会抛异常 → SafeCount 兜底 0,实际保护失效(BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH)。
  153. var srm = await SafeCountAsync(
  154. () => _db.Queryable<AdoS0SrmPurchase>().Where(x => x.SupplierNumber == supp),
  155. "SrmPurchase.SupplierNumber");
  156. if (srm > 0) return (srm, "货源清单 (SrmPurchase)");
  157. return null;
  158. }
  159. /// <summary>
  160. /// 库位(LocationMaster)删除前引用检查。
  161. /// 覆盖下游:货架 / 物料默认库位 / 产线 6 个 Location 变体。
  162. /// </summary>
  163. public async Task<(int Count, string Table)?> LocationReferencesAsync(string? location)
  164. {
  165. if (string.IsNullOrWhiteSpace(location)) return null;
  166. var shelf = await _db.Queryable<AdoS0LocationShelfMaster>()
  167. .Where(x => x.Location == location)
  168. .CountAsync();
  169. if (shelf > 0) return (shelf, "货架 (LocationShelfMaster)");
  170. var item = await _db.Queryable<AdoS0ItemMaster>()
  171. .Where(x => x.Location == location)
  172. .CountAsync();
  173. if (item > 0) return (item, "物料默认库位 (ItemMaster.Location)");
  174. // 补 A2 缺口:LineMaster 的 6 个 Location 变体均为 LocationMaster 的强软引用。
  175. // B1-9 已在保存端做 existence check,此处与之形成写/删对称。
  176. // Workshop 不纳入(B1-10 cancellation:无主表的自由文本)。
  177. var line = await _db.Queryable<AdoS0LineMaster>()
  178. .Where(x => x.Location == location
  179. || x.VLocation == location
  180. || x.Location2 == location
  181. || x.Location3 == location
  182. || x.PickingLocation == location
  183. || x.MidLocation == location)
  184. .CountAsync();
  185. if (line > 0) return (line, "产线 (LineMaster 的 Location/VLocation/Location2/Location3/PickingLocation/MidLocation)");
  186. return null;
  187. }
  188. /// <summary>
  189. /// 部门(DepartmentMaster)删除前引用检查。
  190. /// 覆盖下游:员工所属部门 / 工作中心所属部门。
  191. /// </summary>
  192. public async Task<(int Count, string Table)?> DepartmentReferencesAsync(string? department)
  193. {
  194. if (string.IsNullOrWhiteSpace(department)) return null;
  195. var emp = await _db.Queryable<AdoS0EmployeeMaster>()
  196. .Where(x => x.Department == department)
  197. .CountAsync();
  198. if (emp > 0) return (emp, "员工 (EmployeeMaster)");
  199. var wc = await _db.Queryable<AdoS0WorkCtrMaster>()
  200. .Where(x => x.Department == department)
  201. .CountAsync();
  202. if (wc > 0) return (wc, "工作中心 (WorkCtrMaster)");
  203. return null;
  204. }
  205. }