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;
}
}