IqcReceiptOrchestrator.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
  2. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
  3. public enum IqcReceiptOutcome { Success = 1, Skipped = 2, ValidationFailure = 3, Error = 4 }
  4. public sealed class IqcReceiptResult
  5. {
  6. public IqcReceiptOutcome Outcome { get; set; }
  7. public IReadOnlyList<InventoryGuardError> GuardErrors { get; set; } = System.Array.Empty<InventoryGuardError>();
  8. public string Reason { get; set; }
  9. public int WrittenRows { get; set; }
  10. public string LegacyReturnMsg { get; set; }
  11. }
  12. /// <summary>
  13. /// C2 IQC 收货编排器(Phase 6B)。仅抽取 IQC `@IsProcCall=1` 可达路径,不翻译 4327 行 SP,不含 C3(不可达)/C7(后续)。
  14. /// 事务所有权:**C2 拥有唯一外层事务**,C6/C5 同事务、同成同败;C5 不自 Commit。顺序遵 Phase 6A:CheckInv→C6→C5。
  15. /// 9 守卫复用 <see cref="InventoryGuardValidator"/>,C5 写集复用 <see cref="InventoryTransactionService.BuildWriteSet"/>(纯)。
  16. /// </summary>
  17. public sealed class IqcReceiptOrchestrator
  18. {
  19. private readonly IIqcReceiptUnitOfWorkFactory _factory;
  20. private readonly Numbering.IBusinessNumberProvider _businessNumber;
  21. private readonly string _rctRuleCode;
  22. /// <param name="businessNumber">Phase 5B-3:新收货号取号 provider(生产=NumberRuleService,在外层事务内取号);null=沿用入参 RctNbr(单测)。</param>
  23. /// <param name="rctRuleCode">收货号 NbrControl 规则码;与 businessNumber 同时提供才启用取号。</param>
  24. public IqcReceiptOrchestrator(IIqcReceiptUnitOfWorkFactory factory,
  25. Numbering.IBusinessNumberProvider businessNumber = null, string rctRuleCode = null)
  26. {
  27. _factory = factory ?? throw new ArgumentNullException(nameof(factory));
  28. _businessNumber = businessNumber;
  29. _rctRuleCode = rctRuleCode;
  30. }
  31. /// <param name="postC5Hook">C5 之后的下游业务写(MissedPrint 占位);抛异常触发整体回滚(事务组合验证)。</param>
  32. /// <param name="reloadInTx">
  33. /// Phase 7C A5:**事务内**重读可变收货态的委托。非 null 时,在 BeginAndLock 之后用其结果重判 Runtime Gate、
  34. /// 重建 C6 对账数量,最终过账决策以事务内新鲜数据为准(不再信事务外 snapshot)。返回 Found=false 或门禁不过 → 回滚 0 写。
  35. /// </param>
  36. public async Task<IqcReceiptResult> PostAsync(
  37. IqcAcceptedReceipt receipt, PurchaseReceiptGateInput gate, Func<Task> postC5Hook = null,
  38. Func<Task<ReceiptPostingRefresh>> reloadInTx = null)
  39. {
  40. if (receipt == null) throw new ArgumentNullException(nameof(receipt));
  41. if (gate == null) throw new ArgumentNullException(nameof(gate));
  42. // 事务外预门禁(快速失败,不起事务/不加锁;权威判定在事务内 reloadInTx)
  43. var g = PurchaseReceiptRuntimeGate.Evaluate(gate);
  44. if (!g.CanCreate)
  45. return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Skipped, Reason = g.Reason, WrittenRows = 0, LegacyReturnMsg = g.Reason };
  46. var batch = IqcInventoryBatchBuilder.Build(receipt);
  47. // C6 对账数量(可变)——默认取入参;reloadInTx 命中时以事务内新鲜值覆盖
  48. var orderedQty = receipt.OrderedQty;
  49. var beforeReceivedQty = receipt.BeforeReceivedQty;
  50. var returnedQty = receipt.ReturnedQty;
  51. var sampleQty = receipt.SampleQty;
  52. await using var uow = _factory.Create();
  53. try
  54. {
  55. var ctx = await uow.BeginAndLockAsync(batch.Commands); // BEGIN + 锁 + 锁内快照
  56. // Phase 7C A5:事务内重读可变收货态 → 用最新数据重判门禁、重建 C6(TOCTOU 收口)
  57. if (reloadInTx != null)
  58. {
  59. var fresh = await reloadInTx();
  60. if (fresh == null || !fresh.Found)
  61. {
  62. await uow.RollbackAsync();
  63. return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Skipped, Reason = "事务内收货态已不可用", WrittenRows = 0, LegacyReturnMsg = "事务内收货态已不可用" };
  64. }
  65. var g2 = PurchaseReceiptRuntimeGate.Evaluate(new PurchaseReceiptGateInput
  66. {
  67. HasPendingReceipt = fresh.HasPendingReceipt,
  68. AcceptedQty = receipt.AcceptedQty,
  69. HasReceivableDetails = fresh.HasReceivableDetails,
  70. });
  71. if (!g2.CanCreate)
  72. {
  73. await uow.RollbackAsync();
  74. return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Skipped, Reason = g2.Reason, WrittenRows = 0, LegacyReturnMsg = g2.Reason };
  75. }
  76. orderedQty = fresh.OrderedQty;
  77. beforeReceivedQty = fresh.BeforeReceivedQty;
  78. returnedQty = fresh.ReturnedQty;
  79. sampleQty = fresh.SampleQty;
  80. }
  81. // Phase 5B-3:新收货号在外层事务内取号(NumberRuleService FOR UPDATE 计数器;与库存写同事务)
  82. var rctNbr = receipt.RctNbr;
  83. if (_businessNumber != null && !string.IsNullOrWhiteSpace(_rctRuleCode))
  84. {
  85. var nums = await _businessNumber.NextBatchAsync(_rctRuleCode, receipt.DomainCode, 1, receipt.User ?? string.Empty);
  86. if (nums == null || nums.Count == 0 || string.IsNullOrWhiteSpace(nums[0]))
  87. throw new InvalidOperationException("RctNbr 取号失败(provider 返回空)");
  88. rctNbr = nums[0];
  89. foreach (var c in batch.Commands) c.OrdNbr = rctNbr; // 新收货号回填流水 OrderNbr
  90. }
  91. // CheckInventoryStatus 轻校验(Location 非空;非 9 守卫替代)
  92. var chk = InventoryLocationCheck.Validate(batch.Commands);
  93. if (!chk.Ok)
  94. {
  95. await uow.RollbackAsync();
  96. return new IqcReceiptResult { Outcome = IqcReceiptOutcome.ValidationFailure, Reason = chk.Message, WrittenRows = 0, LegacyReturnMsg = chk.Message };
  97. }
  98. // C6 采购收货回写(shadow,同事务,在 C5 前)——用事务内新鲜对账数量 + 新收货号
  99. await uow.WritePurchaseReceiptAsync(BuildC6(receipt, rctNbr, orderedQty, beforeReceivedQty, returnedQty, sampleQty));
  100. // C5:经组合入口执行(G1-G9 + Calculator + Write 归 InventoryTransactionService;不 Commit)
  101. // Orchestrator 不直接构造写集,杜绝绕过守卫。
  102. var c5 = await InventoryTransactionService.ExecuteWithinAsync(ctx, batch.Commands, ws => uow.WriteInventoryAsync(ws));
  103. if (c5.Outcome != InventoryPostingOutcome.Success)
  104. {
  105. await uow.RollbackAsync(); // C5 守卫失败 → C6 一并回滚
  106. return new IqcReceiptResult { Outcome = IqcReceiptOutcome.ValidationFailure, GuardErrors = c5.GuardErrors, WrittenRows = 0, Reason = c5.ErrorMessage, LegacyReturnMsg = c5.LegacyReturnMsg };
  107. }
  108. // C7 移动任务(InvUpShelf 收货后待上架,TaskID=Receiver)——同外层事务
  109. await uow.WriteMobileTaskAsync(MobileTaskBuilder.InvUpShelf(receipt));
  110. // 下游(MissedPrint 等占位)——失败则整体回滚(C6/C5/C7 全滚)
  111. if (postC5Hook != null) await postC5Hook();
  112. // Phase 7C A3:完成锚(与 C6/C5/C7 同事务提交)——闭合 commit→MarkPosted 崩溃窗口
  113. await uow.WriteCompletionAsync(new IqcCompletionMarker
  114. {
  115. TenantId = receipt.TenantId, DomainCode = receipt.DomainCode, FbillNo = receipt.Fbillno,
  116. PostingId = receipt.PostingId, TransactionGroupId = receipt.TransactionGroupId, RouteKind = "FullReceipt",
  117. });
  118. await uow.CommitAsync();
  119. return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Success, WrittenRows = c5.WrittenRows + 2 /*C6 + C7*/, LegacyReturnMsg = "保存成功" };
  120. }
  121. catch (Exception ex)
  122. {
  123. try { await uow.RollbackAsync(); } catch { /* 不掩盖原异常 */ }
  124. return new IqcReceiptResult { Outcome = IqcReceiptOutcome.Error, WrittenRows = 0, Reason = ex.Message, LegacyReturnMsg = "保存失败," + (ex.Message ?? string.Empty) };
  125. }
  126. }
  127. private static PurchaseReceiptWriteback BuildC6(
  128. IqcAcceptedReceipt r, string rctNbr, decimal orderedQty, decimal beforeReceivedQty, decimal returnedQty, decimal sampleQty)
  129. {
  130. // C6 对账量变(据 LIVE:RctQty += Accepted;完成 (收-退)>=订)——数量取事务内新鲜值,收货号用取号结果
  131. var w = PurchaseReceiptWritebackCalculator.Compute(orderedQty, beforeReceivedQty, returnedQty, r.AcceptedQty);
  132. return new PurchaseReceiptWriteback
  133. {
  134. TenantId = r.TenantId, DomainCode = r.DomainCode, PostingId = r.PostingId, RctNbr = rctNbr,
  135. PurOrd = r.PurOrd ?? string.Empty, PurLine = r.PurLine, Potype = r.Potype,
  136. AcceptedQty = r.AcceptedQty, SampleQty = sampleQty,
  137. QcNbr = r.RctQcNbr, Receiver = r.Receiver, FbillNo = r.Fbillno, QcStatus = "YES", User = r.User,
  138. QtyOrdered = w.QtyOrdered, ReturnedQty = w.ReturnedQty,
  139. BeforeReceivedQty = w.BeforeReceivedQty, AfterReceivedQty = w.AfterReceivedQty,
  140. BeforeOutstandingQty = w.BeforeOutstandingQty, AfterOutstandingQty = w.AfterOutstandingQty,
  141. DetailCompleted = w.DetailCompleted,
  142. };
  143. }
  144. }