using System.Diagnostics; using System.Text.Json; using Admin.NET.Plugin.AiDOP.Entity; using Admin.NET.Plugin.AiDOP.FinishedWarehouse; using Admin.NET.Plugin.ApprovalFlow; using Admin.NET.Plugin.ApprovalFlow.Service; using Yitter.IdGenerator; using ApprovalFlowEntity = Admin.NET.Plugin.ApprovalFlow.ApprovalFlow; namespace Admin.NET.Plugin.AiDOP.Infrastructure; /// /// S7 FQC 成品检验单 检验员+主管流程种子(幂等)。 /// /// 顺序:① 角色 ROLE_S7_FQC_INSPECTOR / ROLE_S7_FQC_SUPERVISOR ② UAT 成员绑定(superAdmin.NET 同绑两角色) /// ③ ApprovalBizType(S7_FQC_INSPBILL) ④ ApprovalFlow 已发布定义(start→N1_INSPECT→N2_SUP_REVIEW→end,节点绑角色,线性无网关)。 /// 全部幂等:存在则跳过,不覆盖、不删除既有角色/流程。superAdmin.NET 不存在时仅告警不崩溃。 /// public static class FqcInspBillFlowSeed { public static void EnsureSeed(ISqlSugarClient db) { try { db.CodeFirst.InitTables( typeof(ApprovalBizType), typeof(ApprovalFlowEntity), typeof(ApprovalFlowVersion)); // QE 处置表(CodeFirst 建表,无 SQL) db.CodeFirst.InitTables(typeof(AdoS7FqcQeDisposition)); // S7-FQC-TENANT-AUTHORITY-1:角色供给改为**租户本地化**(原 EnsureRole 用全局 // ClearFilter().Any(Code) 查重,默认租户一有该 Code 就再不为其它租户建 → 全库单例)。 // 默认租户沿用既有固定 Id,其余租户各自一份;既有跨租户绑定 COPY 保留、不删。 S7TenantAuthoritySeed.EnsureTenantAuthority(db); EnsureUatMembers(db); EnsureBizType(db); EnsurePublishedFlow(db); } catch (Exception ex) { Trace.TraceWarning("S7 FQC inspbill flow seed: " + ex); } } private static void EnsureUatMembers(ISqlSugarClient db) { var user = db.Queryable().ClearFilter() .Where(x => x.Account == FqcInspBillFlowConst.UatAccount) .First(); if (user == null) { Trace.TraceWarning($"S7 FQC flow seed: UAT 账号 {FqcInspBillFlowConst.UatAccount} 不存在,跳过成员绑定(阶段2 冒烟前须指定成员)"); return; } EnsureUserRole(db, user.Id, FqcInspBillFlowConst.RoleInspectorId); EnsureUserRole(db, user.Id, FqcInspBillFlowConst.RoleSupervisorId); EnsureUserRole(db, user.Id, FqcInspBillFlowConst.RoleQeId); } private static void EnsureUserRole(ISqlSugarClient db, long userId, long roleId) { var exists = db.Queryable().ClearFilter().Any(x => x.UserId == userId && x.RoleId == roleId); if (exists) return; db.Insertable(new SysUserRole { Id = YitIdHelper.NextId(), UserId = userId, RoleId = roleId, }).ExecuteCommand(); } private static void EnsureBizType(ISqlSugarClient db) { var exists = db.Queryable().Any(x => x.Code == FqcInspBillFlowConst.BizType); if (exists) return; db.Insertable(new ApprovalBizType { Id = YitIdHelper.NextId(), Code = FqcInspBillFlowConst.BizType, Name = "成品检验单审批", Remark = "S7 成品检验单 检验员→主管 审批流", IsEnabled = true, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, }).ExecuteCommand(); } /// /// S7-FQC-TENANT-AUTHORITY-1:定义播种维度由 **SysOrg** 改为 **可激活租户**。 /// /// 原实现按 SysOrg 逐个播种(Code = $"S7FQC_{orgId}"TenantId = ResolveTenantId(db, orgId)), /// 造成 57 条定义且多条定义租户与业务租户错配(审计实测:5 条 Running + 4 条终态实例的 /// 定义租户是 788012541595717,而业务租户是 797 / 838)。审批权威的维度是**租户**,不是机构。 /// /// 幂等判据改为「已发布定义的 FlowJson 是否已含 RoleCode」:含则跳过,否则补发新版本。 /// **不再下架历史定义**(原 v2→v3 升级会 SetColumns(IsPublished=false))——历史事实保留, /// 新版本靠 StartFlowCore 的「本租户优先 + Version 降序」自然胜出即可。 /// 历史 Running/Approved 实例各用自己的 FlowJsonSnapshot,本方法对其零影响。 /// private static void EnsurePublishedFlow(ISqlSugarClient db) { var now = DateTime.Now; var flowJson = BuildFlowJson(); foreach (var tenantId in S7TenantAuthoritySeed.GetActivatableTenantIds(db)) { var existing = db.Queryable() .ClearFilter() .Where(x => x.BizType == FqcInspBillFlowConst.BizType && x.TenantId == tenantId && !x.IsDelete) .OrderByDescending(x => x.Version) .First(); // 已有 RoleCode 版则跳过(幂等);仍是纯数字 RoleId 的需要补发。 if (existing != null && (existing.FlowJson ?? "").Contains(FqcInspBillFlowConst.RoleSupervisorCode)) continue; var flow = new ApprovalFlowEntity { Id = YitIdHelper.NextId(), Code = $"S7FQC_T{tenantId}", Name = "成品检验单审批", BizType = FqcInspBillFlowConst.BizType, FormJson = "{}", FlowJson = flowJson, Status = 1, Remark = "租户级流程:开始-检验员检验(发起人本人/认领人)-检验主管审核-网关(pd=1)-QE处置-结束(审批权威=RoleCode)", Version = (existing?.Version ?? 0) + 1, IsPublished = true, OrgId = ResolveTenantOrgId(db, tenantId), TenantId = tenantId, CreateTime = now, UpdateTime = now, }; db.Insertable(flow).ExecuteCommand(); db.Insertable(new ApprovalFlowVersion { Id = YitIdHelper.NextId(), FlowId = flow.Id, Version = flow.Version, FlowJson = flow.FlowJson, FormJson = flow.FormJson, PublishTime = now, PublisherName = "系统初始化", CreateTime = now, UpdateTime = now, }).ExecuteCommand(); } } /// 取租户主机构 Id 落 OrgId;OrgId 不参与 Runtime 选取,仅保持字段有意义。 private static long ResolveTenantOrgId(ISqlSugarClient db, long tenantId) { var orgId = db.Queryable() .ClearFilter() .Where(x => x.Id == tenantId) .Select(x => x.OrgId) .First(); return orgId > 0 ? orgId : 0; } private static string BuildFlowJson() { var item = new ApprovalFlowItem { Nodes = new List { new() { Id = "start", Type = "bpmn:startEvent", X = 100, Y = 120, Properties = new FlowProperties { NodeName = "开始" }, Text = new FlowTextItem { X = 100, Y = 154, Value = "开始" }, }, new() { Id = FqcInspBillFlowConst.NodeInspect, Type = "bpmn:userTask", X = 300, Y = 120, Properties = new FlowProperties { NodeName = "检验员检验", // v3:N1 待办归「发起人本人」= 生成检验单的认领人(ApproverType=Initiator,引擎按 InitiatorId 解析)。 // ApproverIds 置空(Initiator 不需要角色/用户 id)。角色约束后置,本节点不再绑成品检验员角色。 ApproverType = nameof(ApproverTypeEnum.Initiator), ApproverIds = "", ApproverNames = "认领人(发起人)", MultiApproveMode = "Any", }, Text = new FlowTextItem { X = 300, Y = 154, Value = "检验员检验" }, }, new() { Id = FqcInspBillFlowConst.NodeSupervisor, Type = "bpmn:userTask", X = 500, Y = 120, Properties = new FlowProperties { NodeName = "检验主管审核", // S7-FQC-TENANT-AUTHORITY-1:权威改为 **RoleCode**,由引擎在目标租户内解析成 // 该租户自己的角色。定义是模板、RoleId 是租户本地身份,模板里不能存某个具体租户的 // 物理 RoleId(原写 RoleSupervisorId=1329915020002,属默认租户)。 // 即使此处能查到本租户 RoleId 也不写入——那会让定义重新变成租户绑定的。 ApproverType = nameof(ApproverTypeEnum.Role), ApproverIds = FqcInspBillFlowConst.RoleSupervisorCode, ApproverNames = "成品检验主管", MultiApproveMode = "Any", }, Text = new FlowTextItem { X = 500, Y = 154, Value = "检验主管审核" }, }, new() { Id = FqcInspBillFlowConst.GatewayResult, Type = "bpmn:exclusiveGateway", X = 660, Y = 120, Properties = new FlowProperties { NodeName = "判定分流", Conditions = new List { new() { Expression = "disposition_required == 1", TargetNodeId = FqcInspBillFlowConst.NodeDisposition, IsDefault = false, Label = "不合格转QE", }, new() { TargetNodeId = "end", IsDefault = true, Label = "合格完成", }, }, }, Text = new FlowTextItem { X = 660, Y = 154, Value = "判定分流" }, }, new() { Id = FqcInspBillFlowConst.NodeDisposition, Type = "bpmn:userTask", X = 820, Y = 120, Properties = new FlowProperties { NodeName = "QE处置", // 同 N2:权威用 RoleCode,不写默认租户物理 RoleId(原 RoleQeId=1329915020003)。 ApproverType = nameof(ApproverTypeEnum.Role), ApproverIds = FqcInspBillFlowConst.RoleQeCode, ApproverNames = "成品检验QE", MultiApproveMode = "Any", }, Text = new FlowTextItem { X = 820, Y = 154, Value = "QE处置" }, }, new() { Id = "end", Type = "bpmn:endEvent", X = 980, Y = 120, Properties = new FlowProperties { NodeName = "结束" }, Text = new FlowTextItem { X = 980, Y = 154, Value = "结束" }, }, }, Edges = new List { BuildEdge("edge-start-n1", "start", FqcInspBillFlowConst.NodeInspect, 140, 120, 260, 120), BuildEdge("edge-n1-n2", FqcInspBillFlowConst.NodeInspect, FqcInspBillFlowConst.NodeSupervisor, 340, 120, 460, 120), BuildEdge("edge-n2-gw", FqcInspBillFlowConst.NodeSupervisor, FqcInspBillFlowConst.GatewayResult, 540, 120, 640, 120), BuildEdge("edge-gw-n3", FqcInspBillFlowConst.GatewayResult, FqcInspBillFlowConst.NodeDisposition, 680, 120, 780, 120), BuildEdge("edge-gw-end", FqcInspBillFlowConst.GatewayResult, "end", 680, 120, 960, 120), BuildEdge("edge-n3-end", FqcInspBillFlowConst.NodeDisposition, "end", 860, 120, 960, 120), } }; return JsonSerializer.Serialize(item); } private static ApprovalFlowEdgeItem BuildEdge(string id, string source, string target, float sx, float sy, float ex, float ey) { return new ApprovalFlowEdgeItem { Id = id, Type = "bpmn:sequenceFlow", SourceNodeId = source, TargetNodeId = target, StartPoint = new FlowEdgePointItem { X = sx, Y = sy }, EndPoint = new FlowEdgePointItem { X = ex, Y = ey }, Properties = new FlowProperties(), PointsList = new List { new() { X = sx, Y = sy }, new() { X = ex, Y = ey }, }, }; } }