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