namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Health; /// Authority 的快照语义种类。决定除「生产运行是否成功」之外还要不要校验表级不变量。 public static class S8AuthorityKind { /// 当前态镜像表(如 mdp_std_so):upsert 覆盖、无发布步骤、读者不按批次取数 ⇒ 无表级不变量可校验。 public const string CurrentStateMirror = "CURRENT_STATE_MIRROR"; /// 当前快照表(如 dwd_requirement_examine_detail):按批次累积 + 末尾原子翻牌发布 ⇒ 必须校验发布不变量。 public const string PublishedSnapshot = "PUBLISHED_SNAPSHOT"; } /// /// 数据集的 Authority 声明:健康判定需要知道的一切,全部声明在数据集定义处。 /// /// 为什么放在数据集定义而不是规则上:不变量是 per-dataset 的。 /// Rule02 与 Rule03 共用同一个生产者作业(实测 job_code 同为 S1_MDP_SYNC_TRANSFORM、 /// 同一次 run、同一个 batch_id),仅靠运行日志无法区分二者;把不变量挂在数据集上, /// Resolver 内部就不需要 switch (ruleCode) /// /// 调度器不感知物理表名:它只传 DatasetCode, /// 由 MDP 适配实现去查具体的表与运行日志。 /// public sealed class S8AuthoritySpec { /// 生产该 Authority 的转换作业编码,对应 mdp_transform_run_log.job_code public string ProducerJobCode { get; init; } = string.Empty; /// public string AuthorityKind { get; init; } = S8AuthorityKind.CurrentStateMirror; /// /// 最近一次成功距今超过该时长即判 STALE。 /// /// 必须显式声明,不从作业周期推导。实测四条理由: /// ① 声明周期 60 分钟,而同租户实际相邻成功间隔为 28–39 分钟,声明值高估; /// ② 近 14 天 3530 个间隔中,最大 8.04 小时、超过 3 小时的有 46 次 —— /// 若按「3 × 周期 = 3 小时」判定,14 天内会产生 46 次假 STALE; /// ③ 触发器调度的是「入队」而非「执行」,每租户实际起跑时间还叠加了不可控的队列延迟; /// ④ 触发器 Args 的单位与形态不统一(PeriodTrigger 与 CronTrigger 混存, /// [Period(ms)][PeriodSeconds(s)] 都序列化成裸数字)。 /// 且该表可被作业管理界面在运行期改写,不应让门禁阈值随之漂移。 /// public TimeSpan StaleWindow { get; init; } = TimeSpan.FromHours(12); /// /// 查找「最近一次成功」时的回溯窗口。不是可选优化: /// 实测无界回溯在长期停摆的租户上单次耗时 146ms(反向扫描 7122 条索引项), /// 加 7 天上界后降到 7.3ms。且语义上等价 —— 「回溯窗口内没有成功」本身就是 STALE 结论。 /// public TimeSpan LookbackWindow { get; init; } = TimeSpan.FromDays(7); /// /// 生产者是否可信。 /// false 表示该 Authority 存在多个写入方、且部分写入方不写运行日志, /// 于是 run-log 无法证明表里最终的数据出自谁 —— 此时一律判 UNKNOWN、拦截恢复。 /// 这是 Rule01 的现状(dwd_supplier_delivery 除 MDP 转换外, /// 还有两个 API 路径直写且不留 run-log)。 /// public bool ProducerTrusted { get; init; } = true; /// ProducerTrusted 为 false 时的原因码,见 public string UntrustedReasonCode { get; init; } = S8AuthorityHealthReason.Rule01MultiWriterUntrusted; // ── 以下仅 AuthorityKind = PUBLISHED_SNAPSHOT 时使用 ────────────────────────────── // 声明在这里而不是写死在 Resolver 里,是为了让 Resolver 保持通用: // 它只按声明去查,内部不出现 switch (datasetCode) 或 switch (tableName)。 // 调度器仍然只传 DatasetCode,不接触任何物理表名。 /// 快照表名。 public string? SnapshotTable { get; init; } /// 快照表里标识批次的列。 public string SnapshotBatchColumn { get; init; } = "calc_batch_id"; /// 快照表里标识「属于当前批次」的列(值为 1 即当前)。 public string SnapshotCurrentFlagColumn { get; init; } = "is_current_flag"; /// /// 当前快照内不允许为空的列。 /// 绝对计数校验(COUNT(*) WHERE 当前 AND 列 IS NULL 必须为 0), /// 而不是空值率 —— 计数在空快照上天然真空成立,与 HEALTHY_EMPTY 零冲突; /// 写成比率则会在空集上 0 除、语义未定义。 /// 只填「结构上不可能为空」的列。例如 bom_level不能放进来 —— /// 转换侧对非数字层级刻意留 NULL("脏值留 NULL,不猜层级"),把它列为必填等于按设计制造假 DEGRADED。 /// public string[] SnapshotRequiredColumns { get; init; } = Array.Empty(); }