S7TenantAuthoritySeed.cs 10 KB

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