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

fix(s7): FQC生成检验单即起流 + N1待办归认领人(发起人本人)

Phase 1G:把 FQC 审批流起流点从提交前移到生成检验单,待办严格落认领人。
- FqcTaskEntryService.GenerateInspBill:建单后 EnsureFlowAsync 幂等起流(无实例则起、
  有则复用),StartFlow 用 UseTran 包裹(镜像 submit-result)保证 实例+N1 任务原子创建,
  N1建失败则回滚为可恢复的有单无流;Claim/Assign 加起流后负责人冻结(EnsureNotLocked)
- FqcInspBillFlowSeed:N1 审批人 Role→Initiator(发起人本人=认领人),幂等升 v3
- FqcInspBillFlowService.SubmitResult:加负责人业务门(jyfzr==UserId);移除 jyfzr 被
  姓名覆写(保持 SysUser.Id 语义)
- FqcInspectionService.AutoJudge:补负责人门(独立公开写口,与 SaveDetail 一致)
- FqcInspBillFlowBizHandler:FlowDefinitionVersion 2→3(文档标记)

实测: 认领人生成→InitiatorId=N1 AssigneeId=jyfzr 三者一致;保存不推进/提交推进N2;
非负责人提交拒;重复生成幂等;孤儿单 generate/submit 双路恢复;起流后 Claim/Assign 冻结。

chore: bump version server 1.0.375
YY968XX 1 день назад
Родитель
Сommit
5b8adbf166

+ 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.374</AssemblyVersion>
-    <FileVersion>1.0.374</FileVersion>
-    <Version>1.0.374</Version>
+    <AssemblyVersion>1.0.375</AssemblyVersion>
+    <FileVersion>1.0.375</FileVersion>
+    <Version>1.0.375</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 2 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillFlowBizHandler.cs

@@ -43,8 +43,8 @@ public static class FqcInspBillFlowConst
     /// <summary>QE 角色固定 Id</summary>
     public const long RoleQeId = 1329915020003L;
 
-    /// <summary>流程定义版本(v2 加入 GW_RESULT/N3_QE_DISPOSITION;seed 幂等升级依据)</summary>
-    public const int FlowDefinitionVersion = 2;
+    /// <summary>流程定义版本(v2 加入 GW_RESULT/N3_QE_DISPOSITION;v3 N1 审批人改 Initiator 发起人本人=认领人;seed 幂等升级依据)</summary>
+    public const int FlowDefinitionVersion = 3;
 
     /// <summary>UAT 冒烟绑定账号(同绑三角色,仅 UAT 自审)</summary>
     public const string UatAccount = "superAdmin.NET";

+ 15 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillFlowService.cs

@@ -78,7 +78,18 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
         if (inst != null && inst.Status != FlowInstanceStatusEnum.Running)
             throw Oops.Oh("该检验单流程已结束,无法再提交检验结果");
 
-        var inspectorName = _userManager.RealName ?? _userManager.Account;
+        // 负责人业务门:当前用户必须是该检验单负责人(qms_fqcbj.jyfzr==UserId)。
+        // Flow 引擎只校 N1 Assignee,业务层再校 jyfzr → 双重保证 jyfzr==当前用户==N1 Assignee 三者一致。
+        var gateTenantId = ResolveTenantOrThrow();
+        if (!string.IsNullOrWhiteSpace(sourceBillNo))
+        {
+            var jyfzrOwner = await _db.Ado.SqlQuerySingleAsync<string>(
+                "SELECT jyfzr FROM qms_fqcbj WHERE FBILLNO=@bjbh AND tenant_id=@t LIMIT 1",
+                new SugarParameter("@bjbh", sourceBillNo), new SugarParameter("@t", gateTenantId));
+            if (!string.Equals((jyfzrOwner ?? "").Trim(), _userManager.UserId.ToString(), StringComparison.Ordinal))
+                throw Oops.Oh("当前用户不是该检验单负责人,无权提交检验结果");
+        }
+
         var now = DateTime.Now;
 
         var tran = await _db.AsTenant().UseTranAsync(async () =>
@@ -127,11 +138,12 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
                     throw Oops.Oh($"检验任务数据异常:来源单号 {sourceBillNo} 匹配到 {taskCount} 条报检任务(qms_fqcbj),无法安全更新,已整体回滚");
                 if (taskCount == 1)
                 {
+                    // 不覆写 jyfzr:保持 Claim/Assign 设定的 SysUser.Id 语义,禁止被姓名污染(负责人门/待办均按 id 比对)。
+                    // 仅更新任务进度(开始/完成时间、检验完成状态);如需展示姓名由 SysUser join 解析。
                     await _db.Ado.ExecuteCommandAsync(
-                        "UPDATE qms_fqcbj SET jyfzr=@jyfzr, jykssj=IFNULL(jykssj,@now), jywcsj=@now, FINSPECTSTATUS=@status WHERE FBILLNO=@bjbh AND tenant_id=@TenantFilter",
+                        "UPDATE qms_fqcbj SET jykssj=IFNULL(jykssj,@now), jywcsj=@now, FINSPECTSTATUS=@status WHERE FBILLNO=@bjbh AND tenant_id=@TenantFilter",
                         new List<SugarParameter>
                         {
-                            new("@jyfzr", inspectorName),
                             new("@now", now),
                             new("@status", FqcInspBillFlowConst.InspectStatusDone),
                             new("@bjbh", sourceBillNo),

+ 9 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspectionService.cs

@@ -165,6 +165,15 @@ public class FqcInspectionService : IDynamicApiController, ITransient
         var tenantId = ResolveTenantOrThrow();
         if (input == null || input.InspBillId <= 0) throw Oops.Oh("检验单 id 非法");
 
+        // 负责人门:仅本人(jyfzr==当前账号)可自动判定(独立公开写口,与 SaveDetail 一致,防越权改他人检验结果)
+        var owner = await _db.Ado.SqlQuerySingleAsync<string>(
+            @"SELECT q.jyfzr FROM qms_qcpp_inspbill i
+              JOIN qms_fqcbj q ON q.FBILLNO=i.lydjbh AND q.id=i.hid AND q.tenant_id=i.tenant_id
+              WHERE i.id=@id AND i.tenant_id=@t LIMIT 1",
+            new SugarParameter("@id", input.InspBillId), new SugarParameter("@t", tenantId));
+        if (!string.Equals((owner ?? "").Trim(), _userManager.UserId.ToString(), StringComparison.Ordinal))
+            throw Oops.Oh("检验负责人与当前账号不一致,无法自动判定");
+
         var jSel = string.Join(",", Enumerable.Range(1, SampleCols).Select(i => $"j{i}"));
         var raw = await _db.Ado.SqlQueryAsync<dynamic>(
             $"SELECT id, jyxm, IFNULL(lrlx,'') lrlx, IFNULL(sx,'') sx, IFNULL(xx,'') xx, {jSel} FROM qms_qcpp_inspbillst WHERE glid=@g AND tenant_id=@t",

+ 57 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcTaskEntryService.cs

@@ -1,5 +1,7 @@
 using System.ComponentModel;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
 
 namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
 
@@ -17,12 +19,14 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
     private readonly ISqlSugarClient _db;
     private readonly UserManager _userManager;
     private readonly FqcApplyService _fqcApply;
+    private readonly FlowEngineService _flowEngine;
 
-    public FqcTaskEntryService(ISqlSugarClient db, UserManager userManager, FqcApplyService fqcApply)
+    public FqcTaskEntryService(ISqlSugarClient db, UserManager userManager, FqcApplyService fqcApply, FlowEngineService flowEngine)
     {
         _db = db;
         _userManager = userManager;
         _fqcApply = fqcApply;
+        _flowEngine = flowEngine;
     }
 
     private long ResolveTenantOrThrow() => AidopTenantScope.ResolveOrThrow(_userManager);
@@ -151,6 +155,7 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
     {
         var ids = NormalizeIds(input?.Ids);
         var tenantId = ResolveTenantOrThrow();
+        await EnsureNotLockedAsync(ids, tenantId); // G4:起流后冻结负责人
         var owner = _userManager.UserId.ToString();
         var n = await _db.Ado.ExecuteCommandAsync(
             $"UPDATE qms_fqcbj SET jyfzr = @Owner WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})",
@@ -167,6 +172,7 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
         var ids = NormalizeIds(input?.Ids);
         if (input == null || input.InspectorUserId <= 0) throw Oops.Oh("请指定检验负责人");
         var tenantId = ResolveTenantOrThrow();
+        await EnsureNotLockedAsync(ids, tenantId); // G4:起流后冻结负责人
         var owner = input.InspectorUserId.ToString();
         var n = await _db.Ado.ExecuteCommandAsync(
             $"UPDATE qms_fqcbj SET jyfzr = @Owner WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})",
@@ -225,9 +231,45 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
             "SELECT id AS Id, FBILLNO AS BillNo FROM qms_qcpp_inspbill WHERE lydjbh=@no AND hid=@Id AND tenant_id=@t LIMIT 1",
             new SugarParameter("@no", apply.BillNo), new SugarParameter("@Id", input.EntryId), new SugarParameter("@t", tenantId));
         if (bill == null) throw Oops.Oh("检验单生成后未能回读,请刷新重试");
+
+        // EnsureFlow:建单后幂等起流(无实例则起、有则复用)。检验单已 DB 幂等,Generate 可安全重调;
+        // 即便"建单成功、起流失败",重调 Generate 会走到这里再次补流 → 无永久孤儿单。起流失败向上抛,暴露真实原因。
+        await EnsureFlowAsync(bill.Id, bill.BillNo);
+
         return new FqcTaskGenerateOutput { InspBillId = bill.Id, InspBillNo = bill.BillNo ?? "" };
     }
 
+    /// <summary>
+    /// 幂等起流:BizType+BizId 已有实例则复用,无则 StartFlow。发起人=当前用户(认领门保证==jyfzr),
+    /// N1(ApproverType=Initiator) 待办自然落发起人=认领人。幂等等级 B(代码级 check-then-start),
+    /// 与 submit-result 的 inst?.Id ?? StartFlow 同范式;ApprovalFlow 核心(BizType,BizId)无 DB 唯一约束,
+    /// 极端并发 race 仅登记风险(本批不改核心表)。
+    /// </summary>
+    private async Task EnsureFlowAsync(long inspBillId, string? billNo)
+    {
+        var hasInstance = await _db.Queryable<ApprovalFlowInstance>()
+            .ClearFilter()
+            .Where(x => x.BizType == FqcInspBillFlowConst.BizType && x.BizId == inspBillId)
+            .AnyAsync();
+        if (hasInstance) return; // 复用已有实例,不重复起流
+
+        // StartFlow 引擎内非原子(Instance Insert 在前、N1 Task Insert 在后,无内部事务)→ 裸调可能留下
+        // "实例有、N1 无"的半启动、永久卡死。用事务包裹(镜像 FqcInspBillFlowService.SubmitResult 的既有范式),
+        // 保证"实例 + N1 任务"要么全成、要么全回滚:N1 建失败 → 实例回滚 → 退化为可恢复 Scenario B(Bill 有 Flow 无),
+        // 由下一次 Generate 经本方法补起。失败明确上抛(不吞错、不在本次请求重试 StartFlow)。
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            await _flowEngine.StartFlow(new StartFlowInput
+            {
+                BizType = FqcInspBillFlowConst.BizType,
+                BizId = inspBillId,
+                BizNo = billNo,
+                Title = $"FQC检验单 {billNo}",
+            });
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("FQC 检验单工作流启动失败");
+    }
+
     private static List<long> NormalizeIds(List<long>? ids)
     {
         var list = (ids ?? new List<long>()).Where(x => x > 0).Distinct().ToList();
@@ -236,6 +278,20 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
         return list;
     }
 
+    /// <summary>
+    /// G4 负责人冻结:选中报检若有任一已生成检验单(=已起流),禁止再改 jyfzr(Claim/Assign),
+    /// 防止 jyfzr 与 N1 待办 Assignee 分叉产生孤儿。以检验单存在性判定(生成即起流,二者等价)。
+    /// </summary>
+    private async Task EnsureNotLockedAsync(List<long> ids, long tenantId)
+    {
+        var locked = await _db.Ado.GetIntAsync(
+            $@"SELECT COUNT(1) FROM qms_qcpp_inspbill b
+               JOIN qms_fqcbj q ON q.FBILLNO=b.lydjbh AND q.id=b.hid AND q.tenant_id=b.tenant_id
+               WHERE q.tenant_id=@t AND q.id IN ({string.Join(",", ids)})",
+            new SugarParameter("@t", tenantId));
+        if (locked > 0) throw Oops.Oh("检验单已生成,检验负责人已锁定,不允许重新认领或调配。");
+    }
+
     private sealed class FqcTaskApplyRow
     {
         public long Id { get; set; }

+ 11 - 9
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/FqcInspBillFlowSeed.cs

@@ -126,13 +126,13 @@ public static class FqcInspBillFlowSeed
                 .OrderByDescending(x => x.Version)
                 .First();
 
-            // v1→v2 幂等升级:已发布定义若已含 N3_QE_DISPOSITION+GW_RESULT(v2)则跳过;
-            // 若仍为 v1(无 N3/网关)则下架旧版、发布 v2。历史 Running/Approved 实例用各自 FlowJsonSnapshot,不受影响。
+            // v2→v3 幂等升级:v3 = N1 检验节点审批人由 Role(成品检验员) 改为 Initiator(发起人本人=认领人)。
+            // 已发布定义若已是 v3(FlowJson 含 "Initiator")则跳过;否则(v1/v2,无 Initiator)下架旧版、发布 v3。
+            // 历史 Running/Approved 实例用各自 FlowJsonSnapshot,不受影响;Seed 重复执行幂等、不重复建多个 v3。
             if (existing != null)
             {
-                var isV2 = (existing.FlowJson ?? "").Contains(FqcInspBillFlowConst.NodeDisposition)
-                    && (existing.FlowJson ?? "").Contains(FqcInspBillFlowConst.GatewayResult);
-                if (isV2) continue;
+                var isV3 = (existing.FlowJson ?? "").Contains(nameof(ApproverTypeEnum.Initiator));
+                if (isV3) continue;
                 db.Updateable<ApprovalFlowEntity>()
                     .SetColumns(x => x.IsPublished == false)
                     .SetColumns(x => x.UpdateTime == now)
@@ -150,7 +150,7 @@ public static class FqcInspBillFlowSeed
                 FormJson = "{}",
                 FlowJson = flowJson,
                 Status = 1,
-                Remark = "系统默认流程 v2:开始-检验员检验-检验主管审核-网关(pd=1)-QE处置-结束",
+                Remark = "系统默认流程 v3:开始-检验员检验(发起人本人/认领人)-检验主管审核-网关(pd=1)-QE处置-结束",
                 Version = nextVersion,
                 IsPublished = true,
                 OrgId = orgId,
@@ -197,9 +197,11 @@ public static class FqcInspBillFlowSeed
                     Properties = new FlowProperties
                     {
                         NodeName = "检验员检验",
-                        ApproverType = nameof(ApproverTypeEnum.Role),
-                        ApproverIds = FqcInspBillFlowConst.RoleInspectorId.ToString(),
-                        ApproverNames = "成品检验员",
+                        // v3:N1 待办归「发起人本人」= 生成检验单的认领人(ApproverType=Initiator,引擎按 InitiatorId 解析)。
+                        // ApproverIds 置空(Initiator 不需要角色/用户 id)。角色约束后置,本节点不再绑成品检验员角色。
+                        ApproverType = nameof(ApproverTypeEnum.Initiator),
+                        ApproverIds = "",
+                        ApproverNames = "认领人(发起人)",
                         MultiApproveMode = "Any",
                     },
                     Text = new FlowTextItem { X = 300, Y = 154, Value = "检验员检验" },