using Admin.NET.Core;
using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
using SqlSugar;
namespace Admin.NET.Plugin.AiDOP.DataPlatform.S0Dim;
///
/// 解析 S0 标准层自动刷新的作用域:哪些租户 × 哪些维度。
///
/// 租户权威 = SysTenant(Status = 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);
}
}