| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- 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>故障注入阶段(事务语义单测;任一阶段→WriteAsync 抛错→全回滚)。</summary>
- public enum FakeInventoryFailPhase { None, InvMaster, ItemLocation, LocationDetail, Transaction }
- /// <summary>
- /// 测试用共享库:backing 按 tenant|domain|… 键隔离;每键一把 SemaphoreSlim 模拟 FOR UPDATE 行锁串行。
- /// 用于「事务/锁设计语义单测」——非 MySQL 并发实证(Phase 5B)。
- /// </summary>
- public sealed class FakeInventoryDatabase
- {
- public Dictionary<string, InventoryBalance> InvMaster { get; } = new(StringComparer.Ordinal);
- public Dictionary<string, InventoryBalance> LocationDetail { get; } = new(StringComparer.Ordinal);
- public Dictionary<string, string> ItemLocation { get; } = new(StringComparer.Ordinal);
- public List<InventoryTransactionRow> Transactions { get; } = new();
- public HashSet<string> Items { get; } = new(StringComparer.Ordinal);
- public HashSet<string> Locations { get; } = new(StringComparer.Ordinal);
- /// <summary>C6 采购收货回写 shadow(键 tenant|domain|posting|purord|line)。</summary>
- public Dictionary<string, PurchaseReceiptWriteback> ReceiptWriteback { get; } = new(StringComparer.Ordinal);
- /// <summary>C7 移动任务 shadow(键 tenant|domain|task_id,模拟 TaskID upsert 幂等)。</summary>
- public Dictionary<string, MobileTaskCommand> MobileTasks { get; } = new(StringComparer.Ordinal);
- /// <summary>Phase 7C 完成锚 shadow(键 tenant|domain|fbillno;业务事务同提交,UNIQUE 幂等)。</summary>
- public Dictionary<string, IqcCompletionMarker> Completions { get; } = new(StringComparer.Ordinal);
- /// <summary>C3 排除锁:IQC 流程绝不写条码卡,本计数恒为 0。</summary>
- public int BarcodeCardWrites { get; set; }
- /// <summary>外层 UoW 调用顺序(断言 CheckInv→C6→C5:记 "C6"/"C5")。</summary>
- public List<string> CallOrder { get; } = new();
- private readonly object _gate = new();
- private readonly Dictionary<string, SemaphoreSlim> _locks = new(StringComparer.Ordinal);
- public SemaphoreSlim LockFor(string key)
- {
- lock (_gate)
- {
- if (!_locks.TryGetValue(key, out var s)) { s = new SemaphoreSlim(1, 1); _locks[key] = s; }
- return s;
- }
- }
- public int TotalPersisted => InvMaster.Count + LocationDetail.Count + ItemLocation.Count + Transactions.Count + ReceiptWriteback.Count + MobileTasks.Count;
- public static string ItemK(long t, string d, string item) => $"{t}|{d}|{item}";
- public static string LocK(long t, string d, string loc) => $"{t}|{d}|{loc}";
- public static string InvK(long t, string d, string item, string loc) => $"{t}|{d}|{item}|{loc}";
- public static string DetK(long t, string d, string item, string loc, string lot, string refs) => $"{t}|{d}|{item}|{loc}|{lot}|{refs}";
- public void SeedMaster(long t, string d, string item, string loc) { Items.Add(ItemK(t, d, item)); Locations.Add(LocK(t, d, loc)); }
- public void SeedStock(long t, string d, string item, string loc, decimal qty, string lot = "", string refs = "")
- {
- SeedMaster(t, d, item, loc);
- var bal = new InventoryBalance { QtyOnHand = qty, AvailStatusQty = qty, Assay = qty, FreezeQty = qty };
- InvMaster[InvK(t, d, item, loc)] = new InventoryBalance { QtyOnHand = qty, AvailStatusQty = qty, Assay = qty, FreezeQty = qty };
- LocationDetail[DetK(t, d, item, loc, lot, refs)] = bal;
- ItemLocation[ItemK(t, d, item)] = loc;
- }
- }
- /// <summary>
- /// 测试用库存工作单元:LockAndLoad 对本次命令涉及的自然键**加锁并读锁内最新值**(模拟 BeginTran+FOR UPDATE);
- /// 锁持有至 Commit/Rollback。Write 缓冲,Commit 才落 backing(全有或全无)。
- /// </summary>
- public sealed class FakeInventoryUnitOfWork : IInventoryUnitOfWork
- {
- private readonly FakeInventoryDatabase _db;
- private readonly FakeInventoryFailPhase _failAt;
- private readonly List<SemaphoreSlim> _held = new();
- private InventoryWriteSet _pending;
- private long _t;
- private string _d;
- private bool _completed;
- public FakeInventoryUnitOfWork(FakeInventoryDatabase db, FakeInventoryFailPhase failAt)
- {
- _db = db;
- _failAt = failAt;
- }
- public async Task<InventoryContext> LockAndLoadAsync(IReadOnlyList<InventoryTransactionCommand> cmds)
- {
- _t = cmds[0].TenantId;
- _d = cmds[0].DomainCode ?? string.Empty;
- // 按排序键加锁(一致顺序防死锁),锁住至 Commit/Rollback → 与真正 Write 同锁边界
- var keys = new SortedSet<string>(StringComparer.Ordinal);
- foreach (var c in cmds)
- {
- 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);
- }
- // 锁内快照(读 backing 最新值)
- var ctx = new InventoryContext();
- foreach (var item in cmds.Select(c => c.ItemNum).Distinct(StringComparer.Ordinal))
- if (_db.Items.Contains(FakeInventoryDatabase.ItemK(_t, _d, item))) ctx.Items.Add(item);
- foreach (var loc in cmds.Select(c => c.Location).Distinct(StringComparer.Ordinal))
- if (_db.Locations.Contains(FakeInventoryDatabase.LocK(_t, _d, loc))) ctx.Locations.Add(loc);
- foreach (var c in cmds)
- {
- 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 d))
- ctx.LocationDetails[InventoryContext.DetailKey(c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? "")] = Clone(d);
- if (_db.ItemLocation.TryGetValue(FakeInventoryDatabase.ItemK(_t, _d, c.ItemNum), out var loc))
- ctx.ItemDefaultLocation[c.ItemNum] = loc;
- }
- return ctx;
- }
- public Task WriteAsync(InventoryWriteSet writeSet)
- {
- if (_failAt != FakeInventoryFailPhase.None)
- throw new InvalidOperationException("inject-fail:" + _failAt);
- _pending = writeSet;
- return Task.CompletedTask;
- }
- public Task CommitAsync()
- {
- if (_completed) return Task.CompletedTask;
- _completed = true;
- if (_pending != null) Apply(_pending);
- ReleaseLocks();
- return Task.CompletedTask;
- }
- public Task RollbackAsync()
- {
- if (_completed) return Task.CompletedTask;
- _completed = true; // 丢弃 _pending
- ReleaseLocks();
- return Task.CompletedTask;
- }
- public ValueTask DisposeAsync()
- {
- if (!_completed) { _completed = true; ReleaseLocks(); }
- return default;
- }
- private void Apply(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 };
- }
- /// <summary>测试用工厂:每次 Create 返回新 UoW(共享同一 FakeInventoryDatabase)。</summary>
- public sealed class FakeInventoryUnitOfWorkFactory : IInventoryUnitOfWorkFactory
- {
- private readonly FakeInventoryDatabase _db;
- private readonly FakeInventoryFailPhase _failAt;
- public FakeInventoryUnitOfWorkFactory(FakeInventoryDatabase db, FakeInventoryFailPhase failAt = FakeInventoryFailPhase.None)
- { _db = db; _failAt = failAt; }
- public IInventoryUnitOfWork Create() => new FakeInventoryUnitOfWork(_db, _failAt);
- }
|