| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- using System.Diagnostics;
- using Admin.NET.Plugin.AiDOP.Manufacturing;
- using Yitter.IdGenerator;
- namespace Admin.NET.Plugin.AiDOP.Infrastructure;
- /// <summary>
- /// S6-TENANT-AUTHORITY-1:S6 过程检验(IPQC)三类审批角色的**租户本地化**权威基线,
- /// 由 <see cref="IpqcInspectionFlowSeed"/>(IPQC_INSPECTION)与
- /// <see cref="S6ProcessInspectionFlowSeed"/>(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 分支在目标租户内重新解析。
- ///
- /// 边界:
- /// - 默认租户 <see cref="SqlSugarConst.DefaultTenantId"/> 的那三行**本身就是它的 tenant-local 角色**,
- /// 沿用 <see cref="IpqcInspectionFlowConst"/> 的既有固定 Id,不重建、不改动。
- /// - membership 迁移是 **COPY 不是 MOVE**:既有跨租户绑定原样保留,供历史实例与尚未迁移的
- /// legacy 定义继续使用;清理属后续独立批次。
- /// - 本文件**不得**出现任何具体租户 Id 或 UAT 账号——只按 SysTenant 注册表通用处理。
- /// </summary>
- internal static class S6TenantAuthoritySeed
- {
- /// <summary>三类 S6 角色:Code / 名称 / 排序号 / 默认租户既有固定 Id。</summary>
- 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),
- };
- /// <summary>三个 S6 RoleCode(供流程种子与 Readiness 判定共用)。</summary>
- public static IReadOnlyList<string> RoleCodes { get; } =
- RoleSpecs.Select(x => x.Code).ToList();
- /// <summary>
- /// 幂等地建立租户本地角色并复制既有成员语义。两条 S6 链的种子都会调用,重复调用无副作用。
- /// </summary>
- public static void EnsureTenantAuthority(ISqlSugarClient db)
- {
- try
- {
- EnsureTenantLocalRoles(db);
- CopyLegacyCrossTenantMembership(db);
- }
- catch (Exception ex)
- {
- Trace.TraceWarning("S6 tenant authority seed: " + ex);
- }
- }
- /// <summary>
- /// 为 SysTenant 注册表中的每个租户建立三个 S6 角色。查重键是 <c>(TenantId, Code)</c>——
- /// 原实现用全局 <c>Any(Code)</c>,一旦默认租户已有该 Code 就再也不会为其它租户建,
- /// 这正是「全库单例」的成因。
- /// </summary>
- 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<SysRole>()
- .ClearFilter<ITenantIdFilter>()
- .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();
- }
- }
- }
- /// <summary>
- /// 把「用户在 A 租户、却绑着默认租户 S6 角色」的既有语义,等价复制成该用户自己租户内的本地绑定。
- /// 只复制**已经存在**的授权事实,不新增任何业务成员;legacy 绑定保留不动(COPY 不是 MOVE)。
- /// </summary>
- private static void CopyLegacyCrossTenantMembership(ISqlSugarClient db)
- {
- foreach (var spec in RoleSpecs)
- {
- var memberIds = db.Queryable<SysUserRole>()
- .ClearFilter()
- .Where(ur => ur.RoleId == spec.LegacyDefaultTenantRoleId)
- .Select(ur => ur.UserId)
- .ToList();
- if (memberIds.Count == 0) continue;
- var crossTenantMembers = db.Queryable<SysUser>()
- .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<SysUserRole>()
- .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();
- }
- }
- }
- /// <summary>按 (TenantId, Code) 取租户本地角色 Id;不存在返回 null。</summary>
- public static long? ResolveLocalRoleId(ISqlSugarClient db, long tenantId, string code)
- {
- return db.Queryable<SysRole>()
- .ClearFilter<ITenantIdFilter>()
- .Where(r => r.TenantId == tenantId && r.Code == code && r.Status == StatusEnum.Enable)
- .Select(r => r.Id)
- .First();
- }
- /// <summary>
- /// Activation Gate:只有三个 S6 角色在该租户内**都存在本地角色且各至少 1 名同租户成员**时,
- /// 才允许为该租户发布 RoleCode 版流程定义。
- ///
- /// 这道门禁的意义:不得把一个「目前还靠 legacy 跨租户 RoleId 正常工作」的租户,
- /// 迁成 0 审批人。成员不齐的租户维持原状(继续命中 legacy 定义),等它齐了下次启动自动补发。
- /// </summary>
- public static List<long> GetActivatableTenantIds(ISqlSugarClient db)
- {
- var ready = new List<long>();
- 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<SysUserRole>()
- .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<SysUser>()
- .ClearFilter()
- .Count(u => memberIds.Contains(u.Id) && u.TenantId == tenantId);
- if (sameTenantMembers == 0) { allReady = false; break; }
- }
- if (allReady) ready.Add(tenantId);
- }
- return ready;
- }
- /// <summary>
- /// 租户清单一律取自项目正式注册表 SysTenant(含默认租户),不自行构造。
- /// </summary>
- private static List<long> ResolveTenantIds(ISqlSugarClient db)
- {
- var ids = db.Queryable<SysTenant>()
- .ClearFilter<ITenantIdFilter>()
- .Where(x => x.Id > 1)
- .Select(x => x.Id)
- .ToList();
- if (!ids.Contains(SqlSugarConst.DefaultTenantId))
- ids.Add(SqlSugarConst.DefaultTenantId);
- return ids.Distinct().ToList();
- }
- }
|