| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
- namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
- /// <summary>IQC 收货外层事务故障注入阶段(事务组合语义单测)。</summary>
- public enum FakeReceiptFailPhase { None, C6, C5, C7 }
- /// <summary>
- /// 测试用 IQC 收货外层工作单元:共享 FakeInventoryDatabase(每键 SemaphoreSlim 串行模拟 FOR UPDATE);
- /// C6/C5 写缓冲,Commit 才落 backing(全有或全无);可在 C6/C5 阶段注入失败。
- /// **事务组合语义单测,非 MySQL 实证(Phase 5B)。**
- /// </summary>
- public sealed class FakeIqcReceiptUnitOfWork : IIqcReceiptUnitOfWork
- {
- private readonly FakeInventoryDatabase _db;
- private readonly FakeReceiptFailPhase _failAt;
- private readonly List<SemaphoreSlim> _held = new();
- private long _t;
- private string _d;
- private PurchaseReceiptWriteback _pendingC6;
- private InventoryWriteSet _pendingWs;
- private MobileTaskCommand _pendingTask;
- private IqcCompletionMarker _pendingCompletion;
- private bool _completed;
- public FakeIqcReceiptUnitOfWork(FakeInventoryDatabase db, FakeReceiptFailPhase failAt)
- { _db = db; _failAt = failAt; }
- public async Task<InventoryContext> BeginAndLockAsync(IReadOnlyList<InventoryTransactionCommand> invCmds)
- {
- _t = invCmds[0].TenantId;
- _d = invCmds[0].DomainCode ?? string.Empty;
- var keys = new SortedSet<string>(StringComparer.Ordinal);
- foreach (var c in invCmds)
- {
- keys.Add("INV:" + FakeInventoryDatabase.InvK(_t, _d, c.ItemNum, c.Location));
- keys.Add("DET:" + FakeInventoryDatabase.DetK(_t, _d, c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? ""));
- }
- foreach (var k in keys) { var s = _db.LockFor(k); await s.WaitAsync(); _held.Add(s); }
- var ctx = new InventoryContext();
- foreach (var item in invCmds.Select(c => c.ItemNum).Distinct(StringComparer.Ordinal))
- if (_db.Items.Contains(FakeInventoryDatabase.ItemK(_t, _d, item))) ctx.Items.Add(item);
- foreach (var loc in invCmds.Select(c => c.Location).Distinct(StringComparer.Ordinal))
- if (_db.Locations.Contains(FakeInventoryDatabase.LocK(_t, _d, loc))) ctx.Locations.Add(loc);
- foreach (var c in invCmds)
- {
- if (_db.InvMaster.TryGetValue(FakeInventoryDatabase.InvK(_t, _d, c.ItemNum, c.Location), out var b))
- ctx.InvMasters[InventoryContext.InvKey(c.ItemNum, c.Location)] = Clone(b);
- if (_db.LocationDetail.TryGetValue(FakeInventoryDatabase.DetK(_t, _d, c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? ""), out var dd))
- ctx.LocationDetails[InventoryContext.DetailKey(c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? "")] = Clone(dd);
- if (_db.ItemLocation.TryGetValue(FakeInventoryDatabase.ItemK(_t, _d, c.ItemNum), out var loc))
- ctx.ItemDefaultLocation[c.ItemNum] = loc;
- }
- return ctx;
- }
- public Task BeginAsync(long tenantId, string domainCode)
- {
- _t = tenantId;
- _d = domainCode ?? string.Empty;
- return Task.CompletedTask; // 任务-only:仅开事务,无库存锁
- }
- public Task WritePurchaseReceiptAsync(PurchaseReceiptWriteback c6)
- {
- _db.CallOrder.Add("C6");
- if (_failAt == FakeReceiptFailPhase.C6) throw new InvalidOperationException("inject-fail:C6");
- _pendingC6 = c6;
- return Task.CompletedTask;
- }
- public Task WriteMobileTaskAsync(MobileTaskCommand task)
- {
- _db.CallOrder.Add("C7");
- if (_failAt == FakeReceiptFailPhase.C7) throw new InvalidOperationException("inject-fail:C7");
- _pendingTask = task;
- return Task.CompletedTask;
- }
- public Task WriteInventoryAsync(InventoryWriteSet writeSet)
- {
- _db.CallOrder.Add("C5");
- if (_failAt == FakeReceiptFailPhase.C5) throw new InvalidOperationException("inject-fail:C5");
- _pendingWs = writeSet;
- return Task.CompletedTask;
- }
- public Task WriteCompletionAsync(IqcCompletionMarker marker)
- {
- _db.CallOrder.Add("COMPLETE");
- _pendingCompletion = marker; // 缓冲,Commit 才落(与业务写同成同败)
- return Task.CompletedTask;
- }
- public Task CommitAsync()
- {
- if (_completed) return Task.CompletedTask;
- _completed = true;
- if (_pendingC6 != null)
- {
- var key = $"{_pendingC6.TenantId}|{_pendingC6.DomainCode}|{_pendingC6.PostingId}|{_pendingC6.PurOrd}|{_pendingC6.PurLine}";
- _db.ReceiptWriteback[key] = _pendingC6;
- }
- if (_pendingWs != null) ApplyInventory(_pendingWs);
- if (_pendingTask != null)
- _db.MobileTasks[$"{_pendingTask.TenantId}|{_pendingTask.DomainCode}|{_pendingTask.TaskId}"] = _pendingTask;
- if (_pendingCompletion != null)
- _db.Completions[$"{_pendingCompletion.TenantId}|{_pendingCompletion.DomainCode}|{_pendingCompletion.FbillNo}"] = _pendingCompletion;
- ReleaseLocks();
- return Task.CompletedTask;
- }
- public Task RollbackAsync()
- {
- if (_completed) return Task.CompletedTask;
- _completed = true; // 丢弃 _pendingC6 / _pendingWs
- ReleaseLocks();
- return Task.CompletedTask;
- }
- public ValueTask DisposeAsync()
- {
- if (!_completed) { _completed = true; ReleaseLocks(); }
- return default;
- }
- private void ApplyInventory(InventoryWriteSet ws)
- {
- foreach (var w in ws.InvMasterWrites)
- _db.InvMaster[FakeInventoryDatabase.InvK(w.TenantId, w.DomainCode ?? "", w.ItemNum, w.Location)] = w.After;
- foreach (var w in ws.ItemLocationWrites)
- _db.ItemLocation[FakeInventoryDatabase.ItemK(w.TenantId, w.DomainCode ?? "", w.ItemNum)] = w.DefaultLocation;
- foreach (var w in ws.LocationDetailWrites)
- _db.LocationDetail[FakeInventoryDatabase.DetK(w.TenantId, w.DomainCode ?? "", w.ItemNum, w.Location, w.LotSerial ?? "", w.Refs ?? "")] = w.After;
- foreach (var r in ws.TransactionRows)
- _db.Transactions.Add(r);
- }
- private void ReleaseLocks()
- {
- for (var i = _held.Count - 1; i >= 0; i--) _held[i].Release();
- _held.Clear();
- }
- private static InventoryBalance Clone(InventoryBalance b)
- => new InventoryBalance { QtyOnHand = b.QtyOnHand, AvailStatusQty = b.AvailStatusQty, Assay = b.Assay, FreezeQty = b.FreezeQty, QtyOnOrd = b.QtyOnOrd };
- }
- public sealed class FakeIqcReceiptUnitOfWorkFactory : IIqcReceiptUnitOfWorkFactory
- {
- private readonly FakeInventoryDatabase _db;
- private readonly FakeReceiptFailPhase _failAt;
- public FakeIqcReceiptUnitOfWorkFactory(FakeInventoryDatabase db, FakeReceiptFailPhase failAt = FakeReceiptFailPhase.None)
- { _db = db; _failAt = failAt; }
- public IIqcReceiptUnitOfWork Create() => new FakeIqcReceiptUnitOfWork(_db, _failAt);
- }
|