using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
/// 故障注入阶段(事务语义单测;任一阶段→WriteAsync 抛错→全回滚)。
public enum FakeInventoryFailPhase { None, InvMaster, ItemLocation, LocationDetail, Transaction }
///
/// 测试用共享库:backing 按 tenant|domain|… 键隔离;每键一把 SemaphoreSlim 模拟 FOR UPDATE 行锁串行。
/// 用于「事务/锁设计语义单测」——非 MySQL 并发实证(Phase 5B)。
///
public sealed class FakeInventoryDatabase
{
public Dictionary InvMaster { get; } = new(StringComparer.Ordinal);
public Dictionary LocationDetail { get; } = new(StringComparer.Ordinal);
public Dictionary ItemLocation { get; } = new(StringComparer.Ordinal);
public List Transactions { get; } = new();
public HashSet Items { get; } = new(StringComparer.Ordinal);
public HashSet Locations { get; } = new(StringComparer.Ordinal);
/// C6 采购收货回写 shadow(键 tenant|domain|posting|purord|line)。
public Dictionary ReceiptWriteback { get; } = new(StringComparer.Ordinal);
/// C7 移动任务 shadow(键 tenant|domain|task_id,模拟 TaskID upsert 幂等)。
public Dictionary MobileTasks { get; } = new(StringComparer.Ordinal);
/// Phase 7C 完成锚 shadow(键 tenant|domain|fbillno;业务事务同提交,UNIQUE 幂等)。
public Dictionary Completions { get; } = new(StringComparer.Ordinal);
/// C3 排除锁:IQC 流程绝不写条码卡,本计数恒为 0。
public int BarcodeCardWrites { get; set; }
/// 外层 UoW 调用顺序(断言 CheckInv→C6→C5:记 "C6"/"C5")。
public List CallOrder { get; } = new();
private readonly object _gate = new();
private readonly Dictionary _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;
}
}
///
/// 测试用库存工作单元:LockAndLoad 对本次命令涉及的自然键**加锁并读锁内最新值**(模拟 BeginTran+FOR UPDATE);
/// 锁持有至 Commit/Rollback。Write 缓冲,Commit 才落 backing(全有或全无)。
///
public sealed class FakeInventoryUnitOfWork : IInventoryUnitOfWork
{
private readonly FakeInventoryDatabase _db;
private readonly FakeInventoryFailPhase _failAt;
private readonly List _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 LockAndLoadAsync(IReadOnlyList cmds)
{
_t = cmds[0].TenantId;
_d = cmds[0].DomainCode ?? string.Empty;
// 按排序键加锁(一致顺序防死锁),锁住至 Commit/Rollback → 与真正 Write 同锁边界
var keys = new SortedSet(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 };
}
/// 测试用工厂:每次 Create 返回新 UoW(共享同一 FakeInventoryDatabase)。
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);
}