MdpInboundFieldMapper.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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"), Req("BizDocType"),
  32. Opt("ItemCode"), Opt("Qty"),
  33. Opt("ApprovedFlag"), Opt("ApprovedTime"), Opt("DocDate"),
  34. Opt("RefTaskNo"), Opt("SummaryFlag"), Opt("VoidFlag"), Opt("TransTime"),
  35. Opt("Domain"), Opt("TransType"), Opt("SrcTransType"), Opt("Location")
  36. ],
  37. ["S5_INVENTORY_OPENING_BALANCE"] =
  38. [
  39. Req("Id"), Opt("idid"), Opt("code"), Opt("sl"), Opt("slzx"),
  40. Opt("shyn"), Opt("shtime"), Opt("date0")
  41. ],
  42. ["S7_FINISHED_ONHAND"] =
  43. [
  44. Req("Id"), Opt("code"), Opt("sl"), Opt("date0"), Opt("location")
  45. ],
  46. ["S7_FINISHED_OPENING_BALANCE"] =
  47. [
  48. Req("Id"), Opt("code"), Opt("sl"), Opt("date0"), Opt("location")
  49. ],
  50. ["MDM_EMPLOYEE_HEADCOUNT"] =
  51. [
  52. Req("Domain"), Req("Employee"), Req("Position"), Req("EmploymentStatus"),
  53. Opt("Name"), Opt("Department")
  54. ],
  55. // 齐套核验由我方资源检查作业产出。第三方推这两张结果才能点亮制造工单缺料规则;
  56. // 推物料主数据或库存现存量替代不了这次核验。
  57. ["S1_REQUIREMENT_EXAMINE_RESULT"] =
  58. [
  59. Req("Id"), Req("bill_no"), Req("morder_no"),
  60. Opt("sentry_id"), Opt("create_time"), Opt("IsDeleted")
  61. ],
  62. ["S1_REQUIREMENT_EXAMINE_DETAIL"] =
  63. [
  64. Req("Id"), Req("examine_id"), Req("item_number"), Req("lack_qty"), Req("level"),
  65. Opt("num"), Opt("item_name"), Opt("needCount"), Opt("qty"), Opt("is_use")
  66. ]
  67. };
  68. public const string ContractVersionHeader = "X-Mdp-Contract-Version";
  69. /// <summary>v1:已开通报文。不声明版本时走这套,必填集合不扩大。</summary>
  70. private static readonly IReadOnlyDictionary<string, MdpInboundSchemaField[]> V1Fields =
  71. new Dictionary<string, MdpInboundSchemaField[]>(StringComparer.OrdinalIgnoreCase)
  72. {
  73. ["S5_INVENTORY_TXN"] =
  74. [
  75. Req("Id"), Opt("idid"), Opt("code"), Opt("sl"), Opt("slzx"),
  76. Opt("shyn"), Opt("shtime"), Opt("date0"), Opt("lynoid"),
  77. Opt("gdyn"), Opt("gdtime"), Opt("rwnoid"), Opt("jhdate"), Opt("addtime")
  78. ],
  79. ["MDM_EMPLOYEE_HEADCOUNT"] =
  80. [
  81. Req("Domain"), Req("Employee"),
  82. Opt("Name"), Opt("Department")
  83. ]
  84. };
  85. private static readonly MdpInboundSchemaField[] CommonContractFields =
  86. [
  87. Req("sourceUpdatedAt"),
  88. Opt("sourceVersion"),
  89. Opt("op")
  90. ];
  91. private readonly ISqlSugarClient _db;
  92. public MdpInboundFieldMapper(ISqlSugarClient db) => _db = db;
  93. public async Task<IReadOnlyList<MdpFieldMap>> LoadAsync(
  94. long tenantId, string entityCode, string accessKey, CancellationToken ct)
  95. {
  96. var code = (entityCode ?? string.Empty).Trim().ToUpperInvariant();
  97. var rows = await _db.Queryable<MdpFieldMap>()
  98. .Where(m => m.TenantId == tenantId && m.Status == 1)
  99. .Where(m => m.AccessKey == accessKey || m.AccessKey == "*")
  100. .Where("UPPER(entity_code) = @code", new SugarParameter("@code", code))
  101. .ToListAsync(ct);
  102. var merged = new Dictionary<string, MdpFieldMap>(StringComparer.OrdinalIgnoreCase);
  103. foreach (var row in rows.Where(r => r.AccessKey == "*"))
  104. merged[row.ContractField] = row;
  105. foreach (var row in rows.Where(r => r.AccessKey != "*"))
  106. merged[row.ContractField] = row;
  107. return merged.Values.ToList();
  108. }
  109. /// <summary>把对外规范字段改写成 raw 键;未登记字段原样保留。输出键名以映射表 raw_key 大小写为准。</summary>
  110. public Dictionary<string, object?> MapRow(
  111. IDictionary<string, object?> row, IReadOnlyList<MdpFieldMap> maps)
  112. {
  113. var byContract = maps.ToDictionary(m => m.ContractField, StringComparer.OrdinalIgnoreCase);
  114. var output = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
  115. foreach (var kv in row)
  116. {
  117. if (byContract.TryGetValue(kv.Key, out var map) && !string.IsNullOrWhiteSpace(map.RawKey))
  118. output[map.RawKey] = kv.Value;
  119. else
  120. output[kv.Key] = kv.Value;
  121. }
  122. return output;
  123. }
  124. public List<string> MissingRequired(IDictionary<string, object?> mapped, IReadOnlyList<MdpFieldMap> maps)
  125. {
  126. var missing = new List<string>();
  127. foreach (var map in maps.Where(m => m.IsRequired == 1))
  128. {
  129. var key = string.IsNullOrWhiteSpace(map.RawKey) ? map.ContractField : map.RawKey;
  130. if (!TryGetNonEmpty(mapped, key))
  131. missing.Add(map.ContractField);
  132. }
  133. return missing;
  134. }
  135. public MdpInboundSchemaData BuildSchema(
  136. MdpEntity entity, IReadOnlyList<MdpFieldMap> maps, string? contractVersion = null)
  137. {
  138. var fields = new List<MdpInboundSchemaField>();
  139. var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  140. void Add(MdpInboundSchemaField f)
  141. {
  142. if (seen.Add(f.Name))
  143. fields.Add(f);
  144. }
  145. if (maps.Count > 0)
  146. {
  147. foreach (var map in maps)
  148. {
  149. Add(new MdpInboundSchemaField
  150. {
  151. Name = map.ContractField,
  152. RawKey = string.IsNullOrWhiteSpace(map.RawKey) ? map.ContractField : map.RawKey,
  153. Required = map.IsRequired == 1
  154. });
  155. }
  156. }
  157. else if (ResolveIdentity(entity.EntityCode, contractVersion) is { } identity)
  158. {
  159. foreach (var f in identity)
  160. Add(f);
  161. }
  162. else if (!string.IsNullOrWhiteSpace(entity.BizKeyExpr))
  163. {
  164. foreach (var part in entity.BizKeyExpr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
  165. Add(Req(part));
  166. }
  167. foreach (var f in CommonContractFields)
  168. Add(f);
  169. return new MdpInboundSchemaData
  170. {
  171. EntityCode = entity.EntityCode,
  172. BizKeyExpr = entity.BizKeyExpr,
  173. Fields = fields
  174. };
  175. }
  176. /// <summary>未声明或无法识别的版本按 v1。只有显式 v2 才使用中立必填集合。</summary>
  177. public static string NormalizeVersion(string? contractVersion)
  178. {
  179. var v = (contractVersion ?? string.Empty).Trim();
  180. return v.Equals("v2", StringComparison.OrdinalIgnoreCase) ? "v2" : "v1";
  181. }
  182. private static MdpInboundSchemaField[]? ResolveIdentity(string entityCode, string? contractVersion)
  183. {
  184. if (NormalizeVersion(contractVersion) == "v1" && V1Fields.TryGetValue(entityCode, out var v1))
  185. return v1;
  186. return IdentityFields.TryGetValue(entityCode, out var fields) ? fields : null;
  187. }
  188. public static Dictionary<string, object?> ElementToDict(JsonElement el)
  189. {
  190. var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
  191. if (el.ValueKind != JsonValueKind.Object)
  192. return dict;
  193. foreach (var prop in el.EnumerateObject())
  194. {
  195. dict[prop.Name] = prop.Value.ValueKind switch
  196. {
  197. JsonValueKind.Null => null,
  198. JsonValueKind.String => prop.Value.GetString(),
  199. JsonValueKind.Number => prop.Value.TryGetInt64(out var l) ? l
  200. : prop.Value.TryGetDecimal(out var d) ? d
  201. : prop.Value.GetDouble(),
  202. JsonValueKind.True => true,
  203. JsonValueKind.False => false,
  204. _ => prop.Value.GetRawText()
  205. };
  206. }
  207. return dict;
  208. }
  209. private static bool TryGetNonEmpty(IDictionary<string, object?> row, string key)
  210. {
  211. foreach (var kv in row)
  212. {
  213. if (!string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))
  214. continue;
  215. if (kv.Value == null || kv.Value == DBNull.Value)
  216. return false;
  217. return !string.IsNullOrWhiteSpace(kv.Value.ToString());
  218. }
  219. return false;
  220. }
  221. private static MdpInboundSchemaField Req(string name) =>
  222. new() { Name = name, RawKey = name, Required = true };
  223. private static MdpInboundSchemaField Opt(string name) =>
  224. new() { Name = name, RawKey = name, Required = false };
  225. }