namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
///
/// C5 库存事务服务(服务化 pr_SFM_InventoryTransactionProcessing2)。
/// 职责边界:仅库存事务合法性(9 守卫) + 量变(calculator) + 流水;不含 C1/C2/C3/C4/C6/C7/Outbox/Domain 解析/Controller。
/// 流程:LoadContext → 9 守卫(失败=ValidationFailure,0 写) → BuildWriteSet → store.WriteAtomic(单事务,异常全回滚=Error)。
///
public sealed class InventoryTransactionService
{
private readonly IInventoryUnitOfWorkFactory _uowFactory;
public InventoryTransactionService(IInventoryUnitOfWorkFactory uowFactory)
{
_uowFactory = uowFactory ?? throw new ArgumentNullException(nameof(uowFactory));
}
public async Task PostAsync(IReadOnlyList cmds)
{
if (cmds == null) throw new ArgumentNullException(nameof(cmds));
if (cmds.Count == 0)
return new InventoryPostingResult { Outcome = InventoryPostingOutcome.Success, WrittenRows = 0, LegacyReturnMsg = "保存成功" };
// 独立入口:自建 UoW(锁边界)→ ExecuteWithinAsync(C5 内部规则)→ Commit。
await using var uow = _uowFactory.Create();
try
{
var ctx = await uow.LockAndLoadAsync(cmds);
var result = await ExecuteWithinAsync(ctx, cmds, ws => uow.WriteAsync(ws));
if (result.Outcome == InventoryPostingOutcome.Success)
await uow.CommitAsync();
else
await uow.RollbackAsync(); // 守卫失败 → 结束事务、0 写
return result;
}
catch (Exception ex)
{
try { await uow.RollbackAsync(); } catch { /* 回滚失败不掩盖原异常 */ }
return new InventoryPostingResult
{
Outcome = InventoryPostingOutcome.Error,
WrittenRows = 0,
ErrorCode = "INV_POST_FAILED",
ErrorMessage = ex.Message,
LegacyReturnMsg = "保存失败," + (ex.Message ?? string.Empty),
};
}
}
///
/// 组合事务入口:在**调用方已开事务/已锁定加载**的前提下执行 C5 内部规则——
/// **G1-G9 守卫(本服务拥有)+ Calculator + BuildWriteSet + 经 writeInventory 落写**;**不 Commit**。
/// 供 IqcReceiptOrchestrator 在外层事务内调用,避免 Orchestrator 直接构造写集绕过守卫。
/// LIVE 顺序:调用方先 CheckInv→C6,再调本方法(内部才跑 9 守卫),与 LIVE 一致。
///
public static async Task ExecuteWithinAsync(
InventoryContext lockedCtx, IReadOnlyList cmds, Func writeInventory)
{
if (lockedCtx == null) throw new ArgumentNullException(nameof(lockedCtx));
if (cmds == null) throw new ArgumentNullException(nameof(cmds));
if (writeInventory == null) throw new ArgumentNullException(nameof(writeInventory));
if (cmds.Count == 0)
return new InventoryPostingResult { Outcome = InventoryPostingOutcome.Success, WrittenRows = 0, LegacyReturnMsg = "保存成功" };
var errors = InventoryGuardValidator.Validate(cmds, lockedCtx); // ← G1-G9 归属 C5
if (errors.Count > 0)
return new InventoryPostingResult
{
Outcome = InventoryPostingOutcome.ValidationFailure,
GuardErrors = errors,
WrittenRows = 0,
ErrorCode = "INV_GUARD_" + (int)errors[0].Code,
ErrorMessage = errors[0].Message,
LegacyReturnMsg = errors[0].Message,
};
var writeSet = BuildWriteSet(cmds, lockedCtx);
await writeInventory(writeSet);
return new InventoryPostingResult
{
Outcome = InventoryPostingOutcome.Success,
WrittenRows = writeSet.TotalWrites,
LegacyReturnMsg = "保存成功",
};
}
/// 据命令 + 上下文计算 W1-W6 写集(纯,无副作用;可单测路由/量变)。
public static InventoryWriteSet BuildWriteSet(IReadOnlyList cmds, InventoryContext ctx)
{
if (cmds == null) throw new ArgumentNullException(nameof(cmds));
if (ctx == null) throw new ArgumentNullException(nameof(ctx));
var ws = new InventoryWriteSet();
// W1/W2 InvMaster:按 item+loc 聚合
foreach (var g in cmds.GroupBy(c => (c.ItemNum, c.Location)))
{
var delta = SumDeltas(g);
var before = ctx.GetInvMaster(g.Key.ItemNum, g.Key.Location) ?? new InventoryBalance();
var hasInbound = g.Any(c => c.ChangeQty > 0m);
ws.InvMasterWrites.Add(new InvMasterWrite
{
TenantId = g.First().TenantId,
DomainCode = g.First().DomainCode,
ItemNum = g.Key.ItemNum,
Location = g.Key.Location,
Delta = delta,
After = InventoryBalanceCalculator.Apply(before, delta),
InsertIfMissing = !ctx.HasInvMaster(g.Key.ItemNum, g.Key.Location) && hasInbound,
User = g.First().User,
});
}
// W3 ItemMaster.Location → ado_item_location_state:仅当当前默认库位为空、且有非空 Location 的命令(取首个)
foreach (var g in cmds.GroupBy(c => c.ItemNum))
{
if (!string.IsNullOrEmpty(ctx.GetItemDefaultLocation(g.Key))) continue;
var loc = g.Select(c => c.Location).FirstOrDefault(l => !string.IsNullOrEmpty(l));
if (string.IsNullOrEmpty(loc)) continue;
ws.ItemLocationWrites.Add(new ItemLocationWrite
{
TenantId = g.First().TenantId,
DomainCode = g.First().DomainCode,
ItemNum = g.Key,
DefaultLocation = loc,
User = g.First().User,
});
}
// W4/W5 LocationDetail:按 item+loc+lot+refs 聚合(写入维度始终含批次,与 LIVE 一致)
foreach (var g in cmds.GroupBy(c => (c.ItemNum, c.Location, Lot: c.LotSerial ?? "", Refs: c.Refs ?? "")))
{
var delta = SumDeltas(g);
var before = ctx.GetLocationDetail(g.Key.ItemNum, g.Key.Location, g.Key.Lot, g.Key.Refs) ?? new InventoryBalance();
var hasInbound = g.Any(c => c.ChangeQty > 0m);
ws.LocationDetailWrites.Add(new LocationDetailWrite
{
TenantId = g.First().TenantId,
DomainCode = g.First().DomainCode,
ItemNum = g.Key.ItemNum,
Location = g.Key.Location,
LotSerial = g.Key.Lot, // 空批空串(Phase 2 契约)
Refs = g.Key.Refs,
Delta = delta,
After = InventoryBalanceCalculator.Apply(before, delta),
InsertIfMissing = ctx.GetLocationDetail(g.Key.ItemNum, g.Key.Location, g.Key.Lot, g.Key.Refs) == null && hasInbound,
Cost = g.Max(c => c.Price),
Curr = g.Select(c => c.Curr).FirstOrDefault(x => !string.IsNullOrEmpty(x)),
User = g.First().User,
});
}
// W6 流水:每命令一行(按 Seq 稳定顺序),BeginBalance = 变动前 LocationDetail 在手
foreach (var c in cmds.OrderBy(c => c.Seq))
{
var d = InventoryBalanceCalculator.ComputeRowDeltas(c);
var beforeQoh = ctx.GetLocationDetail(c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? "")?.QtyOnHand ?? 0m;
var hist = InventoryBalanceCalculator.BuildTransHist(c, d, beforeQoh);
ws.TransactionRows.Add(new InventoryTransactionRow
{
TenantId = c.TenantId,
DomainCode = c.DomainCode,
PostingId = c.PostingId,
TransactionGroupId = c.TransactionGroupId,
Seq = c.Seq,
ItemNum = c.ItemNum,
Location = c.Location,
LotSerial = c.LotSerial ?? "",
TransType = hist.TransType,
QtyChange = hist.QtyChange,
BeginBalance = hist.BeginBalance,
FreezeQty = hist.FreezeQty,
Assay = hist.Assay,
Amt = hist.Amt,
WorkOrd = c.WorkOrd,
ShipperNum = c.ShipperNum,
Remark = c.Remark,
Fbillno = c.Fbillno,
Receiver = c.Receiver,
RctQcNbr = c.RctQcNbr,
User = c.User,
});
}
return ws;
}
private static InventoryDeltas SumDeltas(IEnumerable group)
{
var acc = new InventoryDeltas();
foreach (var c in group)
{
var d = InventoryBalanceCalculator.ComputeRowDeltas(c);
acc.Conv += d.Conv;
acc.DeltaQtyOnHand += d.DeltaQtyOnHand;
acc.DeltaAvailStatusQty += d.DeltaAvailStatusQty;
acc.DeltaAssay += d.DeltaAssay;
acc.DeltaFreezeQty += d.DeltaFreezeQty;
acc.DeltaQtyOnOrd += d.DeltaQtyOnOrd;
}
return acc;
}
}