MdmMirrorUpsertService.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  2. using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
  3. using Admin.NET.Plugin.AiDOP.Entity.S0.Supply;
  4. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  5. using Admin.NET.Plugin.AiDOP.Infrastructure;
  6. using Microsoft.Extensions.Logging;
  7. namespace Admin.NET.Plugin.AiDOP.DataPlatform.Inbound;
  8. /// <summary>主数据 stg 批 COMMITTED 后回灌 S0 镜像。失败不阻断接收。</summary>
  9. public sealed class MdmMirrorUpsertService : ITransient
  10. {
  11. private readonly ISqlSugarClient _db;
  12. private readonly AdoS0ReferenceChecker _refs;
  13. private readonly ILogger<MdmMirrorUpsertService> _logger;
  14. public MdmMirrorUpsertService(
  15. ISqlSugarClient db,
  16. AdoS0ReferenceChecker refs,
  17. ILogger<MdmMirrorUpsertService> logger)
  18. {
  19. _db = db;
  20. _refs = refs;
  21. _logger = logger;
  22. }
  23. public async Task MirrorCommittedAsync(
  24. string entityCode,
  25. long tenantId,
  26. long? factoryId,
  27. string sourceCode,
  28. IReadOnlyList<MdpInboundPreparedRow> rows,
  29. CancellationToken ct)
  30. {
  31. if (rows == null || rows.Count == 0)
  32. return;
  33. var factory = factoryId is > 0 ? factoryId.Value : 1L;
  34. foreach (var row in rows)
  35. {
  36. try
  37. {
  38. var code = (entityCode ?? string.Empty).Trim().ToUpperInvariant();
  39. if (code is "MDM_ITEM")
  40. await MirrorItemAsync(tenantId, factory, sourceCode, row, ct);
  41. else if (code is "MDM_CUSTOMER")
  42. await MirrorCustomerAsync(tenantId, factory, sourceCode, row, ct);
  43. else if (code is "MDM_SUPPLIER")
  44. await MirrorSupplierAsync(tenantId, factory, sourceCode, row, ct);
  45. else if (code is "MDM_LOCATION")
  46. await MirrorLocationAsync(tenantId, factory, sourceCode, row, ct);
  47. else if (code is "MDM_EMPLOYEE_HEADCOUNT")
  48. await MirrorEmployeeAsync(tenantId, factory, sourceCode, row, ct);
  49. }
  50. catch (Exception ex)
  51. {
  52. _logger.LogError(ex, "inbound mirror failed entity={Entity} biz={Biz}", entityCode, row.BizKey);
  53. }
  54. }
  55. }
  56. private async Task MirrorItemAsync(
  57. long tenantId, long factoryId, string sourceCode, MdpInboundPreparedRow row, CancellationToken ct)
  58. {
  59. var itemNum = Str(row.Dict, "ItemNum") ?? Str(row.Dict, "number");
  60. if (string.IsNullOrWhiteSpace(itemNum))
  61. return;
  62. var incomingAt = ParseTime(row.SourceUpdatedAt);
  63. var existing = await _db.Queryable<AdoS0ItemMaster>()
  64. .Where(x => x.TenantId == tenantId && x.ItemNum == itemNum)
  65. .FirstAsync(ct);
  66. if (existing != null && string.IsNullOrWhiteSpace(existing.SourceSystem))
  67. {
  68. await InsertConflictAsync(tenantId, "MDM_ITEM", row.BizKey, sourceCode, row.RawJson, "ItemMaster", ct);
  69. return;
  70. }
  71. if (existing?.SourceUpdatedAt is { } oldAt && incomingAt is { } neu && oldAt > neu)
  72. {
  73. _logger.LogInformation("inbound mirror skipped_stale item={Item}", itemNum);
  74. return;
  75. }
  76. if (!string.IsNullOrWhiteSpace(Str(row.Dict, "Location")))
  77. await _refs.LocationExistsAsync(tenantId, Str(row.Dict, "Location"));
  78. var now = DateTime.Now;
  79. if (existing == null)
  80. {
  81. await _db.Insertable(new AdoS0ItemMaster
  82. {
  83. TenantId = tenantId,
  84. FactoryRefId = factoryId,
  85. DomainCode = Str(row.Dict, "Domain"),
  86. ItemNum = itemNum,
  87. Descr = Str(row.Dict, "Descr") ?? Str(row.Dict, "name") ?? itemNum,
  88. Drawing = Str(row.Dict, "Drawing") ?? Str(row.Dict, "model"),
  89. UM = Str(row.Dict, "UM") ?? Str(row.Dict, "unit"),
  90. ItemType = Str(row.Dict, "ItemType"),
  91. Status = Str(row.Dict, "Status") ?? "normal",
  92. IsActive = !IsInactive(Str(row.Dict, "Status") ?? Str(row.Dict, "is_active")),
  93. SourceSystem = sourceCode,
  94. SourceUpdatedAt = incomingAt,
  95. CreateTime = now,
  96. UpdateTime = now,
  97. UpdateUser = "API_INBOUND"
  98. }).ExecuteCommandAsync(ct);
  99. return;
  100. }
  101. existing.Descr = Str(row.Dict, "Descr") ?? Str(row.Dict, "name") ?? existing.Descr;
  102. existing.Drawing = Str(row.Dict, "Drawing") ?? Str(row.Dict, "model") ?? existing.Drawing;
  103. existing.UM = Str(row.Dict, "UM") ?? Str(row.Dict, "unit") ?? existing.UM;
  104. existing.ItemType = Str(row.Dict, "ItemType") ?? existing.ItemType;
  105. existing.DomainCode = Str(row.Dict, "Domain") ?? existing.DomainCode;
  106. if (!string.IsNullOrWhiteSpace(Str(row.Dict, "Status")))
  107. {
  108. existing.Status = Str(row.Dict, "Status");
  109. existing.IsActive = !IsInactive(existing.Status);
  110. }
  111. existing.SourceSystem = sourceCode;
  112. existing.SourceUpdatedAt = incomingAt ?? existing.SourceUpdatedAt;
  113. existing.UpdateTime = now;
  114. existing.UpdateUser = "API_INBOUND";
  115. await _db.Updateable(existing)
  116. .IgnoreColumns(x => new { x.Location, x.DefaultShelf, x.SafetyStk, x.LotSerialControl, x.AllocateSingleLot })
  117. .ExecuteCommandAsync(ct);
  118. }
  119. private async Task MirrorCustomerAsync(
  120. long tenantId, long factoryId, string sourceCode, MdpInboundPreparedRow row, CancellationToken ct)
  121. {
  122. var cust = Str(row.Dict, "Cust") ?? Str(row.Dict, "custom_no");
  123. if (string.IsNullOrWhiteSpace(cust))
  124. return;
  125. var incomingAt = ParseTime(row.SourceUpdatedAt);
  126. var existing = await _db.Queryable<AdoS0CustMaster>()
  127. .Where(x => x.TenantId == tenantId && x.Cust == cust)
  128. .FirstAsync(ct);
  129. if (existing != null && string.IsNullOrWhiteSpace(existing.SourceSystem))
  130. {
  131. await InsertConflictAsync(tenantId, "MDM_CUSTOMER", row.BizKey, sourceCode, row.RawJson, "CustMaster", ct);
  132. return;
  133. }
  134. if (existing?.SourceUpdatedAt is { } oldAt && incomingAt is { } neu && oldAt > neu)
  135. return;
  136. var now = DateTime.Now;
  137. if (existing == null)
  138. {
  139. await _db.Insertable(new AdoS0CustMaster
  140. {
  141. TenantId = tenantId,
  142. FactoryRefId = factoryId,
  143. Cust = cust,
  144. SortName = Str(row.Dict, "SortName") ?? Str(row.Dict, "custom_name"),
  145. SourceSystem = sourceCode,
  146. SourceUpdatedAt = incomingAt,
  147. CreateTime = now,
  148. UpdateTime = now
  149. }).ExecuteCommandAsync(ct);
  150. return;
  151. }
  152. existing.SortName = Str(row.Dict, "SortName") ?? existing.SortName;
  153. existing.SourceSystem = sourceCode;
  154. existing.SourceUpdatedAt = incomingAt ?? existing.SourceUpdatedAt;
  155. existing.UpdateTime = now;
  156. await _db.Updateable(existing).ExecuteCommandAsync(ct);
  157. }
  158. private async Task MirrorSupplierAsync(
  159. long tenantId, long factoryId, string sourceCode, MdpInboundPreparedRow row, CancellationToken ct)
  160. {
  161. var supp = Str(row.Dict, "Supp") ?? Str(row.Dict, "supplier_number");
  162. if (string.IsNullOrWhiteSpace(supp))
  163. return;
  164. var incomingAt = ParseTime(row.SourceUpdatedAt);
  165. var existing = await _db.Queryable<AdoS0SuppMaster>()
  166. .Where(x => x.TenantId == tenantId && x.Supp == supp)
  167. .FirstAsync(ct);
  168. if (existing != null && string.IsNullOrWhiteSpace(existing.SourceSystem))
  169. {
  170. await InsertConflictAsync(tenantId, "MDM_SUPPLIER", row.BizKey, sourceCode, row.RawJson, "SuppMaster", ct);
  171. return;
  172. }
  173. if (existing?.SourceUpdatedAt is { } oldAt && incomingAt is { } neu && oldAt > neu)
  174. return;
  175. var now = DateTime.Now;
  176. if (existing == null)
  177. {
  178. await _db.Insertable(new AdoS0SuppMaster
  179. {
  180. TenantId = tenantId,
  181. FactoryRefId = factoryId,
  182. Supp = supp,
  183. SortName = Str(row.Dict, "SortName"),
  184. SourceSystem = sourceCode,
  185. SourceUpdatedAt = incomingAt,
  186. CreateTime = now,
  187. UpdateTime = now
  188. }).ExecuteCommandAsync(ct);
  189. return;
  190. }
  191. existing.SortName = Str(row.Dict, "SortName") ?? existing.SortName;
  192. existing.SourceSystem = sourceCode;
  193. existing.SourceUpdatedAt = incomingAt ?? existing.SourceUpdatedAt;
  194. existing.UpdateTime = now;
  195. await _db.Updateable(existing).ExecuteCommandAsync(ct);
  196. }
  197. private async Task MirrorLocationAsync(
  198. long tenantId, long factoryId, string sourceCode, MdpInboundPreparedRow row, CancellationToken ct)
  199. {
  200. var loc = Str(row.Dict, "location") ?? Str(row.Dict, "Location");
  201. if (string.IsNullOrWhiteSpace(loc))
  202. return;
  203. var incomingAt = ParseTime(row.SourceUpdatedAt);
  204. var existing = await _db.Queryable<AdoS0LocationMaster>()
  205. .Where(x => x.TenantId == tenantId && x.Location == loc)
  206. .FirstAsync(ct);
  207. if (existing != null && string.IsNullOrWhiteSpace(existing.SourceSystem))
  208. {
  209. await InsertConflictAsync(tenantId, "MDM_LOCATION", row.BizKey, sourceCode, row.RawJson, "LocationMaster", ct);
  210. return;
  211. }
  212. if (existing?.SourceUpdatedAt is { } oldAt && incomingAt is { } neu && oldAt > neu)
  213. return;
  214. var now = DateTime.Now;
  215. if (existing == null)
  216. {
  217. await _db.Insertable(new AdoS0LocationMaster
  218. {
  219. TenantId = tenantId,
  220. FactoryRefId = factoryId,
  221. DomainCode = Str(row.Dict, "Domain") ?? "",
  222. Location = loc,
  223. Descr = Str(row.Dict, "descr") ?? Str(row.Dict, "Descr"),
  224. SourceSystem = sourceCode,
  225. SourceUpdatedAt = incomingAt,
  226. CreateTime = now,
  227. UpdateTime = now
  228. }).ExecuteCommandAsync(ct);
  229. return;
  230. }
  231. existing.Descr = Str(row.Dict, "descr") ?? existing.Descr;
  232. existing.SourceSystem = sourceCode;
  233. existing.SourceUpdatedAt = incomingAt ?? existing.SourceUpdatedAt;
  234. existing.UpdateTime = now;
  235. await _db.Updateable(existing).ExecuteCommandAsync(ct);
  236. }
  237. private async Task MirrorEmployeeAsync(
  238. long tenantId, long factoryId, string sourceCode, MdpInboundPreparedRow row, CancellationToken ct)
  239. {
  240. var emp = Str(row.Dict, "Employee") ?? Str(row.Dict, "employee");
  241. if (string.IsNullOrWhiteSpace(emp))
  242. return;
  243. if (!string.IsNullOrWhiteSpace(Str(row.Dict, "Department")))
  244. await _refs.DepartmentExistsAsync(tenantId, Str(row.Dict, "Department"));
  245. var incomingAt = ParseTime(row.SourceUpdatedAt);
  246. var existing = await _db.Queryable<AdoS0EmployeeMaster>()
  247. .Where(x => x.TenantId == tenantId && x.Employee == emp)
  248. .FirstAsync(ct);
  249. if (existing != null && string.IsNullOrWhiteSpace(existing.SourceSystem))
  250. {
  251. await InsertConflictAsync(tenantId, "MDM_EMPLOYEE_HEADCOUNT", row.BizKey, sourceCode, row.RawJson, "EmployeeMaster", ct);
  252. return;
  253. }
  254. if (existing?.SourceUpdatedAt is { } oldAt && incomingAt is { } neu && oldAt > neu)
  255. return;
  256. var now = DateTime.Now;
  257. if (existing == null)
  258. {
  259. await _db.Insertable(new AdoS0EmployeeMaster
  260. {
  261. TenantId = tenantId,
  262. FactoryRefId = factoryId,
  263. DomainCode = Str(row.Dict, "Domain") ?? "",
  264. Employee = emp,
  265. Name = Str(row.Dict, "Name"),
  266. SourceSystem = sourceCode,
  267. SourceUpdatedAt = incomingAt,
  268. CreateTime = now,
  269. UpdateTime = now
  270. }).ExecuteCommandAsync(ct);
  271. return;
  272. }
  273. existing.Name = Str(row.Dict, "Name") ?? existing.Name;
  274. existing.SourceSystem = sourceCode;
  275. existing.SourceUpdatedAt = incomingAt ?? existing.SourceUpdatedAt;
  276. existing.UpdateTime = now;
  277. await _db.Updateable(existing).ExecuteCommandAsync(ct);
  278. }
  279. private async Task InsertConflictAsync(
  280. long tenantId, string entityCode, string bizKey, string sourceCode, string raw, string table, CancellationToken ct)
  281. {
  282. var now = DateTime.Now;
  283. await _db.Insertable(new MdpInboundConflict
  284. {
  285. TenantId = tenantId,
  286. EntityCode = entityCode,
  287. BizKey = bizKey ?? string.Empty,
  288. SourceSystem = sourceCode,
  289. IncomingRaw = raw ?? string.Empty,
  290. MirrorTable = table,
  291. ConflictType = "MANUAL_ROW_EXISTS",
  292. Status = "PENDING",
  293. CreateTime = now,
  294. UpdateTime = now
  295. }).ExecuteCommandAsync(ct);
  296. }
  297. private static string Str(IDictionary<string, object?> row, string key)
  298. {
  299. foreach (var kv in row)
  300. {
  301. if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))
  302. return kv.Value?.ToString();
  303. }
  304. return null;
  305. }
  306. private static DateTime? ParseTime(string raw) =>
  307. DateTimeOffset.TryParse(raw, out var dto) ? dto.LocalDateTime : null;
  308. private static bool IsInactive(string status) =>
  309. string.Equals(status, "INACTIVE", StringComparison.OrdinalIgnoreCase)
  310. || status == "0"
  311. || string.Equals(status, "false", StringComparison.OrdinalIgnoreCase);
  312. }