Просмотр исходного кода

fix(s5): write IQC bill completion status on flow end | server 1.0.472

P1-A:流程走完后 qms_qcp_inspbill.FBILLSTATUS 仍停留「检验中」。

根因不是遗漏而是当时的显式设计:IqcInspBillFlowBizHandler 三个回调全为 no-op,
注释写明「流程结束回调:本批 no-op(不写 qms/FBILLSTATUS/...)。审核结果由
ApprovalFlowInstance 状态承载」。故 FBILLSTATUS 全仓只有 generate 时的一次
硬编码 '检验中',无任何 UPDATE。

修复
- 实现 IqcInspBillFlowBizHandler.OnFlowCompleted:仅当 finalStatus=Approved 时
  写 FBILLSTATUS='检验完成',按 id + tenant_id 限定。
- 写在流程结束回调而非 Service:该回调由引擎在实例真正终结时触发且仅一次,
  合格走 GW_PD 默认边直达 end、不合格必须走完 N3 SQE 才 end,
  故「NG 单在主管通过时提前写完成」结构上不可能发生,无需在 Service 判断网关走向。
- 事务:InvokeHandler 捕获后 rethrow,而 supervisor-approve / sqe-submit-disposition
  均以 UseTranAsync 包住 Approve,本次 UPDATE 与流程推进落在同一事务。
- 取值 '检验完成' 复用项目既有 done-word,与同一 INSERT 写的 '检验中' 同词族;
  库内 C/A/COMPLETED/CREATED/NG 均为无代码写入者的历史 seed,不采用,也不自造新枚举。

明确不改
- 报检分录 FINSPECTSTATUS:其既定写入者是 IqcWmsResultPushService 的 165 推送路径,
  语义为「已提交并回写 WMS」,与本字段的「审批流走完」不同轴;当前不回写是 P2 箱码
  fixture 缺口所致,留待 P2 一并处理。
- 不写 FAUDITORID、不碰库存、不改 pd/clfs、不动 detail 接口(仍返回 raw 状态)。

验证
- build 0 error;契约测试 8 条 + Resolver 26 条共 34/34 PASS。
- 运行态(PASS 路径):A01 新单 IQC202609030002 经 认领 → 来料检验 → 录入合格 100/0
  → 提交 → 主管通过,实例 Approved/end,FBILLSTATUS 由「检验中」变为「检验完成」,
  页面「单据状态」同步显示检验完成。
- NG 路径无可用运行对象(A04 已终结且禁止 reset),以契约测试固化「仅 Approved 才写」
  「Service 侧不得写 FBILLSTATUS」两条不变量覆盖,运行态缺口已记录。
- 回归:IqcTaskEntryService / IqcInspBillFlowService / IqcWmsResultPushService /
  IqcInspBillService / Resolver / 桥表服务 / IqcResultMapping 均零改动;
  B-2 检规快照 U01-1-63 / A.1 / 2021.07.13 未受影响。
YY968XX 1 день назад
Родитель
Сommit
1f2f8b4794

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.471</AssemblyVersion>
-    <FileVersion>1.0.471</FileVersion>
-    <Version>1.0.471</Version>
+    <AssemblyVersion>1.0.472</AssemblyVersion>
+    <FileVersion>1.0.472</FileVersion>
+    <Version>1.0.472</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 124 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S5/MaterialWarehouse/IqcBillStatusCompletionContractTests.cs

@@ -0,0 +1,124 @@
+using Xunit;
+
+namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
+
+/// <summary>
+/// P1-A 源码契约测试:qms_qcp_inspbill.FBILLSTATUS 完成态回写。
+///
+/// 固化四条不变量:
+///   ① 终态只由流程结束回调 OnFlowCompleted 写,且仅 Approved 才写;
+///   ② NG 单不得在主管通过(N2)时提前写完成 —— 靠「只在实例终结回调里写」结构性保证,
+///      故 supervisor-approve / sqe-submit-disposition 所在的 Service 内不得出现 FBILLSTATUS 写入;
+///   ③ 取值复用既有 done-word '检验完成',不自造新枚举、不采用无写入者的 COMPLETED/C/A/NG;
+///   ④ 回写必须租户隔离,且不越界改 pd/clfs/FINSPECTSTATUS。
+/// </summary>
+public class IqcBillStatusCompletionContractTests
+{
+    private static string Handler() => File.ReadAllText(FindFile(
+        "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "IqcInspBillFlowBizHandler.cs"));
+
+    private static string FlowService() => File.ReadAllText(FindFile(
+        "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "IqcInspBillFlowService.cs"));
+
+    private static string TaskEntryService() => File.ReadAllText(FindFile(
+        "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "IqcTaskEntryService.cs"));
+
+    private static string InspBillService() => File.ReadAllText(FindFile(
+        "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "IqcInspBillService.cs"));
+
+    // ── ① 终态只在 OnFlowCompleted 写,且仅 Approved ──
+    [Fact]
+    public void Handler_WritesBillStatus_OnlyOnApprovedCompletion()
+    {
+        var s = Handler();
+        Assert.Contains("public async Task OnFlowCompleted", s);
+        Assert.Contains("if (finalStatus != FlowInstanceStatusEnum.Approved) return;", s);
+        Assert.Contains("UPDATE qms_qcp_inspbill SET FBILLSTATUS=@Status", s);
+    }
+
+    // ── ② NG 不得提前完成:Service 侧不得出现 FBILLSTATUS 写入 ──
+    [Fact]
+    public void FlowService_DoesNotWriteBillStatus()
+    {
+        var s = FlowService();
+        Assert.DoesNotContain("SET FBILLSTATUS", s);
+        Assert.DoesNotContain("FBILLSTATUS=", s);
+        // submit-result 仍只写这四列
+        Assert.Contains("UPDATE qms_qcp_inspbill SET pd=@pd, dhsl=@dhsl, bhgsl=@bhgsl, clfs=@clfs", s);
+    }
+
+    /// <summary>生成时仍写 '检验中',是唯一的初始态写入点。</summary>
+    [Fact]
+    public void GenerateInspBill_StillWritesInProgressStatus()
+    {
+        var s = TaskEntryService();
+        Assert.Contains("'检验中'", s);
+        Assert.DoesNotContain("SET FBILLSTATUS", s);
+    }
+
+    // ── ③ 取值复用既有 done-word,不自造枚举 ──
+    [Fact]
+    public void BillStatusDone_ReusesExistingVocabulary()
+    {
+        var s = Handler();
+        Assert.Contains("public const string BillStatusDone = \"检验完成\";", s);
+        Assert.Contains("IqcInspBillFlowConst.BillStatusDone", s);
+        // 不得引入库内那些无代码写入者的历史 seed 值
+        Assert.DoesNotContain("\"COMPLETED\"", s);
+        Assert.DoesNotContain("\"CREATED\"", s);
+    }
+
+    // ── ④ 租户隔离 + 不越界 ──
+    [Fact]
+    public void Handler_CompletionWrite_IsTenantScoped()
+    {
+        var s = Handler();
+        Assert.Contains("WHERE id=@id AND tenant_id=@TenantId", s);
+        Assert.Contains("_userManager.TenantId", s);
+    }
+
+    [Fact]
+    public void Handler_DoesNotTouchOtherBusinessFields()
+    {
+        var s = Handler();
+        // 不改判定/处理方式/审核人/报检分录状态
+        Assert.DoesNotContain("SET pd=", s);
+        Assert.DoesNotContain("SET clfs=", s);
+        Assert.DoesNotContain("FAUDITORID=", s);
+        Assert.DoesNotContain("qms_qcp_insappnentry", s);
+    }
+
+    /// <summary>详情接口仍原样返回 FBILLSTATUS,不得在 GET 侧动态伪造完成态。</summary>
+    [Fact]
+    public void DetailApi_ReturnsRawBillStatus_NoDerivedCompletion()
+    {
+        var s = InspBillService();
+        Assert.Contains("ib.FBILLSTATUS     AS BillStatus", s);
+        Assert.DoesNotContain("检验完成", s);
+    }
+
+    /// <summary>B-2 检规快照逻辑未被本次修改波及。</summary>
+    [Fact]
+    public void SpecSnapshotColumns_StillWrittenOnlyAtGeneration()
+    {
+        var gen = TaskEntryService();
+        Assert.Contains("jgbh, jgbb, jgsxrq, jygfid", gen);
+        Assert.Contains("spec.IsMatched", gen);
+
+        var handler = Handler();
+        Assert.DoesNotContain("jgbh", handler);
+        Assert.DoesNotContain("jygfid", handler);
+    }
+
+    private static string FindFile(params string[] parts)
+    {
+        var dir = new DirectoryInfo(AppContext.BaseDirectory);
+        while (dir != null)
+        {
+            var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
+            if (File.Exists(candidate)) return candidate;
+            dir = dir.Parent;
+        }
+        throw new FileNotFoundException(string.Join("/", parts));
+    }
+}

+ 44 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/IqcInspBillFlowBizHandler.cs

@@ -47,6 +47,12 @@ public static class IqcInspBillFlowConst
     /// <summary>UAT 冒烟绑定账号(同绑两角色,仅 UAT 自审)</summary>
     public const string UatAccount = "superAdmin.NET";
 
+    /// <summary>
+    /// 检验单业务状态 qms_qcp_inspbill.FBILLSTATUS 的终态值(P1-A)。
+    /// 生成时由 IqcTaskEntryService 写 '检验中',流程 Approved 终结时由 Handler 写本值。
+    /// </summary>
+    public const string BillStatusDone = "检验完成";
+
     // pd 判定:0=合格 1=不合格 | clfs 处理方式:0=让步接收 1=挑选 2=报废
 }
 
@@ -76,8 +82,44 @@ public class IqcInspBillFlowBizHandler : IFlowBizHandler, ITransient
     /// <summary>单节点完成回调:本批 no-op。</summary>
     public Task OnNodeCompleted(long bizId, long instanceId, string nodeId, string nodeName, long? approverUserId) => Task.CompletedTask;
 
-    /// <summary>流程结束回调:本批 no-op(不写 qms/FBILLSTATUS/FAUDITORID/库存)。审核结果由 ApprovalFlowInstance 状态承载。</summary>
-    public Task OnFlowCompleted(long bizId, long instanceId, FlowInstanceStatusEnum finalStatus, long? lastApproverId) => Task.CompletedTask;
+    /// <summary>
+    /// 流程结束回调:把检验单业务状态推进到终态(P1-A)。
+    ///
+    /// 【为什么写在这里而不是 IqcInspBillFlowService】
+    /// 本回调由流程引擎在**实例真正终结**时触发且仅触发一次,天然覆盖两条路径:
+    ///   · 合格(pd=0):N2 主管通过 → GW_PD 默认边 → end → 本回调;
+    ///   · 不合格(pd=1):N2 主管通过 → GW_PD 走 N3_SQE_DISPOSITION(实例仍 Running,**不触发本回调**)
+    ///                   → SQE 处置完成 → end → 本回调。
+    /// 即「NG 单在主管通过时提前写完成」在结构上不可能发生,无需在 Service 里判断网关走向。
+    ///
+    /// 【事务】FlowEngineService.InvokeHandler 捕获后 rethrow,而 supervisor-approve /
+    /// sqe-submit-disposition 均以 _db.AsTenant().UseTranAsync 包住 _flowEngine.Approve,
+    /// 故本次 UPDATE 与流程推进落在同一事务:不会出现「流程已完成但业务单状态没写成功」。
+    ///
+    /// 【取值】'检验完成' 与同一条 INSERT 写入的 '检验中'(IqcTaskEntryService 生成时)同词族,
+    /// 也是项目既有 done-word(参见 FqcInspBillFlowBizHandler.InspectStatusDone)。
+    /// 库内 FBILLSTATUS 另有 C/A/COMPLETED/CREATED/NG 等历史 seed 值,无代码写入者,不予采用,
+    /// 亦不自造新枚举。
+    ///
+    /// 【边界】只在 Approved(正常走完)时写;Rejected / Cancelled 保持原状态,不写完成。
+    /// 不写 FAUDITORID、不碰库存、不改 pd/clfs、不动报检分录 FINSPECTSTATUS
+    /// (后者的既定写入者是 IqcWmsResultPushService 的 165 推送路径,语义为「已提交并回写 WMS」,
+    ///  与本字段的「审批流走完」不是同一语义轴)。
+    /// </summary>
+    public async Task OnFlowCompleted(long bizId, long instanceId, FlowInstanceStatusEnum finalStatus, long? lastApproverId)
+    {
+        if (finalStatus != FlowInstanceStatusEnum.Approved) return;
+
+        // 同库分租户:本回调在请求上下文内同步调用,租户取当前登录用户,绝不跨租户改单。
+        await _db.Ado.ExecuteCommandAsync(
+            "UPDATE qms_qcp_inspbill SET FBILLSTATUS=@Status WHERE id=@id AND tenant_id=@TenantId",
+            new List<SugarParameter>
+            {
+                new("@Status", IqcInspBillFlowConst.BillStatusDone),
+                new("@id", bizId),
+                new("@TenantId", _userManager.TenantId),
+            });
+    }
 
     /// <summary>返回检验单基础信息(本批线性流程无网关条件,仅供日志/展示)。</summary>
     public async Task<Dictionary<string, object>> GetBizData(long bizId)