using System.Diagnostics; using System.Text.Json; using Admin.NET.Plugin.AiDOP.Entity; using Admin.NET.Plugin.AiDOP.Manufacturing; 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; /// /// S6 过程检验单(IPQC)检验员+主管流程种子(幂等)。 /// /// 顺序:① 扩展表 ado_s6_ipqc_inspection_flow_state + 处置表 ado_s6_ipqc_quality_disposition(CodeFirst 建表,无 SQL) /// ② 角色 ROLE_S6_IPQC_INSPECTOR / SUPERVISOR / QUALITY_ENGINEER ③ UAT 成员绑定(superAdmin.NET 同绑三角色,仅 UAT 自审) /// ④ ApprovalBizType(IPQC_INSPECTION) ⑤ ApprovalFlow v2 已发布定义 /// (start→N1_INSPECT→N2_SUP_REVIEW→GW_RESULT(disposition_required==1→N3 / 否则→end)→N3_QUALITY_DISPOSITION→end)。 /// v1→v2 幂等升级:已发布定义含 N3 则跳过;仍为 v1 则下架旧版发布 v2;历史 Running 实例用各自 FlowJsonSnapshot 冻结不受影响。 /// superAdmin.NET 不存在时仅告警不崩溃。 /// public static class IpqcInspectionFlowSeed { public static void EnsureSeed(ISqlSugarClient db) { try { db.CodeFirst.InitTables( typeof(ApprovalBizType), typeof(ApprovalFlowEntity), typeof(ApprovalFlowVersion)); // 流程状态轻量扩展表 + 质量处置表(CodeFirst 建表,无 SQL) db.CodeFirst.InitTables(typeof(AdoS6IpqcInspectionFlowState)); db.CodeFirst.InitTables(typeof(AdoS6IpqcQualityDisposition)); // S6-TENANT-AUTHORITY-1:三个 S6 角色改由共享种子按租户建立(原实现全库单例挂默认租户), // 并把既有跨租户成员语义等价复制到各自租户。两条 S6 链共用,重复调用幂等。 S6TenantAuthoritySeed.EnsureTenantAuthority(db); EnsureUatMembers(db); EnsureBizType(db); EnsurePublishedFlow(db); } catch (Exception ex) { Trace.TraceWarning("S6 IPQC inspection flow seed: " + ex); } } private static void EnsureUatMembers(ISqlSugarClient db) { var user = db.Queryable().ClearFilter() .Where(x => x.Account == IpqcInspectionFlowConst.UatAccount) .First(); if (user == null) { Trace.TraceWarning($"S6 IPQC flow seed: UAT 账号 {IpqcInspectionFlowConst.UatAccount} 不存在,跳过成员绑定(阶段2 冒烟前须指定成员)"); return; } EnsureUserRole(db, user.Id, IpqcInspectionFlowConst.RoleInspectorId); EnsureUserRole(db, user.Id, IpqcInspectionFlowConst.RoleSupervisorId); EnsureUserRole(db, user.Id, IpqcInspectionFlowConst.RoleQualityEngineerId); } 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 == IpqcInspectionFlowConst.BizType); if (exists) return; db.Insertable(new ApprovalBizType { Id = YitIdHelper.NextId(), Code = IpqcInspectionFlowConst.BizType, Name = "过程检验单审批", Remark = "S6 过程检验单 检验员→主管 审批流", IsEnabled = true, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, }).ExecuteCommand(); } /// /// S6-TENANT-AUTHORITY-1:副本维度由 **Org 改为 Tenant**。 /// /// 原实现按 SysOrg 逐组织建一份,而 FlowEngineService.StartFlowCore 选定义时**只按 TenantId** 过滤、 /// 完全不看 OrgId —— 维度错配导致同一租户下出现多份排序键全并列的候选,FirstAsync 选谁不确定。 /// 改为每租户一份后,租户内候选唯一,且新版本号更高,选取具确定性。 /// /// 只为通过 Activation Gate(三个角色在本租户均有同租户成员)的租户发布, /// 避免把目前仍靠 legacy 跨租户 RoleId 工作的租户迁成 0 审批人。 /// 历史定义(按 Org 建的、以及 TenantId 为空的)**一律不下架、不删除**:本方法纯增量, /// 依靠「本租户优先 + 版本号更高」让新定义被稳定命中;legacy 清理属后续独立批次。 /// private static void EnsurePublishedFlow(ISqlSugarClient db) { var now = DateTime.Now; var flowJson = BuildFlowJson(); foreach (var tenantId in S6TenantAuthoritySeed.GetActivatableTenantIds(db)) { var existing = db.Queryable() .ClearFilter() .Where(x => x.BizType == IpqcInspectionFlowConst.BizType && x.TenantId == tenantId && !x.IsDelete) .OrderByDescending(x => x.Version) .First(); // 目标形态判据:审批权威已是 RoleCode。含 RoleCode 即为当前版,跳过(幂等)。 // 仍是纯数字 RoleId 的(无论 v1 还是 v2 拓扑)都需要补发一版 RoleCode 定义。 if (existing != null && (existing.FlowJson ?? "").Contains(IpqcInspectionFlowConst.RoleSupervisorCode)) continue; var nextVersion = (existing?.Version ?? 0) + 1; var flow = new ApprovalFlowEntity { Id = YitIdHelper.NextId(), Code = $"S6IPQC_T{tenantId}", Name = "过程检验单审批", BizType = IpqcInspectionFlowConst.BizType, FormJson = "{}", FlowJson = flowJson, Status = 1, Remark = "租户级流程:开始-检验员检验-检验主管审核-网关(整批判定)-质量处置-结束(审批权威=RoleCode)", Version = nextVersion, 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 选取(StartFlowCore 已 ClearFilter), /// 仅为保持实体字段有意义;取不到时落 0。 /// 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 = IpqcInspectionFlowConst.NodeInspect, Type = "bpmn:userTask", X = 300, Y = 120, Properties = new FlowProperties { NodeName = "检验员检验", ApproverType = nameof(ApproverTypeEnum.Role), // S6-TENANT-AUTHORITY-1:审批权威用 RoleCode,不用默认租户物理 RoleId。 // FlowEngineService 的 Code 分支会按 (Code, effectiveTenantId, Status=Enable) // 在**当前租户内**重新解析出本地 RoleId,天然租户安全。 ApproverIds = IpqcInspectionFlowConst.RoleInspectorCode, ApproverNames = "过程检验员", MultiApproveMode = "Any", }, Text = new FlowTextItem { X = 300, Y = 154, Value = "检验员检验" }, }, new() { Id = IpqcInspectionFlowConst.NodeSupervisor, Type = "bpmn:userTask", X = 500, Y = 120, Properties = new FlowProperties { NodeName = "检验主管审核", ApproverType = nameof(ApproverTypeEnum.Role), ApproverIds = IpqcInspectionFlowConst.RoleSupervisorCode, ApproverNames = "过程检验主管", MultiApproveMode = "Any", }, Text = new FlowTextItem { X = 500, Y = 154, Value = "检验主管审核" }, }, new() { Id = IpqcInspectionFlowConst.GatewayResult, Type = "bpmn:exclusiveGateway", X = 660, Y = 120, Properties = new FlowProperties { NodeName = "整批判定分流", Conditions = new List { new() { Expression = "disposition_required == 1", TargetNodeId = IpqcInspectionFlowConst.NodeDisposition, IsDefault = false, Label = "整批不合格转处置", }, new() { TargetNodeId = "end", IsDefault = true, Label = "整批合格完成", }, }, }, Text = new FlowTextItem { X = 660, Y = 154, Value = "整批判定分流" }, }, new() { Id = IpqcInspectionFlowConst.NodeDisposition, Type = "bpmn:userTask", X = 820, Y = 120, Properties = new FlowProperties { NodeName = "质量处置", ApproverType = nameof(ApproverTypeEnum.Role), ApproverIds = IpqcInspectionFlowConst.RoleQualityEngineerCode, ApproverNames = "过程检验质量工程师", MultiApproveMode = "Any", }, Text = new FlowTextItem { X = 820, Y = 154, Value = "质量处置" }, }, 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", IpqcInspectionFlowConst.NodeInspect, 140, 120, 260, 120), BuildEdge("edge-n1-n2", IpqcInspectionFlowConst.NodeInspect, IpqcInspectionFlowConst.NodeSupervisor, 340, 120, 460, 120), BuildEdge("edge-n2-gw", IpqcInspectionFlowConst.NodeSupervisor, IpqcInspectionFlowConst.GatewayResult, 540, 120, 640, 120), BuildEdge("edge-gw-n3", IpqcInspectionFlowConst.GatewayResult, IpqcInspectionFlowConst.NodeDisposition, 680, 120, 780, 120), BuildEdge("edge-gw-end", IpqcInspectionFlowConst.GatewayResult, "end", 680, 120, 960, 120), BuildEdge("edge-n3-end", IpqcInspectionFlowConst.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 }, }, }; } }