InMemoryIqcPostingStore.cs 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. using System.Collections.Concurrent;
  2. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Entity;
  3. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency;
  4. /// <summary>
  5. /// 线程安全内存幂等存储——用于并发/单元测试。以 ConcurrentDictionary.TryAdd 模拟
  6. /// (tenant_id, domain_code, fbillno) 的 DB UNIQUE 原子占位语义(TryAdd 原子,多并发仅一路成功)。
  7. /// **不代表真实 MySQL 唯一约束**(那属 Phase 4B)。
  8. /// </summary>
  9. public sealed class InMemoryIqcPostingStore : IIqcPostingStore
  10. {
  11. private readonly ConcurrentDictionary<string, AdoIqcInventoryPosting> _store =
  12. new(StringComparer.Ordinal);
  13. private static string Key(long tenantId, string domainCode, string fbillNo)
  14. => $"{tenantId}|{domainCode ?? string.Empty}|{fbillNo ?? string.Empty}";
  15. public Task<PostingInsertResult> TryInsertAsync(AdoIqcInventoryPosting posting)
  16. {
  17. if (posting == null) throw new ArgumentNullException(nameof(posting));
  18. var key = Key(posting.TenantId, posting.DomainCode, posting.FbillNo);
  19. var inserted = _store.TryAdd(key, posting);
  20. return Task.FromResult(new PostingInsertResult
  21. {
  22. Inserted = inserted,
  23. Existing = inserted ? null : _store.TryGetValue(key, out var e) ? e : null,
  24. });
  25. }
  26. public Task<AdoIqcInventoryPosting> GetAsync(long tenantId, string domainCode, string fbillNo)
  27. {
  28. _store.TryGetValue(Key(tenantId, domainCode, fbillNo), out var e);
  29. return Task.FromResult(e);
  30. }
  31. public Task UpdateStatusAsync(AdoIqcInventoryPosting posting, string status)
  32. {
  33. if (posting == null) throw new ArgumentNullException(nameof(posting));
  34. if (_store.TryGetValue(Key(posting.TenantId, posting.DomainCode, posting.FbillNo), out var e))
  35. {
  36. lock (e) { e.PostingStatus = status; e.UpdateTime = DateTime.Now; }
  37. }
  38. posting.PostingStatus = status;
  39. return Task.CompletedTask;
  40. }
  41. /// <summary>原子抢占:对既有记录取锁,校验 PROCESSING + stale 后刷新心跳。lock(e) 保证并发仅一路成功。</summary>
  42. public Task<bool> TryRecoverStaleAsync(long tenantId, string domainCode, string fbillNo, DateTime staleBefore, DateTime now)
  43. {
  44. if (!_store.TryGetValue(Key(tenantId, domainCode, fbillNo), out var e))
  45. return Task.FromResult(false);
  46. lock (e)
  47. {
  48. var status = (e.PostingStatus ?? string.Empty).Trim().ToUpperInvariant();
  49. var heartbeat = e.UpdateTime ?? e.CreateTime;
  50. if (status == "PROCESSING" && heartbeat < staleBefore)
  51. {
  52. e.UpdateTime = now; // 续租=抢占
  53. return Task.FromResult(true);
  54. }
  55. return Task.FromResult(false);
  56. }
  57. }
  58. }