MdmMirrorUpsertService.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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. }
  272. else
  273. {
  274. existing.Name = Str(row.Dict, "Name") ?? existing.Name;
  275. existing.SourceSystem = sourceCode;
  276. existing.SourceUpdatedAt = incomingAt ?? existing.SourceUpdatedAt;
  277. existing.UpdateTime = now;
  278. await _db.Updateable(existing).ExecuteCommandAsync(ct);
  279. }
  280. try
  281. {
  282. await _db.Ado.ExecuteCommandAsync(
  283. """
  284. INSERT INTO mdp_std_employee
  285. (tenant_id, factory_id, source_system, domain, employee_no, employee_name, department_code,
  286. position_code, src_position_raw, employment_status, src_employment_status_raw,
  287. source_row_id, source_biz_key, sync_batch_id, sync_time)
  288. VALUES
  289. (@tid, @factory, @src, @domain, @emp, @name, @dept,
  290. @pos, @posRaw, @status, @statusRaw,
  291. @emp, @biz, @batch, @now)
  292. ON DUPLICATE KEY UPDATE
  293. employee_name=VALUES(employee_name), department_code=VALUES(department_code),
  294. position_code=VALUES(position_code), src_position_raw=VALUES(src_position_raw),
  295. employment_status=VALUES(employment_status), src_employment_status_raw=VALUES(src_employment_status_raw),
  296. sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time)
  297. """,
  298. new SugarParameter("@tid", tenantId),
  299. new SugarParameter("@factory", factoryId),
  300. new SugarParameter("@src", string.IsNullOrWhiteSpace(sourceCode) ? "API" : sourceCode),
  301. new SugarParameter("@domain", Str(row.Dict, "Domain") ?? ""),
  302. new SugarParameter("@emp", emp),
  303. new SugarParameter("@name", Str(row.Dict, "Name")),
  304. new SugarParameter("@dept", Str(row.Dict, "Department")),
  305. new SugarParameter("@pos", NormalizePosition(Str(row.Dict, "Position"))),
  306. new SugarParameter("@posRaw", Str(row.Dict, "Position")),
  307. new SugarParameter("@status", NormalizeEmployment(Str(row.Dict, "EmploymentStatus"))),
  308. new SugarParameter("@statusRaw", Str(row.Dict, "EmploymentStatus")),
  309. new SugarParameter("@biz", $"{Str(row.Dict, "Domain")}:{emp}"),
  310. new SugarParameter("@batch", "INBOUND_EMP"),
  311. new SugarParameter("@now", now));
  312. }
  313. catch (Exception ex)
  314. {
  315. _logger.LogWarning(ex, "雇员中立层未就绪,已跳过 mdp_std_employee,镜像主数据仍已写入");
  316. }
  317. }
  318. private static string NormalizePosition(string? raw)
  319. {
  320. var v = (raw ?? string.Empty).Trim().ToUpperInvariant();
  321. return v is "PLANNER" or "WAREHOUSE" or "PRODUCTION" ? v : "OTHER";
  322. }
  323. private static string NormalizeEmployment(string? raw)
  324. {
  325. var v = (raw ?? string.Empty).Trim();
  326. var u = v.ToUpperInvariant();
  327. if (v is "在职" || u is "ACTIVE" or "ONJOB" or "ON_JOB") return "ACTIVE";
  328. if (v is "离职" or "辞退" || u is "LEFT" or "LEAVE" or "RESIGNED" or "TERMINATED" or "QUIT") return "LEFT";
  329. if (v is "停用" || u is "INACTIVE" or "DISABLED" or "SUSPENDED") return "INACTIVE";
  330. return "UNKNOWN";
  331. }
  332. private async Task InsertConflictAsync(
  333. long tenantId, string entityCode, string bizKey, string sourceCode, string raw, string table, CancellationToken ct)
  334. {
  335. var now = DateTime.Now;
  336. await _db.Insertable(new MdpInboundConflict
  337. {
  338. TenantId = tenantId,
  339. EntityCode = entityCode,
  340. BizKey = bizKey ?? string.Empty,
  341. SourceSystem = sourceCode,
  342. IncomingRaw = raw ?? string.Empty,
  343. MirrorTable = table,
  344. ConflictType = "MANUAL_ROW_EXISTS",
  345. Status = "PENDING",
  346. CreateTime = now,
  347. UpdateTime = now
  348. }).ExecuteCommandAsync(ct);
  349. }
  350. private static string Str(IDictionary<string, object?> row, string key)
  351. {
  352. foreach (var kv in row)
  353. {
  354. if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))
  355. return kv.Value?.ToString();
  356. }
  357. return null;
  358. }
  359. private static DateTime? ParseTime(string raw) =>
  360. DateTimeOffset.TryParse(raw, out var dto) ? dto.LocalDateTime : null;
  361. private static bool IsInactive(string status) =>
  362. string.Equals(status, "INACTIVE", StringComparison.OrdinalIgnoreCase)
  363. || status == "0"
  364. || string.Equals(status, "false", StringComparison.OrdinalIgnoreCase);
  365. }