| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677 |
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Entity;
- namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency;
- /// <summary>
- /// IQC 入库过账幂等服务。幂等键 = tenant_id + domain_code + FBILLNO(DB UNIQUE 兜底,见 <see cref="AdoIqcInventoryPosting"/>)。
- /// **禁止**用 FINSPECTSTATUS='检验完成' 判断是否已入库;**禁止**先查后插(有并发窗口)。
- /// 认领语义:INSERT 原子占位(成功=FirstProcess);撞唯一约束→回读既有→按 posting_status 分类。
- /// 注意:Phase 4 仅建立幂等认领;真正的库存过账动作在 Phase 5。
- /// </summary>
- public sealed class IqcPostingIdempotencyService
- {
- private readonly IIqcPostingStore _store;
- public IqcPostingIdempotencyService(IIqcPostingStore store)
- {
- _store = store ?? throw new ArgumentNullException(nameof(store));
- }
- /// <summary>
- /// 认领一次 IQC 入库过账。<paramref name="init"/> 可在插入前补齐过账头字段(receiver/rct_qc_nbr/branch_no/mrb_status 等)。
- /// </summary>
- public async Task<IqcPostingClaimResult> ClaimAsync(
- long tenantId, string domainCode, string fbillNo, Action<AdoIqcInventoryPosting> init = null)
- {
- if (string.IsNullOrWhiteSpace(fbillNo))
- throw new ArgumentException("FBILLNO 不能为空", nameof(fbillNo));
- var posting = new AdoIqcInventoryPosting
- {
- TenantId = tenantId,
- DomainCode = domainCode ?? string.Empty,
- FbillNo = fbillNo,
- PostingStatus = "PROCESSING",
- PostingMode = "SHADOW",
- CreateTime = DateTime.Now,
- };
- posting.UpdateTime = posting.CreateTime; // 认领即起租(lease 心跳基准,供 stale 恢复)
- init?.Invoke(posting);
- var ins = await _store.TryInsertAsync(posting);
- if (ins.Inserted)
- return new IqcPostingClaimResult { Outcome = IqcPostingClaimOutcome.FirstProcess, Posting = posting, IsNew = true };
- var existing = ins.Existing ?? await _store.GetAsync(tenantId, domainCode ?? string.Empty, fbillNo);
- return new IqcPostingClaimResult { Outcome = Classify(existing?.PostingStatus), Posting = existing, IsNew = false };
- }
- /// <summary>业务提交成功后标记过账完成(POSTED)——L1 consume 端在业务事务 Commit 成功后调用(ack)。</summary>
- public Task MarkPostedAsync(AdoIqcInventoryPosting posting) => _store.UpdateStatusAsync(posting, "POSTED");
- /// <summary>业务失败后标记 FAILED(可重试)——保持事件 retryable,不 ack。</summary>
- public Task MarkFailedAsync(AdoIqcInventoryPosting posting) => _store.UpdateStatusAsync(posting, "FAILED");
- /// <summary>
- /// 尝试原子抢占 stale PROCESSING(Phase 7C):lease 已过期才抢占成功(rows=1→true)。
- /// 并发多路仅一路 true,用于 stale Processing 恢复(避免永久 skip)。
- /// </summary>
- public Task<bool> TryRecoverStaleAsync(long tenantId, string domainCode, string fbillNo, TimeSpan lease)
- {
- var now = DateTime.Now;
- return _store.TryRecoverStaleAsync(tenantId, domainCode ?? string.Empty, fbillNo, now - lease, now);
- }
- /// <summary>按既有过账头状态分类认领结果。未知态保守视为 Processing(不重复过账)。</summary>
- public static IqcPostingClaimOutcome Classify(string postingStatus)
- {
- switch ((postingStatus ?? string.Empty).Trim().ToUpperInvariant())
- {
- case "POSTED": return IqcPostingClaimOutcome.AlreadyProcessed;
- case "FAILED": return IqcPostingClaimOutcome.FailedRetryable;
- case "PROCESSING":
- case "PENDING": return IqcPostingClaimOutcome.Processing;
- default: return IqcPostingClaimOutcome.Processing;
- }
- }
- }
|