| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
- using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
- using Admin.NET.Plugin.AiDOP.Entity.S0.Supply;
- using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
- using Microsoft.Extensions.Logging;
- using SqlSugar;
- namespace Admin.NET.Plugin.AiDOP.Infrastructure;
- /// <summary>
- /// B2 物料作用域校验结果。
- /// </summary>
- public enum MaterialScopeCheck
- {
- /// <summary>引用值存在且作用域一致。</summary>
- Match,
- /// <summary>引用值不存在于 ItemMaster(走 S01011)。</summary>
- NotFound,
- /// <summary>引用值存在但 Company/Factory 不匹配(走 S01012)。</summary>
- ScopeMiss
- }
- /// <summary>
- /// S0 删除前下游引用检查。
- /// 每个主数据主表在 DELETE 前调用对应方法,若存在下游引用则拒绝删除。
- /// 返回 null 表示无引用可安全删除;非空表示 (引用数, 下游表语义描述)。
- /// </summary>
- public sealed class AdoS0ReferenceChecker
- {
- private readonly ISqlSugarClient _db;
- private readonly ILogger<AdoS0ReferenceChecker> _logger;
- public AdoS0ReferenceChecker(ISqlSugarClient db, ILogger<AdoS0ReferenceChecker> logger)
- {
- _db = db;
- _logger = logger;
- }
- /// <summary>
- /// 容错包装:对已知 schema 不对齐表(如 srm_purchase)的查询失败时不阻断删除,只记日志。
- /// 独立问题由 BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH 单独修。
- /// </summary>
- private async Task<int> SafeCountAsync<T>(Func<ISugarQueryable<T>> queryFactory, string tag) where T : class, new()
- {
- try
- {
- return await queryFactory().CountAsync();
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "[AdoS0ReferenceChecker] skip {Tag} check due to schema mismatch", tag);
- return 0;
- }
- }
- /// <summary>
- /// 保存端:物料(ItemMaster)存在性检查。
- /// 仅校验"目标主表存在该 itemNum",不做 factoryRefId/domain 作用域校验(B2 归口)。
- /// 空值视为"未填"直接放行,由调用方先做必填校验。
- /// </summary>
- public async Task<bool> MaterialExistsAsync(string? itemNum)
- {
- if (string.IsNullOrWhiteSpace(itemNum)) return true;
- return await _db.Queryable<AdoS0ItemMaster>()
- .Where(x => x.ItemNum == itemNum)
- .AnyAsync();
- }
- /// <summary>
- /// B2 保存端:物料(ItemMaster)存在 + 作用域一致性检查。
- /// 返回:
- /// null — 引用值空,放行(交给必填校验)
- /// NotFound — 引用值不存在(应返 S01011)
- /// ScopeMiss — 引用值存在但 Company/Factory 不匹配(应返 S01012)
- /// Match — 存在且作用域一致,放行
- /// 0/0 边界:目标 ItemMaster 的 CompanyRefId=0 且 FactoryRefId=0 被视为"未 scope 化的历史数据",
- /// 仅当来源 Routing 同为 0/0 时才允许匹配;否则判 ScopeMiss(不做全局物料白名单)。
- /// </summary>
- public async Task<MaterialScopeCheck> MaterialExistsInScopeAsync(string? itemNum, long companyRefId, long factoryRefId)
- {
- if (string.IsNullOrWhiteSpace(itemNum)) return MaterialScopeCheck.Match;
- var match = await _db.Queryable<AdoS0ItemMaster>()
- .Where(x => x.ItemNum == itemNum)
- .Select(x => new { x.CompanyRefId, x.FactoryRefId })
- .ToListAsync();
- if (match.Count == 0) return MaterialScopeCheck.NotFound;
- // 存在 scope 完全一致的物料(含双方同为 0/0 的历史记录)
- if (match.Any(m => m.CompanyRefId == companyRefId && m.FactoryRefId == factoryRefId))
- return MaterialScopeCheck.Match;
- return MaterialScopeCheck.ScopeMiss;
- }
- /// <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)。
- /// 空值视为"未填"直接放行;作用域(Domain/Factory)一致性归 B2。
- /// </summary>
- public async Task<bool> WorkCenterExistsAsync(string? workCenterCode)
- {
- if (string.IsNullOrWhiteSpace(workCenterCode)) return true;
- return await _db.Queryable<AdoS0WorkCtrMaster>()
- .Where(x => x.WorkCtr == workCenterCode)
- .AnyAsync();
- }
- /// <summary>
- /// 物料(ItemMaster)删除前引用检查。
- /// 覆盖下游:工艺路线明细 / 物料替代关系 / 货源清单(SRM 采购)。
- /// </summary>
- public async Task<(int Count, string Table)?> MaterialReferencesAsync(string? itemNum)
- {
- if (string.IsNullOrWhiteSpace(itemNum)) return null;
- var routing = await _db.Queryable<AdoS0MfgRoutingOpDetail>()
- .Where(x => x.MaterialCode == itemNum)
- .CountAsync();
- if (routing > 0) return (routing, "工艺路线明细 (RoutingOpDetail)");
- var sub = await _db.Queryable<AdoS0ItemSubstituteDetail>()
- .Where(x => x.ItemNum == itemNum || x.SubstituteItem == itemNum)
- .CountAsync();
- if (sub > 0) return (sub, "物料替代关系 (ItemSubstituteDetail)");
- // 不查 SRM:srm_purchase.icitem_id 是上游系统(金蝶等)的物料 ID,
- // 与本地 AdoS0ItemMaster.Id 不在同一坐标系,无法直接 JOIN;
- // SRM 表也没有物料编码 (ItemNum) 字符串列。
- // 物料防线已由本地 RoutingOpDetail / ItemSubstituteDetail 覆盖。
- // 详见 BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH。
- return null;
- }
- /// <summary>
- /// 供应商(SuppMaster)删除前引用检查。
- /// 覆盖下游:货源清单(SRM 采购)。
- /// </summary>
- public async Task<(int Count, string Table)?> SupplierReferencesAsync(string? supp)
- {
- if (string.IsNullOrWhiteSpace(supp)) return null;
- // 只用 SupplierNumber(真实列)。原 OR 分支的 x.Supplier 是 [SugarColumn(IsIgnore = true)]
- // 展示字段,进 Where 会抛异常 → SafeCount 兜底 0,实际保护失效(BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH)。
- var srm = await SafeCountAsync(
- () => _db.Queryable<AdoS0SrmPurchase>().Where(x => x.SupplierNumber == supp),
- "SrmPurchase.SupplierNumber");
- if (srm > 0) return (srm, "货源清单 (SrmPurchase)");
- return null;
- }
- /// <summary>
- /// 库位(LocationMaster)删除前引用检查。
- /// 覆盖下游:货架 / 物料默认库位 / 产线 6 个 Location 变体。
- /// </summary>
- public async Task<(int Count, string Table)?> LocationReferencesAsync(string? location)
- {
- if (string.IsNullOrWhiteSpace(location)) return null;
- var shelf = await _db.Queryable<AdoS0LocationShelfMaster>()
- .Where(x => x.Location == location)
- .CountAsync();
- if (shelf > 0) return (shelf, "货架 (LocationShelfMaster)");
- var item = await _db.Queryable<AdoS0ItemMaster>()
- .Where(x => x.Location == location)
- .CountAsync();
- if (item > 0) return (item, "物料默认库位 (ItemMaster.Location)");
- // 补 A2 缺口:LineMaster 的 6 个 Location 变体均为 LocationMaster 的强软引用。
- // B1-9 已在保存端做 existence check,此处与之形成写/删对称。
- // Workshop 不纳入(B1-10 cancellation:无主表的自由文本)。
- var line = await _db.Queryable<AdoS0LineMaster>()
- .Where(x => x.Location == location
- || x.VLocation == location
- || x.Location2 == location
- || x.Location3 == location
- || x.PickingLocation == location
- || x.MidLocation == location)
- .CountAsync();
- if (line > 0) return (line, "产线 (LineMaster 的 Location/VLocation/Location2/Location3/PickingLocation/MidLocation)");
- return null;
- }
- /// <summary>
- /// 部门(DepartmentMaster)删除前引用检查。
- /// 覆盖下游:员工所属部门 / 工作中心所属部门。
- /// </summary>
- public async Task<(int Count, string Table)?> DepartmentReferencesAsync(string? department)
- {
- if (string.IsNullOrWhiteSpace(department)) return null;
- var emp = await _db.Queryable<AdoS0EmployeeMaster>()
- .Where(x => x.Department == department)
- .CountAsync();
- if (emp > 0) return (emp, "员工 (EmployeeMaster)");
- var wc = await _db.Queryable<AdoS0WorkCtrMaster>()
- .Where(x => x.Department == department)
- .CountAsync();
- if (wc > 0) return (wc, "工作中心 (WorkCtrMaster)");
- return null;
- }
- }
|