MdpInboundFieldMapper.cs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. using System.Text.Json;
  2. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  3. namespace Admin.NET.Plugin.AiDOP.DataPlatform.Inbound;
  4. /// <summary>
  5. /// mdp_field_map 两层映射:精确 access_key 覆盖 * 兜底;无映射行则恒等。
  6. /// 与 PULL 的 mdp_field_mapping 不是同一张表。
  7. /// </summary>
  8. public sealed class MdpInboundFieldMapper : ITransient
  9. {
  10. private static readonly IReadOnlyDictionary<string, MdpInboundSchemaField[]> IdentityFields =
  11. new Dictionary<string, MdpInboundSchemaField[]>(StringComparer.OrdinalIgnoreCase)
  12. {
  13. ["S1_SALES_ORDER_ENTRY"] =
  14. [
  15. Req("bill_no"), Req("entry_seq"), Req("seorder_id"), Req("qty"),
  16. Opt("item_number"), Opt("item_name"), Opt("specification"), Opt("unit"),
  17. Opt("map_number"), Opt("map_name"), Opt("bom_number"),
  18. Opt("deliver_notice_count"), Opt("deliver_count"),
  19. Opt("price"), Opt("tax_price"), Opt("amount"), Opt("total_amount"),
  20. Opt("plan_date"), Opt("date"), Opt("sys_capacity_date"), Opt("sys_material_date"),
  21. Opt("planner_no"), Opt("planner_name"), Opt("custom_order_bill_no"),
  22. Opt("progress"), Opt("urgent")
  23. ],
  24. ["MDM_ITEM"] =
  25. [
  26. Req("ItemNum"), Req("Descr"),
  27. Opt("Drawing"), Opt("UM"), Opt("ItemType"), Opt("Status"), Opt("Domain")
  28. ],
  29. ["S5_INVENTORY_TXN"] =
  30. [
  31. Req("Id"), Opt("idid"), Opt("code"), Opt("sl"), Opt("slzx"),
  32. Opt("shyn"), Opt("shtime"), Opt("date0"), Opt("lynoid"),
  33. Opt("gdyn"), Opt("gdtime"), Opt("rwnoid"), Opt("jhdate"), Opt("addtime")
  34. ],
  35. ["S5_INVENTORY_OPENING_BALANCE"] =
  36. [
  37. Req("Id"), Opt("idid"), Opt("code"), Opt("sl"), Opt("slzx"),
  38. Opt("shyn"), Opt("shtime"), Opt("date0")
  39. ],
  40. ["S7_FINISHED_ONHAND"] =
  41. [
  42. Req("Id"), Opt("code"), Opt("sl"), Opt("date0"), Opt("location")
  43. ],
  44. ["S7_FINISHED_OPENING_BALANCE"] =
  45. [
  46. Req("Id"), Opt("code"), Opt("sl"), Opt("date0"), Opt("location")
  47. ],
  48. ["MDM_EMPLOYEE_HEADCOUNT"] =
  49. [
  50. Req("Domain"), Req("Employee"),
  51. Opt("Name"), Opt("Department")
  52. ]
  53. };
  54. private static readonly MdpInboundSchemaField[] CommonContractFields =
  55. [
  56. Req("sourceUpdatedAt"),
  57. Opt("sourceVersion"),
  58. Opt("op")
  59. ];
  60. private readonly ISqlSugarClient _db;
  61. public MdpInboundFieldMapper(ISqlSugarClient db) => _db = db;
  62. public async Task<IReadOnlyList<MdpFieldMap>> LoadAsync(
  63. long tenantId, string entityCode, string accessKey, CancellationToken ct)
  64. {
  65. var code = (entityCode ?? string.Empty).Trim().ToUpperInvariant();
  66. var rows = await _db.Queryable<MdpFieldMap>()
  67. .Where(m => m.TenantId == tenantId && m.Status == 1)
  68. .Where(m => m.AccessKey == accessKey || m.AccessKey == "*")
  69. .Where("UPPER(entity_code) = @code", new SugarParameter("@code", code))
  70. .ToListAsync(ct);
  71. var merged = new Dictionary<string, MdpFieldMap>(StringComparer.OrdinalIgnoreCase);
  72. foreach (var row in rows.Where(r => r.AccessKey == "*"))
  73. merged[row.ContractField] = row;
  74. foreach (var row in rows.Where(r => r.AccessKey != "*"))
  75. merged[row.ContractField] = row;
  76. return merged.Values.ToList();
  77. }
  78. /// <summary>把对外规范字段改写成 raw 键;未登记字段原样保留。输出键名以映射表 raw_key 大小写为准。</summary>
  79. public Dictionary<string, object?> MapRow(
  80. IDictionary<string, object?> row, IReadOnlyList<MdpFieldMap> maps)
  81. {
  82. var byContract = maps.ToDictionary(m => m.ContractField, StringComparer.OrdinalIgnoreCase);
  83. var output = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
  84. foreach (var kv in row)
  85. {
  86. if (byContract.TryGetValue(kv.Key, out var map) && !string.IsNullOrWhiteSpace(map.RawKey))
  87. output[map.RawKey] = kv.Value;
  88. else
  89. output[kv.Key] = kv.Value;
  90. }
  91. return output;
  92. }
  93. public List<string> MissingRequired(IDictionary<string, object?> mapped, IReadOnlyList<MdpFieldMap> maps)
  94. {
  95. var missing = new List<string>();
  96. foreach (var map in maps.Where(m => m.IsRequired == 1))
  97. {
  98. var key = string.IsNullOrWhiteSpace(map.RawKey) ? map.ContractField : map.RawKey;
  99. if (!TryGetNonEmpty(mapped, key))
  100. missing.Add(map.ContractField);
  101. }
  102. return missing;
  103. }
  104. public MdpInboundSchemaData BuildSchema(
  105. MdpEntity entity, IReadOnlyList<MdpFieldMap> maps)
  106. {
  107. var fields = new List<MdpInboundSchemaField>();
  108. var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  109. void Add(MdpInboundSchemaField f)
  110. {
  111. if (seen.Add(f.Name))
  112. fields.Add(f);
  113. }
  114. if (maps.Count > 0)
  115. {
  116. foreach (var map in maps)
  117. {
  118. Add(new MdpInboundSchemaField
  119. {
  120. Name = map.ContractField,
  121. RawKey = string.IsNullOrWhiteSpace(map.RawKey) ? map.ContractField : map.RawKey,
  122. Required = map.IsRequired == 1
  123. });
  124. }
  125. }
  126. else if (IdentityFields.TryGetValue(entity.EntityCode, out var identity))
  127. {
  128. foreach (var f in identity)
  129. Add(f);
  130. }
  131. else if (!string.IsNullOrWhiteSpace(entity.BizKeyExpr))
  132. {
  133. foreach (var part in entity.BizKeyExpr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
  134. Add(Req(part));
  135. }
  136. foreach (var f in CommonContractFields)
  137. Add(f);
  138. return new MdpInboundSchemaData
  139. {
  140. EntityCode = entity.EntityCode,
  141. BizKeyExpr = entity.BizKeyExpr,
  142. Fields = fields
  143. };
  144. }
  145. public static Dictionary<string, object?> ElementToDict(JsonElement el)
  146. {
  147. var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
  148. if (el.ValueKind != JsonValueKind.Object)
  149. return dict;
  150. foreach (var prop in el.EnumerateObject())
  151. {
  152. dict[prop.Name] = prop.Value.ValueKind switch
  153. {
  154. JsonValueKind.Null => null,
  155. JsonValueKind.String => prop.Value.GetString(),
  156. JsonValueKind.Number => prop.Value.TryGetInt64(out var l) ? l
  157. : prop.Value.TryGetDecimal(out var d) ? d
  158. : prop.Value.GetDouble(),
  159. JsonValueKind.True => true,
  160. JsonValueKind.False => false,
  161. _ => prop.Value.GetRawText()
  162. };
  163. }
  164. return dict;
  165. }
  166. private static bool TryGetNonEmpty(IDictionary<string, object?> row, string key)
  167. {
  168. foreach (var kv in row)
  169. {
  170. if (!string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))
  171. continue;
  172. if (kv.Value == null || kv.Value == DBNull.Value)
  173. return false;
  174. return !string.IsNullOrWhiteSpace(kv.Value.ToString());
  175. }
  176. return false;
  177. }
  178. private static MdpInboundSchemaField Req(string name) =>
  179. new() { Name = name, RawKey = name, Required = true };
  180. private static MdpInboundSchemaField Opt(string name) =>
  181. new() { Name = name, RawKey = name, Required = false };
  182. }