| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
- /// <summary>
- /// C5 库存事务服务(服务化 pr_SFM_InventoryTransactionProcessing2)。
- /// 职责边界:仅库存事务合法性(9 守卫) + 量变(calculator) + 流水;不含 C1/C2/C3/C4/C6/C7/Outbox/Domain 解析/Controller。
- /// 流程:LoadContext → 9 守卫(失败=ValidationFailure,0 写) → BuildWriteSet → store.WriteAtomic(单事务,异常全回滚=Error)。
- /// </summary>
- public sealed class InventoryTransactionService
- {
- private readonly IInventoryUnitOfWorkFactory _uowFactory;
- public InventoryTransactionService(IInventoryUnitOfWorkFactory uowFactory)
- {
- _uowFactory = uowFactory ?? throw new ArgumentNullException(nameof(uowFactory));
- }
- public async Task<InventoryPostingResult> PostAsync(IReadOnlyList<InventoryTransactionCommand> 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),
- };
- }
- }
- /// <summary>
- /// 组合事务入口:在**调用方已开事务/已锁定加载**的前提下执行 C5 内部规则——
- /// **G1-G9 守卫(本服务拥有)+ Calculator + BuildWriteSet + 经 writeInventory 落写**;**不 Commit**。
- /// 供 IqcReceiptOrchestrator 在外层事务内调用,避免 Orchestrator 直接构造写集绕过守卫。
- /// LIVE 顺序:调用方先 CheckInv→C6,再调本方法(内部才跑 9 守卫),与 LIVE 一致。
- /// </summary>
- public static async Task<InventoryPostingResult> ExecuteWithinAsync(
- InventoryContext lockedCtx, IReadOnlyList<InventoryTransactionCommand> cmds, Func<InventoryWriteSet, Task> 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 = "保存成功",
- };
- }
- /// <summary>据命令 + 上下文计算 W1-W6 写集(纯,无副作用;可单测路由/量变)。</summary>
- public static InventoryWriteSet BuildWriteSet(IReadOnlyList<InventoryTransactionCommand> 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<InventoryTransactionCommand> 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;
- }
- }
|