| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- 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<InventoryGuardError> GuardErrors { get; set; } = System.Array.Empty<InventoryGuardError>();
- public string Reason { get; set; }
- public int WrittenRows { get; set; }
- public string LegacyReturnMsg { get; set; }
- }
- /// <summary>
- /// C2 IQC 收货编排器(Phase 6B)。仅抽取 IQC `@IsProcCall=1` 可达路径,不翻译 4327 行 SP,不含 C3(不可达)/C7(后续)。
- /// 事务所有权:**C2 拥有唯一外层事务**,C6/C5 同事务、同成同败;C5 不自 Commit。顺序遵 Phase 6A:CheckInv→C6→C5。
- /// 9 守卫复用 <see cref="InventoryGuardValidator"/>,C5 写集复用 <see cref="InventoryTransactionService.BuildWriteSet"/>(纯)。
- /// </summary>
- public sealed class IqcReceiptOrchestrator
- {
- private readonly IIqcReceiptUnitOfWorkFactory _factory;
- private readonly Numbering.IBusinessNumberProvider _businessNumber;
- private readonly string _rctRuleCode;
- /// <param name="businessNumber">Phase 5B-3:新收货号取号 provider(生产=NumberRuleService,在外层事务内取号);null=沿用入参 RctNbr(单测)。</param>
- /// <param name="rctRuleCode">收货号 NbrControl 规则码;与 businessNumber 同时提供才启用取号。</param>
- public IqcReceiptOrchestrator(IIqcReceiptUnitOfWorkFactory factory,
- Numbering.IBusinessNumberProvider businessNumber = null, string rctRuleCode = null)
- {
- _factory = factory ?? throw new ArgumentNullException(nameof(factory));
- _businessNumber = businessNumber;
- _rctRuleCode = rctRuleCode;
- }
- /// <param name="postC5Hook">C5 之后的下游业务写(MissedPrint 占位);抛异常触发整体回滚(事务组合验证)。</param>
- /// <param name="reloadInTx">
- /// Phase 7C A5:**事务内**重读可变收货态的委托。非 null 时,在 BeginAndLock 之后用其结果重判 Runtime Gate、
- /// 重建 C6 对账数量,最终过账决策以事务内新鲜数据为准(不再信事务外 snapshot)。返回 Found=false 或门禁不过 → 回滚 0 写。
- /// </param>
- public async Task<IqcReceiptResult> PostAsync(
- IqcAcceptedReceipt receipt, PurchaseReceiptGateInput gate, Func<Task> postC5Hook = null,
- Func<Task<ReceiptPostingRefresh>> 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,
- };
- }
- }
|