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"), 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") ], ["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"), 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) { 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 (IdentityFields.TryGetValue(entity.EntityCode, out var 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 }; } 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 }; }