using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory; namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt; public enum IqcReceiptOutcome { Success = 1, Skipped = 2, ValidationFailure = 3, Error = 4 } public sealed class IqcReceiptResult { public IqcReceiptOutcome Outcome { get; set; } public IReadOnlyList GuardErrors { get; set; } = System.Array.Empty(); public string Reason { get; set; } public int WrittenRows { get; set; } public string LegacyReturnMsg { get; set; } } /// /// C2 IQC 收货编排器(Phase 6B)。仅抽取 IQC `@IsProcCall=1` 可达路径,不翻译 4327 行 SP,不含 C3(不可达)/C7(后续)。 /// 事务所有权:**C2 拥有唯一外层事务**,C6/C5 同事务、同成同败;C5 不自 Commit。顺序遵 Phase 6A:CheckInv→C6→C5。 /// 9 守卫复用 ,C5 写集复用 (纯)。 /// public sealed class IqcReceiptOrchestrator { private readonly IIqcReceiptUnitOfWorkFactory _factory; private readonly Numbering.IBusinessNumberProvider _businessNumber; private readonly string _rctRuleCode; /// Phase 5B-3:新收货号取号 provider(生产=NumberRuleService,在外层事务内取号);null=沿用入参 RctNbr(单测)。 /// 收货号 NbrControl 规则码;与 businessNumber 同时提供才启用取号。 public IqcReceiptOrchestrator(IIqcReceiptUnitOfWorkFactory factory, Numbering.IBusinessNumberProvider businessNumber = null, string rctRuleCode = null) { _factory = factory ?? throw new ArgumentNullException(nameof(factory)); _businessNumber = businessNumber; _rctRuleCode = rctRuleCode; } /// C5 之后的下游业务写(MissedPrint 占位);抛异常触发整体回滚(事务组合验证)。 /// /// Phase 7C A5:**事务内**重读可变收货态的委托。非 null 时,在 BeginAndLock 之后用其结果重判 Runtime Gate、 /// 重建 C6 对账数量,最终过账决策以事务内新鲜数据为准(不再信事务外 snapshot)。返回 Found=false 或门禁不过 → 回滚 0 写。 /// public async Task PostAsync( IqcAcceptedReceipt receipt, PurchaseReceiptGateInput gate, Func postC5Hook = null, Func> reloadInTx = null) { if (receipt == null) throw new ArgumentNullException(nameof(receipt)); if (gate == null) throw new ArgumentNullException(nameof(gate)); // 事务外预门禁(快速失败,不起事务/不加锁;权威判定在事务内 reloadInTx) var g = PurchaseReceiptRuntimeGate.Evaluate(gate); if (!g.CanCreate) return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Skipped, Reason = g.Reason, WrittenRows = 0, LegacyReturnMsg = g.Reason }; var batch = IqcInventoryBatchBuilder.Build(receipt); // C6 对账数量(可变)——默认取入参;reloadInTx 命中时以事务内新鲜值覆盖 var orderedQty = receipt.OrderedQty; var beforeReceivedQty = receipt.BeforeReceivedQty; var returnedQty = receipt.ReturnedQty; var sampleQty = receipt.SampleQty; await using var uow = _factory.Create(); try { var ctx = await uow.BeginAndLockAsync(batch.Commands); // BEGIN + 锁 + 锁内快照 // Phase 7C A5:事务内重读可变收货态 → 用最新数据重判门禁、重建 C6(TOCTOU 收口) if (reloadInTx != null) { var fresh = await reloadInTx(); if (fresh == null || !fresh.Found) { await uow.RollbackAsync(); return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Skipped, Reason = "事务内收货态已不可用", WrittenRows = 0, LegacyReturnMsg = "事务内收货态已不可用" }; } var g2 = PurchaseReceiptRuntimeGate.Evaluate(new PurchaseReceiptGateInput { HasPendingReceipt = fresh.HasPendingReceipt, AcceptedQty = receipt.AcceptedQty, HasReceivableDetails = fresh.HasReceivableDetails, }); if (!g2.CanCreate) { await uow.RollbackAsync(); return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Skipped, Reason = g2.Reason, WrittenRows = 0, LegacyReturnMsg = g2.Reason }; } orderedQty = fresh.OrderedQty; beforeReceivedQty = fresh.BeforeReceivedQty; returnedQty = fresh.ReturnedQty; sampleQty = fresh.SampleQty; } // Phase 5B-3:新收货号在外层事务内取号(NumberRuleService FOR UPDATE 计数器;与库存写同事务) var rctNbr = receipt.RctNbr; if (_businessNumber != null && !string.IsNullOrWhiteSpace(_rctRuleCode)) { var nums = await _businessNumber.NextBatchAsync(_rctRuleCode, receipt.DomainCode, 1, receipt.User ?? string.Empty); if (nums == null || nums.Count == 0 || string.IsNullOrWhiteSpace(nums[0])) throw new InvalidOperationException("RctNbr 取号失败(provider 返回空)"); rctNbr = nums[0]; foreach (var c in batch.Commands) c.OrdNbr = rctNbr; // 新收货号回填流水 OrderNbr } // CheckInventoryStatus 轻校验(Location 非空;非 9 守卫替代) var chk = InventoryLocationCheck.Validate(batch.Commands); if (!chk.Ok) { await uow.RollbackAsync(); return new IqcReceiptResult { Outcome = IqcReceiptOutcome.ValidationFailure, Reason = chk.Message, WrittenRows = 0, LegacyReturnMsg = chk.Message }; } // C6 采购收货回写(shadow,同事务,在 C5 前)——用事务内新鲜对账数量 + 新收货号 await uow.WritePurchaseReceiptAsync(BuildC6(receipt, rctNbr, orderedQty, beforeReceivedQty, returnedQty, sampleQty)); // C5:经组合入口执行(G1-G9 + Calculator + Write 归 InventoryTransactionService;不 Commit) // Orchestrator 不直接构造写集,杜绝绕过守卫。 var c5 = await InventoryTransactionService.ExecuteWithinAsync(ctx, batch.Commands, ws => uow.WriteInventoryAsync(ws)); if (c5.Outcome != InventoryPostingOutcome.Success) { await uow.RollbackAsync(); // C5 守卫失败 → C6 一并回滚 return new IqcReceiptResult { Outcome = IqcReceiptOutcome.ValidationFailure, GuardErrors = c5.GuardErrors, WrittenRows = 0, Reason = c5.ErrorMessage, LegacyReturnMsg = c5.LegacyReturnMsg }; } // C7 移动任务(InvUpShelf 收货后待上架,TaskID=Receiver)——同外层事务 await uow.WriteMobileTaskAsync(MobileTaskBuilder.InvUpShelf(receipt)); // 下游(MissedPrint 等占位)——失败则整体回滚(C6/C5/C7 全滚) if (postC5Hook != null) await postC5Hook(); // Phase 7C A3:完成锚(与 C6/C5/C7 同事务提交)——闭合 commit→MarkPosted 崩溃窗口 await uow.WriteCompletionAsync(new IqcCompletionMarker { TenantId = receipt.TenantId, DomainCode = receipt.DomainCode, FbillNo = receipt.Fbillno, PostingId = receipt.PostingId, TransactionGroupId = receipt.TransactionGroupId, RouteKind = "FullReceipt", }); await uow.CommitAsync(); return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Success, WrittenRows = c5.WrittenRows + 2 /*C6 + C7*/, LegacyReturnMsg = "保存成功" }; } catch (Exception ex) { try { await uow.RollbackAsync(); } catch { /* 不掩盖原异常 */ } return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Error, WrittenRows = 0, Reason = ex.Message, LegacyReturnMsg = "保存失败," + (ex.Message ?? string.Empty) }; } } private static PurchaseReceiptWriteback BuildC6( IqcAcceptedReceipt r, string rctNbr, decimal orderedQty, decimal beforeReceivedQty, decimal returnedQty, decimal sampleQty) { // C6 对账量变(据 LIVE:RctQty += Accepted;完成 (收-退)>=订)——数量取事务内新鲜值,收货号用取号结果 var w = PurchaseReceiptWritebackCalculator.Compute(orderedQty, beforeReceivedQty, returnedQty, r.AcceptedQty); return new PurchaseReceiptWriteback { TenantId = r.TenantId, DomainCode = r.DomainCode, PostingId = r.PostingId, RctNbr = rctNbr, PurOrd = r.PurOrd ?? string.Empty, PurLine = r.PurLine, Potype = r.Potype, AcceptedQty = r.AcceptedQty, SampleQty = sampleQty, QcNbr = r.RctQcNbr, Receiver = r.Receiver, FbillNo = r.Fbillno, QcStatus = "YES", User = r.User, QtyOrdered = w.QtyOrdered, ReturnedQty = w.ReturnedQty, BeforeReceivedQty = w.BeforeReceivedQty, AfterReceivedQty = w.AfterReceivedQty, BeforeOutstandingQty = w.BeforeOutstandingQty, AfterOutstandingQty = w.AfterOutstandingQty, DetailCompleted = w.DetailCompleted, }; } }