SqlSugarIqcPostingStore.cs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Entity;
  2. using SqlSugar;
  3. using Yitter.IdGenerator;
  4. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency;
  5. /// <summary>
  6. /// 生产 IQC 过账头幂等存储:依赖 `ado_iqc_inventory_posting` 的 DB UNIQUE `uk_ado_iqc_posting_fbillno`
  7. /// (tenant_id, domain_code, fbillno) 原子占位——**INSERT 直上,撞唯一约束(1062)时回读既有记录**,不做先查后插。
  8. ///
  9. /// ⚠️ Phase 4B(未在本轮实库验证):目标库 = aidopdev(ConfigId 1300000000001);DB UNIQUE 实际拒绝与真实并发行为待补。
  10. /// </summary>
  11. public sealed class SqlSugarIqcPostingStore : IIqcPostingStore, ITransient
  12. {
  13. private readonly ISqlSugarClient _db;
  14. public SqlSugarIqcPostingStore(ISqlSugarClient db)
  15. {
  16. _db = db;
  17. }
  18. public async Task<PostingInsertResult> TryInsertAsync(AdoIqcInventoryPosting posting)
  19. {
  20. if (posting == null) throw new ArgumentNullException(nameof(posting));
  21. if (posting.Id == 0) posting.Id = YitIdHelper.NextId();
  22. if (posting.CreateTime == default) posting.CreateTime = DateTime.Now;
  23. if (posting.UpdateTime == null) posting.UpdateTime = posting.CreateTime; // 认领即起租(lease 心跳基准)
  24. try
  25. {
  26. await _db.Insertable(posting).ExecuteCommandAsync();
  27. return new PostingInsertResult { Inserted = true };
  28. }
  29. catch (Exception ex) when (IsDuplicateKey(ex))
  30. {
  31. var existing = await GetAsync(posting.TenantId, posting.DomainCode, posting.FbillNo);
  32. return new PostingInsertResult { Inserted = false, Existing = existing };
  33. }
  34. }
  35. public async Task<AdoIqcInventoryPosting> GetAsync(long tenantId, string domainCode, string fbillNo)
  36. {
  37. var domain = domainCode ?? string.Empty;
  38. return await _db.Queryable<AdoIqcInventoryPosting>()
  39. .Where(x => x.TenantId == tenantId && x.DomainCode == domain && x.FbillNo == fbillNo)
  40. .FirstAsync();
  41. }
  42. public async Task UpdateStatusAsync(AdoIqcInventoryPosting posting, string status)
  43. {
  44. if (posting == null) throw new ArgumentNullException(nameof(posting));
  45. await _db.Ado.ExecuteCommandAsync(
  46. "UPDATE ado_iqc_inventory_posting SET posting_status=@st, update_time=@now WHERE tenant_id=@t AND domain_code=@d AND fbillno=@fb",
  47. new SugarParameter("@st", status), new SugarParameter("@now", DateTime.Now),
  48. new SugarParameter("@t", posting.TenantId), new SugarParameter("@d", posting.DomainCode ?? ""),
  49. new SugarParameter("@fb", posting.FbillNo));
  50. posting.PostingStatus = status;
  51. }
  52. public async Task<bool> TryRecoverStaleAsync(long tenantId, string domainCode, string fbillNo, DateTime staleBefore, DateTime now)
  53. {
  54. // 单条件原子更新:仅 PROCESSING 且心跳过期者被抢占(rows=1);并发多路仅一路命中,无先查后改窗口。
  55. var rows = await _db.Ado.ExecuteCommandAsync(
  56. "UPDATE ado_iqc_inventory_posting SET update_time=@now " +
  57. "WHERE tenant_id=@t AND domain_code=@d AND fbillno=@fb AND posting_status='PROCESSING' AND update_time < @stale",
  58. new SugarParameter("@now", now), new SugarParameter("@t", tenantId),
  59. new SugarParameter("@d", domainCode ?? ""), new SugarParameter("@fb", fbillNo),
  60. new SugarParameter("@stale", staleBefore));
  61. return rows == 1;
  62. }
  63. /// <summary>沿异常链识别 MySQL 唯一键冲突(1062 / Duplicate entry / 目标唯一索引名)。</summary>
  64. private static bool IsDuplicateKey(Exception ex)
  65. {
  66. for (var e = ex; e != null; e = e.InnerException)
  67. {
  68. var m = e.Message;
  69. if (!string.IsNullOrEmpty(m) &&
  70. (m.Contains("Duplicate entry") || m.Contains("1062") || m.Contains("uk_ado_iqc_posting_fbillno")))
  71. return true;
  72. }
  73. return false;
  74. }
  75. }