using System.Text.Json; using Admin.NET.Plugin.AiDOP.Entity.DataPlatform; namespace Admin.NET.Plugin.AiDOP.DataPlatform.Inbound; /// /// mdp_field_map 两层映射:精确 access_key 覆盖 * 兜底;无映射行则恒等。 /// 与 PULL 的 mdp_field_mapping 不是同一张表。 /// public sealed class MdpInboundFieldMapper : ITransient { private static readonly IReadOnlyDictionary IdentityFields = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["S1_SALES_ORDER_ENTRY"] = [ Req("bill_no"), Req("entry_seq"), Req("seorder_id"), Req("qty"), Opt("item_number"), Opt("item_name"), Opt("specification"), Opt("unit"), Opt("map_number"), Opt("map_name"), Opt("bom_number"), Opt("deliver_notice_count"), Opt("deliver_count"), Opt("price"), Opt("tax_price"), Opt("amount"), Opt("total_amount"), Opt("plan_date"), Opt("date"), Opt("sys_capacity_date"), Opt("sys_material_date"), Opt("planner_no"), Opt("planner_name"), Opt("custom_order_bill_no"), Opt("progress"), Opt("urgent") ], ["MDM_ITEM"] = [ Req("ItemNum"), Req("Descr"), Opt("Drawing"), Opt("UM"), Opt("ItemType"), Opt("Status"), Opt("Domain") ], ["S5_INVENTORY_TXN"] = [ Req("Id"), Req("BizDocType"), Opt("ItemCode"), Opt("Qty"), Opt("ApprovedFlag"), Opt("ApprovedTime"), Opt("DocDate"), Opt("RefTaskNo"), Opt("SummaryFlag"), Opt("VoidFlag"), Opt("TransTime"), Opt("Domain"), Opt("TransType"), Opt("SrcTransType"), Opt("Location") ], ["S5_INVENTORY_OPENING_BALANCE"] = [ Req("Id"), Opt("idid"), Opt("code"), Opt("sl"), Opt("slzx"), Opt("shyn"), Opt("shtime"), Opt("date0") ], ["S7_FINISHED_ONHAND"] = [ Req("Id"), Opt("code"), Opt("sl"), Opt("date0"), Opt("location") ], ["S7_FINISHED_OPENING_BALANCE"] = [ Req("Id"), Opt("code"), Opt("sl"), Opt("date0"), Opt("location") ], ["MDM_EMPLOYEE_HEADCOUNT"] = [ Req("Domain"), Req("Employee"), Req("Position"), Req("EmploymentStatus"), Opt("Name"), Opt("Department") ], // 齐套核验由我方资源检查作业产出。第三方推这两张结果才能点亮制造工单缺料规则; // 推物料主数据或库存现存量替代不了这次核验。 ["S1_REQUIREMENT_EXAMINE_RESULT"] = [ Req("Id"), Req("bill_no"), Req("morder_no"), Opt("sentry_id"), Opt("create_time"), Opt("IsDeleted") ], ["S1_REQUIREMENT_EXAMINE_DETAIL"] = [ Req("Id"), Req("examine_id"), Req("item_number"), Req("lack_qty"), Req("level"), Opt("num"), Opt("item_name"), Opt("needCount"), Opt("qty"), Opt("is_use") ] }; public const string ContractVersionHeader = "X-Mdp-Contract-Version"; /// v1:已开通报文。不声明版本时走这套,必填集合不扩大。 private static readonly IReadOnlyDictionary V1Fields = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["S5_INVENTORY_TXN"] = [ Req("Id"), Opt("idid"), Opt("code"), Opt("sl"), Opt("slzx"), Opt("shyn"), Opt("shtime"), Opt("date0"), Opt("lynoid"), Opt("gdyn"), Opt("gdtime"), Opt("rwnoid"), Opt("jhdate"), Opt("addtime") ], ["MDM_EMPLOYEE_HEADCOUNT"] = [ Req("Domain"), Req("Employee"), Opt("Name"), Opt("Department") ] }; private static readonly MdpInboundSchemaField[] CommonContractFields = [ Req("sourceUpdatedAt"), Opt("sourceVersion"), Opt("op") ]; private readonly ISqlSugarClient _db; public MdpInboundFieldMapper(ISqlSugarClient db) => _db = db; public async Task> LoadAsync( long tenantId, string entityCode, string accessKey, CancellationToken ct) { var code = (entityCode ?? string.Empty).Trim().ToUpperInvariant(); var rows = await _db.Queryable() .Where(m => m.TenantId == tenantId && m.Status == 1) .Where(m => m.AccessKey == accessKey || m.AccessKey == "*") .Where("UPPER(entity_code) = @code", new SugarParameter("@code", code)) .ToListAsync(ct); var merged = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var row in rows.Where(r => r.AccessKey == "*")) merged[row.ContractField] = row; foreach (var row in rows.Where(r => r.AccessKey != "*")) merged[row.ContractField] = row; return merged.Values.ToList(); } /// 把对外规范字段改写成 raw 键;未登记字段原样保留。输出键名以映射表 raw_key 大小写为准。 public Dictionary MapRow( IDictionary row, IReadOnlyList maps) { var byContract = maps.ToDictionary(m => m.ContractField, StringComparer.OrdinalIgnoreCase); var output = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kv in row) { if (byContract.TryGetValue(kv.Key, out var map) && !string.IsNullOrWhiteSpace(map.RawKey)) output[map.RawKey] = kv.Value; else output[kv.Key] = kv.Value; } return output; } public List MissingRequired(IDictionary mapped, IReadOnlyList maps) { var missing = new List(); foreach (var map in maps.Where(m => m.IsRequired == 1)) { var key = string.IsNullOrWhiteSpace(map.RawKey) ? map.ContractField : map.RawKey; if (!TryGetNonEmpty(mapped, key)) missing.Add(map.ContractField); } return missing; } public MdpInboundSchemaData BuildSchema( MdpEntity entity, IReadOnlyList maps, string? contractVersion = null) { var fields = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); void Add(MdpInboundSchemaField f) { if (seen.Add(f.Name)) fields.Add(f); } if (maps.Count > 0) { foreach (var map in maps) { Add(new MdpInboundSchemaField { Name = map.ContractField, RawKey = string.IsNullOrWhiteSpace(map.RawKey) ? map.ContractField : map.RawKey, Required = map.IsRequired == 1 }); } } else if (ResolveIdentity(entity.EntityCode, contractVersion) is { } identity) { foreach (var f in identity) Add(f); } else if (!string.IsNullOrWhiteSpace(entity.BizKeyExpr)) { foreach (var part in entity.BizKeyExpr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) Add(Req(part)); } foreach (var f in CommonContractFields) Add(f); return new MdpInboundSchemaData { EntityCode = entity.EntityCode, BizKeyExpr = entity.BizKeyExpr, Fields = fields }; } /// 未声明或无法识别的版本按 v1。只有显式 v2 才使用中立必填集合。 public static string NormalizeVersion(string? contractVersion) { var v = (contractVersion ?? string.Empty).Trim(); return v.Equals("v2", StringComparison.OrdinalIgnoreCase) ? "v2" : "v1"; } private static MdpInboundSchemaField[]? ResolveIdentity(string entityCode, string? contractVersion) { if (NormalizeVersion(contractVersion) == "v1" && V1Fields.TryGetValue(entityCode, out var v1)) return v1; return IdentityFields.TryGetValue(entityCode, out var fields) ? fields : null; } public static Dictionary ElementToDict(JsonElement el) { var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); if (el.ValueKind != JsonValueKind.Object) return dict; foreach (var prop in el.EnumerateObject()) { dict[prop.Name] = prop.Value.ValueKind switch { JsonValueKind.Null => null, JsonValueKind.String => prop.Value.GetString(), JsonValueKind.Number => prop.Value.TryGetInt64(out var l) ? l : prop.Value.TryGetDecimal(out var d) ? d : prop.Value.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, _ => prop.Value.GetRawText() }; } return dict; } private static bool TryGetNonEmpty(IDictionary row, string key) { foreach (var kv in row) { if (!string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase)) continue; if (kv.Value == null || kv.Value == DBNull.Value) return false; return !string.IsNullOrWhiteSpace(kv.Value.ToString()); } return false; } private static MdpInboundSchemaField Req(string name) => new() { Name = name, RawKey = name, Required = true }; private static MdpInboundSchemaField Opt(string name) => new() { Name = name, RawKey = name, Required = false }; }