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; /// /// B2 物料作用域校验结果。 /// public enum MaterialScopeCheck { /// 引用值存在且作用域一致。 Match, /// 引用值不存在于 ItemMaster(走 S01011)。 NotFound, /// 引用值存在但 Company/Factory 不匹配(走 S01012)。 ScopeMiss } /// /// S0 删除前下游引用检查。 /// 每个主数据主表在 DELETE 前调用对应方法,若存在下游引用则拒绝删除。 /// 返回 null 表示无引用可安全删除;非空表示 (引用数, 下游表语义描述)。 /// public sealed class AdoS0ReferenceChecker { private readonly ISqlSugarClient _db; private readonly ILogger _logger; public AdoS0ReferenceChecker(ISqlSugarClient db, ILogger logger) { _db = db; _logger = logger; } /// /// 容错包装:对已知 schema 不对齐表(如 srm_purchase)的查询失败时不阻断删除,只记日志。 /// 独立问题由 BUG-S0-SRMPURCHASE-SCHEMA-MISMATCH 单独修。 /// private async Task SafeCountAsync(Func> 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; } } /// /// 保存端:物料(ItemMaster)存在性检查。 /// 仅校验"目标主表存在该 itemNum",不做 factoryRefId/domain 作用域校验(B2 归口)。 /// 空值视为"未填"直接放行,由调用方先做必填校验。 /// public async Task MaterialExistsAsync(string? itemNum) { if (string.IsNullOrWhiteSpace(itemNum)) return true; return await _db.Queryable() .Where(x => x.ItemNum == itemNum) .AnyAsync(); } /// /// B2 保存端:物料(ItemMaster)存在 + 作用域一致性检查。 /// 返回: /// null — 引用值空,放行(交给必填校验) /// NotFound — 引用值不存在(应返 S01011) /// ScopeMiss — 引用值存在但 Company/Factory 不匹配(应返 S01012) /// Match — 存在且作用域一致,放行 /// 0/0 边界:目标 ItemMaster 的 CompanyRefId=0 且 FactoryRefId=0 被视为"未 scope 化的历史数据", /// 仅当来源 Routing 同为 0/0 时才允许匹配;否则判 ScopeMiss(不做全局物料白名单)。 /// public async Task MaterialExistsInScopeAsync(string? itemNum, long companyRefId, long factoryRefId) { if (string.IsNullOrWhiteSpace(itemNum)) return MaterialScopeCheck.Match; var match = await _db.Queryable() .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; } /// /// 保存端:库位(LocationMaster)存在性检查。 /// 用于货架等"从属于 LocationMaster 聚合"的子实体写入保护。 /// 空值视为"未填"直接放行;作用域(Company/Factory/Domain)一致性归 B2。 /// public async Task LocationExistsAsync(string? location) { if (string.IsNullOrWhiteSpace(location)) return true; return await _db.Queryable() .Where(x => x.Location == location) .AnyAsync(); } /// /// 保存端:部门(DepartmentMaster)存在性检查。 /// 空值视为"未填"直接放行;作用域(DomainCode/Factory)一致性归 B2。 /// 注意:历史数据中存在大量 Department 未对齐到 DepartmentMaster 的情况, /// 本方法只拦截新写入,不影响历史记录读取。 /// public async Task DepartmentExistsAsync(string? department) { if (string.IsNullOrWhiteSpace(department)) return true; return await _db.Queryable() .Where(x => x.Department == department) .AnyAsync(); } /// /// 保存端:工作中心(WorkCtrMaster)存在性检查。 /// 注意跨命名:业务键是 WorkCtrMaster.WorkCtr(非 WorkCenterCode)。 /// 空值视为"未填"直接放行;作用域(Domain/Factory)一致性归 B2。 /// public async Task WorkCenterExistsAsync(string? workCenterCode) { if (string.IsNullOrWhiteSpace(workCenterCode)) return true; return await _db.Queryable() .Where(x => x.WorkCtr == workCenterCode) .AnyAsync(); } /// /// 物料(ItemMaster)删除前引用检查。 /// 覆盖下游:工艺路线明细 / 物料替代关系 / 货源清单(SRM 采购)。 /// public async Task<(int Count, string Table)?> MaterialReferencesAsync(string? itemNum) { if (string.IsNullOrWhiteSpace(itemNum)) return null; var routing = await _db.Queryable() .Where(x => x.MaterialCode == itemNum) .CountAsync(); if (routing > 0) return (routing, "工艺路线明细 (RoutingOpDetail)"); var sub = await _db.Queryable() .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; } /// /// 供应商(SuppMaster)删除前引用检查。 /// 覆盖下游:货源清单(SRM 采购)。 /// 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().Where(x => x.SupplierNumber == supp), "SrmPurchase.SupplierNumber"); if (srm > 0) return (srm, "货源清单 (SrmPurchase)"); return null; } /// /// 库位(LocationMaster)删除前引用检查。 /// 覆盖下游:货架 / 物料默认库位 / 产线 6 个 Location 变体。 /// public async Task<(int Count, string Table)?> LocationReferencesAsync(string? location) { if (string.IsNullOrWhiteSpace(location)) return null; var shelf = await _db.Queryable() .Where(x => x.Location == location) .CountAsync(); if (shelf > 0) return (shelf, "货架 (LocationShelfMaster)"); var item = await _db.Queryable() .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() .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; } /// /// 部门(DepartmentMaster)删除前引用检查。 /// 覆盖下游:员工所属部门 / 工作中心所属部门。 /// public async Task<(int Count, string Table)?> DepartmentReferencesAsync(string? department) { if (string.IsNullOrWhiteSpace(department)) return null; var emp = await _db.Queryable() .Where(x => x.Department == department) .CountAsync(); if (emp > 0) return (emp, "员工 (EmployeeMaster)"); var wc = await _db.Queryable() .Where(x => x.Department == department) .CountAsync(); if (wc > 0) return (wc, "工作中心 (WorkCtrMaster)"); return null; } }