FakeIqcReceiptUnitOfWork.cs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
  2. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
  3. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  4. /// <summary>IQC 收货外层事务故障注入阶段(事务组合语义单测)。</summary>
  5. public enum FakeReceiptFailPhase { None, C6, C5, C7 }
  6. /// <summary>
  7. /// 测试用 IQC 收货外层工作单元:共享 FakeInventoryDatabase(每键 SemaphoreSlim 串行模拟 FOR UPDATE);
  8. /// C6/C5 写缓冲,Commit 才落 backing(全有或全无);可在 C6/C5 阶段注入失败。
  9. /// **事务组合语义单测,非 MySQL 实证(Phase 5B)。**
  10. /// </summary>
  11. public sealed class FakeIqcReceiptUnitOfWork : IIqcReceiptUnitOfWork
  12. {
  13. private readonly FakeInventoryDatabase _db;
  14. private readonly FakeReceiptFailPhase _failAt;
  15. private readonly List<SemaphoreSlim> _held = new();
  16. private long _t;
  17. private string _d;
  18. private PurchaseReceiptWriteback _pendingC6;
  19. private InventoryWriteSet _pendingWs;
  20. private MobileTaskCommand _pendingTask;
  21. private IqcCompletionMarker _pendingCompletion;
  22. private bool _completed;
  23. public FakeIqcReceiptUnitOfWork(FakeInventoryDatabase db, FakeReceiptFailPhase failAt)
  24. { _db = db; _failAt = failAt; }
  25. public async Task<InventoryContext> BeginAndLockAsync(IReadOnlyList<InventoryTransactionCommand> invCmds)
  26. {
  27. _t = invCmds[0].TenantId;
  28. _d = invCmds[0].DomainCode ?? string.Empty;
  29. var keys = new SortedSet<string>(StringComparer.Ordinal);
  30. foreach (var c in invCmds)
  31. {
  32. keys.Add("INV:" + FakeInventoryDatabase.InvK(_t, _d, c.ItemNum, c.Location));
  33. keys.Add("DET:" + FakeInventoryDatabase.DetK(_t, _d, c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? ""));
  34. }
  35. foreach (var k in keys) { var s = _db.LockFor(k); await s.WaitAsync(); _held.Add(s); }
  36. var ctx = new InventoryContext();
  37. foreach (var item in invCmds.Select(c => c.ItemNum).Distinct(StringComparer.Ordinal))
  38. if (_db.Items.Contains(FakeInventoryDatabase.ItemK(_t, _d, item))) ctx.Items.Add(item);
  39. foreach (var loc in invCmds.Select(c => c.Location).Distinct(StringComparer.Ordinal))
  40. if (_db.Locations.Contains(FakeInventoryDatabase.LocK(_t, _d, loc))) ctx.Locations.Add(loc);
  41. foreach (var c in invCmds)
  42. {
  43. if (_db.InvMaster.TryGetValue(FakeInventoryDatabase.InvK(_t, _d, c.ItemNum, c.Location), out var b))
  44. ctx.InvMasters[InventoryContext.InvKey(c.ItemNum, c.Location)] = Clone(b);
  45. if (_db.LocationDetail.TryGetValue(FakeInventoryDatabase.DetK(_t, _d, c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? ""), out var dd))
  46. ctx.LocationDetails[InventoryContext.DetailKey(c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? "")] = Clone(dd);
  47. if (_db.ItemLocation.TryGetValue(FakeInventoryDatabase.ItemK(_t, _d, c.ItemNum), out var loc))
  48. ctx.ItemDefaultLocation[c.ItemNum] = loc;
  49. }
  50. return ctx;
  51. }
  52. public Task BeginAsync(long tenantId, string domainCode)
  53. {
  54. _t = tenantId;
  55. _d = domainCode ?? string.Empty;
  56. return Task.CompletedTask; // 任务-only:仅开事务,无库存锁
  57. }
  58. public Task WritePurchaseReceiptAsync(PurchaseReceiptWriteback c6)
  59. {
  60. _db.CallOrder.Add("C6");
  61. if (_failAt == FakeReceiptFailPhase.C6) throw new InvalidOperationException("inject-fail:C6");
  62. _pendingC6 = c6;
  63. return Task.CompletedTask;
  64. }
  65. public Task WriteMobileTaskAsync(MobileTaskCommand task)
  66. {
  67. _db.CallOrder.Add("C7");
  68. if (_failAt == FakeReceiptFailPhase.C7) throw new InvalidOperationException("inject-fail:C7");
  69. _pendingTask = task;
  70. return Task.CompletedTask;
  71. }
  72. public Task WriteInventoryAsync(InventoryWriteSet writeSet)
  73. {
  74. _db.CallOrder.Add("C5");
  75. if (_failAt == FakeReceiptFailPhase.C5) throw new InvalidOperationException("inject-fail:C5");
  76. _pendingWs = writeSet;
  77. return Task.CompletedTask;
  78. }
  79. public Task WriteCompletionAsync(IqcCompletionMarker marker)
  80. {
  81. _db.CallOrder.Add("COMPLETE");
  82. _pendingCompletion = marker; // 缓冲,Commit 才落(与业务写同成同败)
  83. return Task.CompletedTask;
  84. }
  85. public Task CommitAsync()
  86. {
  87. if (_completed) return Task.CompletedTask;
  88. _completed = true;
  89. if (_pendingC6 != null)
  90. {
  91. var key = $"{_pendingC6.TenantId}|{_pendingC6.DomainCode}|{_pendingC6.PostingId}|{_pendingC6.PurOrd}|{_pendingC6.PurLine}";
  92. _db.ReceiptWriteback[key] = _pendingC6;
  93. }
  94. if (_pendingWs != null) ApplyInventory(_pendingWs);
  95. if (_pendingTask != null)
  96. _db.MobileTasks[$"{_pendingTask.TenantId}|{_pendingTask.DomainCode}|{_pendingTask.TaskId}"] = _pendingTask;
  97. if (_pendingCompletion != null)
  98. _db.Completions[$"{_pendingCompletion.TenantId}|{_pendingCompletion.DomainCode}|{_pendingCompletion.FbillNo}"] = _pendingCompletion;
  99. ReleaseLocks();
  100. return Task.CompletedTask;
  101. }
  102. public Task RollbackAsync()
  103. {
  104. if (_completed) return Task.CompletedTask;
  105. _completed = true; // 丢弃 _pendingC6 / _pendingWs
  106. ReleaseLocks();
  107. return Task.CompletedTask;
  108. }
  109. public ValueTask DisposeAsync()
  110. {
  111. if (!_completed) { _completed = true; ReleaseLocks(); }
  112. return default;
  113. }
  114. private void ApplyInventory(InventoryWriteSet ws)
  115. {
  116. foreach (var w in ws.InvMasterWrites)
  117. _db.InvMaster[FakeInventoryDatabase.InvK(w.TenantId, w.DomainCode ?? "", w.ItemNum, w.Location)] = w.After;
  118. foreach (var w in ws.ItemLocationWrites)
  119. _db.ItemLocation[FakeInventoryDatabase.ItemK(w.TenantId, w.DomainCode ?? "", w.ItemNum)] = w.DefaultLocation;
  120. foreach (var w in ws.LocationDetailWrites)
  121. _db.LocationDetail[FakeInventoryDatabase.DetK(w.TenantId, w.DomainCode ?? "", w.ItemNum, w.Location, w.LotSerial ?? "", w.Refs ?? "")] = w.After;
  122. foreach (var r in ws.TransactionRows)
  123. _db.Transactions.Add(r);
  124. }
  125. private void ReleaseLocks()
  126. {
  127. for (var i = _held.Count - 1; i >= 0; i--) _held[i].Release();
  128. _held.Clear();
  129. }
  130. private static InventoryBalance Clone(InventoryBalance b)
  131. => new InventoryBalance { QtyOnHand = b.QtyOnHand, AvailStatusQty = b.AvailStatusQty, Assay = b.Assay, FreezeQty = b.FreezeQty, QtyOnOrd = b.QtyOnOrd };
  132. }
  133. public sealed class FakeIqcReceiptUnitOfWorkFactory : IIqcReceiptUnitOfWorkFactory
  134. {
  135. private readonly FakeInventoryDatabase _db;
  136. private readonly FakeReceiptFailPhase _failAt;
  137. public FakeIqcReceiptUnitOfWorkFactory(FakeInventoryDatabase db, FakeReceiptFailPhase failAt = FakeReceiptFailPhase.None)
  138. { _db = db; _failAt = failAt; }
  139. public IIqcReceiptUnitOfWork Create() => new FakeIqcReceiptUnitOfWork(_db, _failAt);
  140. }