using System.Collections.Concurrent; using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Entity; namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency; /// /// 线程安全内存幂等存储——用于并发/单元测试。以 ConcurrentDictionary.TryAdd 模拟 /// (tenant_id, domain_code, fbillno) 的 DB UNIQUE 原子占位语义(TryAdd 原子,多并发仅一路成功)。 /// **不代表真实 MySQL 唯一约束**(那属 Phase 4B)。 /// public sealed class InMemoryIqcPostingStore : IIqcPostingStore { private readonly ConcurrentDictionary _store = new(StringComparer.Ordinal); private static string Key(long tenantId, string domainCode, string fbillNo) => $"{tenantId}|{domainCode ?? string.Empty}|{fbillNo ?? string.Empty}"; public Task TryInsertAsync(AdoIqcInventoryPosting posting) { if (posting == null) throw new ArgumentNullException(nameof(posting)); var key = Key(posting.TenantId, posting.DomainCode, posting.FbillNo); var inserted = _store.TryAdd(key, posting); return Task.FromResult(new PostingInsertResult { Inserted = inserted, Existing = inserted ? null : _store.TryGetValue(key, out var e) ? e : null, }); } public Task GetAsync(long tenantId, string domainCode, string fbillNo) { _store.TryGetValue(Key(tenantId, domainCode, fbillNo), out var e); return Task.FromResult(e); } public Task UpdateStatusAsync(AdoIqcInventoryPosting posting, string status) { if (posting == null) throw new ArgumentNullException(nameof(posting)); if (_store.TryGetValue(Key(posting.TenantId, posting.DomainCode, posting.FbillNo), out var e)) { lock (e) { e.PostingStatus = status; e.UpdateTime = DateTime.Now; } } posting.PostingStatus = status; return Task.CompletedTask; } /// 原子抢占:对既有记录取锁,校验 PROCESSING + stale 后刷新心跳。lock(e) 保证并发仅一路成功。 public Task TryRecoverStaleAsync(long tenantId, string domainCode, string fbillNo, DateTime staleBefore, DateTime now) { if (!_store.TryGetValue(Key(tenantId, domainCode, fbillNo), out var e)) return Task.FromResult(false); lock (e) { var status = (e.PostingStatus ?? string.Empty).Trim().ToUpperInvariant(); var heartbeat = e.UpdateTime ?? e.CreateTime; if (status == "PROCESSING" && heartbeat < staleBefore) { e.UpdateTime = now; // 续租=抢占 return Task.FromResult(true); } return Task.FromResult(false); } } }