S0DimScopeCatalog.cs 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  3. using SqlSugar;
  4. namespace Admin.NET.Plugin.AiDOP.DataPlatform.S0Dim;
  5. /// <summary>
  6. /// 解析 S0 标准层自动刷新的作用域:哪些租户 × 哪些维度。
  7. ///
  8. /// <para><b>租户权威 = <c>SysTenant</c>(<c>Status = Enable</c>)</b>,与
  9. /// <c>MdpRebuildScopeCatalog</c> / <c>S1MdpScopeCatalog</c> 同源。
  10. /// ⚠️ 表名是 PascalCase 的 <c>SysTenant</c>,不是 <c>sys_tenant</c>——查错名字会得到
  11. /// 「表不存在」并误判成「框架表不可达」。</para>
  12. ///
  13. /// <para>🔴 <b>与 S1/MdpRebuild 的唯一差异:不排除默认租户。</b>
  14. /// 那两个 catalog 都写了 <c>x.Id != SqlSugarConst.DefaultTenantId</c>,
  15. /// 但 S0 照抄会造成实际数据缺口 —— 实测默认租户 <c>1300000000001</c> 在
  16. /// <c>ImageType</c> 与 6 张 <c>qms_*</c> 字典里共有 <b>28 行真实数据</b>,
  17. /// 却 <b>从未刷新过一次</b>(<c>mdp_transform_run_log</c> 中该租户的 S0 记录数 = 0)。
  18. /// 根因是现有 HTTP 入口对它<b>结构性不可达</b>:
  19. /// <c>AdoS0DimOpsAuthorizeAttribute</c> 只放行超管,而 <c>AdoS0TenantScope</c>
  20. /// 又拒绝「超管 + 目标租户 = 默认租户」的组合,两道门的交集是空集。
  21. /// 定时作业不经过这两道 HTTP 门,正好补上这个洞。</para>
  22. ///
  23. /// <para><b>维度权威 = <see cref="S0DimCatalog.All"/></b>,动态遍历,不维护第二份 key 清单。
  24. /// 但要**减去 <c>mdp_entity.status != 1</c> 的已停用对象**:
  25. /// <c>S0DimMaterializer.AssertEntityContractAsync</c> 对停用对象是**抛异常**的,
  26. /// 手工触发时这是合理的 fail-fast(人主动停用了就该报错),
  27. /// 但自动作业每一拍都会为它产出一条 FAILED,把真实告警淹没。
  28. /// 所以「停用」要在入队前就与「失败」分开。</para>
  29. /// </summary>
  30. public sealed class S0DimScopeCatalog : ITransient
  31. {
  32. private readonly ISqlSugarClient _db;
  33. /// <summary>注入本库客户端。</summary>
  34. public S0DimScopeCatalog(ISqlSugarClient db) => _db = db;
  35. /// <summary>自动刷新的作用域。</summary>
  36. /// <param name="Tenants">启用租户(含默认租户,见类型注释)。</param>
  37. /// <param name="Definitions">已启用的维度定义(已剔除 <c>mdp_entity.status != 1</c>)。</param>
  38. /// <param name="DisabledKeys">被剔除的维度 key,供日志外化,不静默跳过。</param>
  39. public sealed record Scope(
  40. IReadOnlyList<long> Tenants,
  41. IReadOnlyList<S0DimDefinition> Definitions,
  42. IReadOnlyList<string> DisabledKeys);
  43. /// <summary>解析当前应覆盖的租户与维度。</summary>
  44. public async Task<Scope> ResolveAsync(CancellationToken ct = default)
  45. {
  46. var tenants = await _db.Queryable<SysTenant>()
  47. .Where(x => x.Status == StatusEnum.Enable)
  48. .Select(x => x.Id)
  49. .ToListAsync(ct);
  50. var disabled = (await _db.Queryable<MdpEntity>()
  51. .Where(x => x.EntityCode.StartsWith("S0_") && x.Status != 1)
  52. .Select(x => x.EntityCode)
  53. .ToListAsync(ct))
  54. .ToHashSet(StringComparer.Ordinal);
  55. var defs = S0DimCatalog.All.Where(d => !disabled.Contains(d.EntityCode)).ToList();
  56. var disabledKeys = S0DimCatalog.All.Where(d => disabled.Contains(d.EntityCode))
  57. .Select(d => d.Key).ToList();
  58. return new Scope(tenants.Distinct().OrderBy(x => x).ToList(), defs, disabledKeys);
  59. }
  60. }