using System.Diagnostics; using Admin.NET.Plugin.AiDOP.Manufacturing; using Yitter.IdGenerator; namespace Admin.NET.Plugin.AiDOP.Infrastructure; /// /// S6-TENANT-AUTHORITY-1:S6 过程检验(IPQC)三类审批角色的**租户本地化**权威基线, /// 由 (IPQC_INSPECTION)与 /// (S6_PROCESS_INSPECTION)两条链共用。 /// /// 修复的缺陷(SYSTEMIC TENANT CLONE DEFECT):原先三个角色是**全库单例、固定挂默认租户**, /// 各租户用户靠「跨租户 SysUserRole 绑定」借用;流程定义又把默认租户的**物理 RoleId** /// 写死进 FlowJson。于是「谁是主管」这件事跨租户共享,且可用性挂在一条无约束、无审计、 /// 可被直连 SQL 单行改动的手工绑定上——该绑定一旦被删,整个租户的 N2 当场解析为 0 人。 /// /// 目标模型(与平台主流一致:ROLE_QC / SMART_OPS_* 等 11 个 Code 均为每租户一份): /// 每个租户各有自己的 ROLE_S6_IPQC_INSPECTOR / _SUPERVISOR / _QUALITY_ENGINEER, /// Code 相同、RoleId 不同、成员与角色同租户;FlowJson 以 **RoleCode** 表达审批权威, /// 由 FlowEngineService 的 Code 分支在目标租户内重新解析。 /// /// 边界: /// - 默认租户 的那三行**本身就是它的 tenant-local 角色**, /// 沿用 的既有固定 Id,不重建、不改动。 /// - membership 迁移是 **COPY 不是 MOVE**:既有跨租户绑定原样保留,供历史实例与尚未迁移的 /// legacy 定义继续使用;清理属后续独立批次。 /// - 本文件**不得**出现任何具体租户 Id 或 UAT 账号——只按 SysTenant 注册表通用处理。 /// internal static class S6TenantAuthoritySeed { /// 三类 S6 角色:Code / 名称 / 排序号 / 默认租户既有固定 Id。 private static readonly (string Code, string Name, int OrderNo, long LegacyDefaultTenantRoleId)[] RoleSpecs = { (IpqcInspectionFlowConst.RoleInspectorCode, "过程检验员", 820, IpqcInspectionFlowConst.RoleInspectorId), (IpqcInspectionFlowConst.RoleSupervisorCode, "过程检验主管", 821, IpqcInspectionFlowConst.RoleSupervisorId), (IpqcInspectionFlowConst.RoleQualityEngineerCode, "过程检验质量工程师", 822, IpqcInspectionFlowConst.RoleQualityEngineerId), }; /// 三个 S6 RoleCode(供流程种子与 Readiness 判定共用)。 public static IReadOnlyList RoleCodes { get; } = RoleSpecs.Select(x => x.Code).ToList(); /// /// 幂等地建立租户本地角色并复制既有成员语义。两条 S6 链的种子都会调用,重复调用无副作用。 /// public static void EnsureTenantAuthority(ISqlSugarClient db) { try { EnsureTenantLocalRoles(db); CopyLegacyCrossTenantMembership(db); } catch (Exception ex) { Trace.TraceWarning("S6 tenant authority seed: " + ex); } } /// /// 为 SysTenant 注册表中的每个租户建立三个 S6 角色。查重键是 (TenantId, Code)—— /// 原实现用全局 Any(Code),一旦默认租户已有该 Code 就再也不会为其它租户建, /// 这正是「全库单例」的成因。 /// private static void EnsureTenantLocalRoles(ISqlSugarClient db) { var now = DateTime.Now; foreach (var tenantId in ResolveTenantIds(db)) { foreach (var spec in RoleSpecs) { var exists = db.Queryable() .ClearFilter() .Any(r => r.TenantId == tenantId && r.Code == spec.Code); if (exists) continue; // 默认租户沿用既有固定 Id(历史 FlowJson / 业务门禁仍按该 Id 判定,不能换号); // 其余租户一律走雪花号,遵循 SysRole 的 tenant-local 身份语义。 var roleId = tenantId == SqlSugarConst.DefaultTenantId ? spec.LegacyDefaultTenantRoleId : YitIdHelper.NextId(); db.Insertable(new SysRole { Id = roleId, TenantId = tenantId, Name = spec.Name, Code = spec.Code, OrderNo = spec.OrderNo, DataScope = DataScopeEnum.Self, Status = StatusEnum.Enable, Remark = "S6 过程检验流程角色(租户本地)", CreateTime = now, }).ExecuteCommand(); } } } /// /// 把「用户在 A 租户、却绑着默认租户 S6 角色」的既有语义,等价复制成该用户自己租户内的本地绑定。 /// 只复制**已经存在**的授权事实,不新增任何业务成员;legacy 绑定保留不动(COPY 不是 MOVE)。 /// private static void CopyLegacyCrossTenantMembership(ISqlSugarClient db) { foreach (var spec in RoleSpecs) { var memberIds = db.Queryable() .ClearFilter() .Where(ur => ur.RoleId == spec.LegacyDefaultTenantRoleId) .Select(ur => ur.UserId) .ToList(); if (memberIds.Count == 0) continue; var crossTenantMembers = db.Queryable() .ClearFilter() .Where(u => memberIds.Contains(u.Id) && u.TenantId != SqlSugarConst.DefaultTenantId) .ToList(); foreach (var user in crossTenantMembers) { if (user.TenantId is not > 0) continue; // 无租户归属的用户不产生本地绑定 var localRoleId = ResolveLocalRoleId(db, user.TenantId.Value, spec.Code); if (localRoleId is not > 0) continue; // SysUserRole 无 (UserId, RoleId) 唯一约束,防重只能靠这里自己做。 var bound = db.Queryable() .ClearFilter() .Any(x => x.UserId == user.Id && x.RoleId == localRoleId.Value); if (bound) continue; db.Insertable(new SysUserRole { Id = YitIdHelper.NextId(), UserId = user.Id, RoleId = localRoleId.Value, }).ExecuteCommand(); } } } /// 按 (TenantId, Code) 取租户本地角色 Id;不存在返回 null。 public static long? ResolveLocalRoleId(ISqlSugarClient db, long tenantId, string code) { return db.Queryable() .ClearFilter() .Where(r => r.TenantId == tenantId && r.Code == code && r.Status == StatusEnum.Enable) .Select(r => r.Id) .First(); } /// /// Activation Gate:只有三个 S6 角色在该租户内**都存在本地角色且各至少 1 名同租户成员**时, /// 才允许为该租户发布 RoleCode 版流程定义。 /// /// 这道门禁的意义:不得把一个「目前还靠 legacy 跨租户 RoleId 正常工作」的租户, /// 迁成 0 审批人。成员不齐的租户维持原状(继续命中 legacy 定义),等它齐了下次启动自动补发。 /// public static List GetActivatableTenantIds(ISqlSugarClient db) { var ready = new List(); foreach (var tenantId in ResolveTenantIds(db)) { var allReady = true; foreach (var spec in RoleSpecs) { var roleId = ResolveLocalRoleId(db, tenantId, spec.Code); if (roleId is not > 0) { allReady = false; break; } var memberIds = db.Queryable() .ClearFilter() .Where(ur => ur.RoleId == roleId.Value) .Select(ur => ur.UserId) .ToList(); if (memberIds.Count == 0) { allReady = false; break; } // 成员必须与角色同租户——审批人解析最终按 SysUser.TenantId 过滤, // 只数绑定行会把「有绑定但没有同租户成员」误判为 ready。 var sameTenantMembers = db.Queryable() .ClearFilter() .Count(u => memberIds.Contains(u.Id) && u.TenantId == tenantId); if (sameTenantMembers == 0) { allReady = false; break; } } if (allReady) ready.Add(tenantId); } return ready; } /// /// 租户清单一律取自项目正式注册表 SysTenant(含默认租户),不自行构造。 /// private static List ResolveTenantIds(ISqlSugarClient db) { var ids = db.Queryable() .ClearFilter() .Where(x => x.Id > 1) .Select(x => x.Id) .ToList(); if (!ids.Contains(SqlSugarConst.DefaultTenantId)) ids.Add(SqlSugarConst.DefaultTenantId); return ids.Distinct().ToList(); } }