S6TenantAuthoritySeed.cs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. using System.Diagnostics;
  2. using Admin.NET.Plugin.AiDOP.Manufacturing;
  3. using Yitter.IdGenerator;
  4. namespace Admin.NET.Plugin.AiDOP.Infrastructure;
  5. /// <summary>
  6. /// S6-TENANT-AUTHORITY-1:S6 过程检验(IPQC)三类审批角色的**租户本地化**权威基线,
  7. /// 由 <see cref="IpqcInspectionFlowSeed"/>(IPQC_INSPECTION)与
  8. /// <see cref="S6ProcessInspectionFlowSeed"/>(S6_PROCESS_INSPECTION)两条链共用。
  9. ///
  10. /// 修复的缺陷(SYSTEMIC TENANT CLONE DEFECT):原先三个角色是**全库单例、固定挂默认租户**,
  11. /// 各租户用户靠「跨租户 SysUserRole 绑定」借用;流程定义又把默认租户的**物理 RoleId**
  12. /// 写死进 FlowJson。于是「谁是主管」这件事跨租户共享,且可用性挂在一条无约束、无审计、
  13. /// 可被直连 SQL 单行改动的手工绑定上——该绑定一旦被删,整个租户的 N2 当场解析为 0 人。
  14. ///
  15. /// 目标模型(与平台主流一致:ROLE_QC / SMART_OPS_* 等 11 个 Code 均为每租户一份):
  16. /// 每个租户各有自己的 ROLE_S6_IPQC_INSPECTOR / _SUPERVISOR / _QUALITY_ENGINEER,
  17. /// Code 相同、RoleId 不同、成员与角色同租户;FlowJson 以 **RoleCode** 表达审批权威,
  18. /// 由 FlowEngineService 的 Code 分支在目标租户内重新解析。
  19. ///
  20. /// 边界:
  21. /// - 默认租户 <see cref="SqlSugarConst.DefaultTenantId"/> 的那三行**本身就是它的 tenant-local 角色**,
  22. /// 沿用 <see cref="IpqcInspectionFlowConst"/> 的既有固定 Id,不重建、不改动。
  23. /// - membership 迁移是 **COPY 不是 MOVE**:既有跨租户绑定原样保留,供历史实例与尚未迁移的
  24. /// legacy 定义继续使用;清理属后续独立批次。
  25. /// - 本文件**不得**出现任何具体租户 Id 或 UAT 账号——只按 SysTenant 注册表通用处理。
  26. /// </summary>
  27. internal static class S6TenantAuthoritySeed
  28. {
  29. /// <summary>三类 S6 角色:Code / 名称 / 排序号 / 默认租户既有固定 Id。</summary>
  30. private static readonly (string Code, string Name, int OrderNo, long LegacyDefaultTenantRoleId)[] RoleSpecs =
  31. {
  32. (IpqcInspectionFlowConst.RoleInspectorCode, "过程检验员", 820, IpqcInspectionFlowConst.RoleInspectorId),
  33. (IpqcInspectionFlowConst.RoleSupervisorCode, "过程检验主管", 821, IpqcInspectionFlowConst.RoleSupervisorId),
  34. (IpqcInspectionFlowConst.RoleQualityEngineerCode, "过程检验质量工程师", 822, IpqcInspectionFlowConst.RoleQualityEngineerId),
  35. };
  36. /// <summary>三个 S6 RoleCode(供流程种子与 Readiness 判定共用)。</summary>
  37. public static IReadOnlyList<string> RoleCodes { get; } =
  38. RoleSpecs.Select(x => x.Code).ToList();
  39. /// <summary>
  40. /// 幂等地建立租户本地角色并复制既有成员语义。两条 S6 链的种子都会调用,重复调用无副作用。
  41. /// </summary>
  42. public static void EnsureTenantAuthority(ISqlSugarClient db)
  43. {
  44. try
  45. {
  46. EnsureTenantLocalRoles(db);
  47. CopyLegacyCrossTenantMembership(db);
  48. }
  49. catch (Exception ex)
  50. {
  51. Trace.TraceWarning("S6 tenant authority seed: " + ex);
  52. }
  53. }
  54. /// <summary>
  55. /// 为 SysTenant 注册表中的每个租户建立三个 S6 角色。查重键是 <c>(TenantId, Code)</c>——
  56. /// 原实现用全局 <c>Any(Code)</c>,一旦默认租户已有该 Code 就再也不会为其它租户建,
  57. /// 这正是「全库单例」的成因。
  58. /// </summary>
  59. private static void EnsureTenantLocalRoles(ISqlSugarClient db)
  60. {
  61. var now = DateTime.Now;
  62. foreach (var tenantId in ResolveTenantIds(db))
  63. {
  64. foreach (var spec in RoleSpecs)
  65. {
  66. var exists = db.Queryable<SysRole>()
  67. .ClearFilter<ITenantIdFilter>()
  68. .Any(r => r.TenantId == tenantId && r.Code == spec.Code);
  69. if (exists) continue;
  70. // 默认租户沿用既有固定 Id(历史 FlowJson / 业务门禁仍按该 Id 判定,不能换号);
  71. // 其余租户一律走雪花号,遵循 SysRole 的 tenant-local 身份语义。
  72. var roleId = tenantId == SqlSugarConst.DefaultTenantId
  73. ? spec.LegacyDefaultTenantRoleId
  74. : YitIdHelper.NextId();
  75. db.Insertable(new SysRole
  76. {
  77. Id = roleId,
  78. TenantId = tenantId,
  79. Name = spec.Name,
  80. Code = spec.Code,
  81. OrderNo = spec.OrderNo,
  82. DataScope = DataScopeEnum.Self,
  83. Status = StatusEnum.Enable,
  84. Remark = "S6 过程检验流程角色(租户本地)",
  85. CreateTime = now,
  86. }).ExecuteCommand();
  87. }
  88. }
  89. }
  90. /// <summary>
  91. /// 把「用户在 A 租户、却绑着默认租户 S6 角色」的既有语义,等价复制成该用户自己租户内的本地绑定。
  92. /// 只复制**已经存在**的授权事实,不新增任何业务成员;legacy 绑定保留不动(COPY 不是 MOVE)。
  93. /// </summary>
  94. private static void CopyLegacyCrossTenantMembership(ISqlSugarClient db)
  95. {
  96. foreach (var spec in RoleSpecs)
  97. {
  98. var memberIds = db.Queryable<SysUserRole>()
  99. .ClearFilter()
  100. .Where(ur => ur.RoleId == spec.LegacyDefaultTenantRoleId)
  101. .Select(ur => ur.UserId)
  102. .ToList();
  103. if (memberIds.Count == 0) continue;
  104. var crossTenantMembers = db.Queryable<SysUser>()
  105. .ClearFilter()
  106. .Where(u => memberIds.Contains(u.Id) && u.TenantId != SqlSugarConst.DefaultTenantId)
  107. .ToList();
  108. foreach (var user in crossTenantMembers)
  109. {
  110. if (user.TenantId is not > 0) continue; // 无租户归属的用户不产生本地绑定
  111. var localRoleId = ResolveLocalRoleId(db, user.TenantId.Value, spec.Code);
  112. if (localRoleId is not > 0) continue;
  113. // SysUserRole 无 (UserId, RoleId) 唯一约束,防重只能靠这里自己做。
  114. var bound = db.Queryable<SysUserRole>()
  115. .ClearFilter()
  116. .Any(x => x.UserId == user.Id && x.RoleId == localRoleId.Value);
  117. if (bound) continue;
  118. db.Insertable(new SysUserRole
  119. {
  120. Id = YitIdHelper.NextId(),
  121. UserId = user.Id,
  122. RoleId = localRoleId.Value,
  123. }).ExecuteCommand();
  124. }
  125. }
  126. }
  127. /// <summary>按 (TenantId, Code) 取租户本地角色 Id;不存在返回 null。</summary>
  128. public static long? ResolveLocalRoleId(ISqlSugarClient db, long tenantId, string code)
  129. {
  130. return db.Queryable<SysRole>()
  131. .ClearFilter<ITenantIdFilter>()
  132. .Where(r => r.TenantId == tenantId && r.Code == code && r.Status == StatusEnum.Enable)
  133. .Select(r => r.Id)
  134. .First();
  135. }
  136. /// <summary>
  137. /// Activation Gate:只有三个 S6 角色在该租户内**都存在本地角色且各至少 1 名同租户成员**时,
  138. /// 才允许为该租户发布 RoleCode 版流程定义。
  139. ///
  140. /// 这道门禁的意义:不得把一个「目前还靠 legacy 跨租户 RoleId 正常工作」的租户,
  141. /// 迁成 0 审批人。成员不齐的租户维持原状(继续命中 legacy 定义),等它齐了下次启动自动补发。
  142. /// </summary>
  143. public static List<long> GetActivatableTenantIds(ISqlSugarClient db)
  144. {
  145. var ready = new List<long>();
  146. foreach (var tenantId in ResolveTenantIds(db))
  147. {
  148. var allReady = true;
  149. foreach (var spec in RoleSpecs)
  150. {
  151. var roleId = ResolveLocalRoleId(db, tenantId, spec.Code);
  152. if (roleId is not > 0) { allReady = false; break; }
  153. var memberIds = db.Queryable<SysUserRole>()
  154. .ClearFilter()
  155. .Where(ur => ur.RoleId == roleId.Value)
  156. .Select(ur => ur.UserId)
  157. .ToList();
  158. if (memberIds.Count == 0) { allReady = false; break; }
  159. // 成员必须与角色同租户——审批人解析最终按 SysUser.TenantId 过滤,
  160. // 只数绑定行会把「有绑定但没有同租户成员」误判为 ready。
  161. var sameTenantMembers = db.Queryable<SysUser>()
  162. .ClearFilter()
  163. .Count(u => memberIds.Contains(u.Id) && u.TenantId == tenantId);
  164. if (sameTenantMembers == 0) { allReady = false; break; }
  165. }
  166. if (allReady) ready.Add(tenantId);
  167. }
  168. return ready;
  169. }
  170. /// <summary>
  171. /// 租户清单一律取自项目正式注册表 SysTenant(含默认租户),不自行构造。
  172. /// </summary>
  173. private static List<long> ResolveTenantIds(ISqlSugarClient db)
  174. {
  175. var ids = db.Queryable<SysTenant>()
  176. .ClearFilter<ITenantIdFilter>()
  177. .Where(x => x.Id > 1)
  178. .Select(x => x.Id)
  179. .ToList();
  180. if (!ids.Contains(SqlSugarConst.DefaultTenantId))
  181. ids.Add(SqlSugarConst.DefaultTenantId);
  182. return ids.Distinct().ToList();
  183. }
  184. }