using System.Text.Json.Serialization; namespace Admin.NET.Plugin.AiDOP.DataPlatform.S0Dim; /// /// source / dim 两侧属性校验和的一次读数(countchecksum **成对**,缺一不可)。 /// /// 唯一产出, /// 供两处共用:① ReconcileAsync 的 Layer 3 写后对账; /// ② 的写前闸门。两者共用同一读取路径, /// 保证「写前判等」与「写后对账」不会各跑一套口径。 /// public sealed class S0DimChecksumPair { /// 源侧参与校验和的行数(按租户)。 public int SourceCount { get; set; } /// dim 侧参与校验和的行数(按租户)。 public int DimCount { get; set; } /// /// 源侧校验和原值。DECIMAL(40,0) 的精确整数,**不外化为 JSON 数字** —— /// 20+ 位整数经 JSON/MySQL JSON 取出时会退化成 DOUBLE 丢精度,读者会看到一个错的数字。 /// 对外一律走 字符串。 /// [JsonIgnore] public decimal SourceChecksumValue { get; set; } /// [JsonIgnore] public decimal DimChecksumValue { get; set; } /// 源侧校验和(十进制字符串,与 同格式)。 public string SourceChecksum => SourceChecksumValue.ToString("F0"); /// dim 侧校验和(十进制字符串,与 同格式)。 public string DimChecksum => DimChecksumValue.ToString("F0"); /// /// 两侧完全一致。count 与 checksum 必须同时相等 —— 判据与 /// S0DimReconciler Layer 3 的失败条件逐字互补,不是「只比 checksum」。 /// public bool IsMatch => IsMatchOf(SourceCount, SourceChecksumValue, DimCount, DimChecksumValue); /// /// 校验和判等的**唯一**判据(纯函数,可单测)。 /// /// 🔴 count 不可省略SUM(RowHash) 是可交换的加法, /// 理论上不同行集可能求和相同;S0DimSqlBuilder.BuildSourceChecksumSql 的注释 /// 明确写了「必须与 COUNT 成对使用」。只比 checksum 会让闸门在极端情况下误判「无变化」 /// 而跳过一次本该执行的刷新。 /// public static bool IsMatchOf(int sourceCount, decimal sourceChecksum, int dimCount, decimal dimChecksum) => sourceCount == dimCount && sourceChecksum == dimChecksum; } /// 业务键集合三层等价的差集计数。全部为 0 才算通过。 public sealed class S0DimBizKeyDiff { /// source 有而 stg 没有。 public int SourceMinusStaging { get; set; } /// stg 有而 source 没有。 public int StagingMinusSource { get; set; } /// stg 有而 dim 没有。 public int StagingMinusDim { get; set; } /// dim 有而 stg 没有。 public int DimMinusStaging { get; set; } /// 四个方向是否全为 0。 public bool IsEqual => SourceMinusStaging == 0 && StagingMinusSource == 0 && StagingMinusDim == 0 && DimMinusStaging == 0; } /// 某一层业务键的质量(重复 / 空值)。 public sealed class S0DimBizKeyQuality { /// 层名:source / staging / dim。 public string Layer { get; set; } = ""; /// 总行数。 public int Total { get; set; } /// 去重后行数。 public int Distinct { get; set; } /// 空业务键行数(stg 侧出现即意味着 biz_key_expr 回落到了 source_row_id)。 public int Blank { get; set; } /// 无重复且无空值。 public bool IsClean => Total == Distinct && Blank == 0; } /// 单个维度的对账结果。 public sealed class S0DimReconcileResult { /// 维度 Key。 public string Key { get; set; } = ""; /// 租户。 public long TenantId { get; set; } /// 本轮批次(只读对账时可能为 null)。 public string? BatchId { get; set; } /// 源表行数(按租户)。 public int SourceCount { get; set; } /// staging 行数(四段谓词)。 public int StagingCount { get; set; } /// dim 行数(按租户)。 public int DimCount { get; set; } /// 源表中无法归属租户的行(整条链路不可见,WARN)。 public int SourceUntenanted { get; set; } /// dim 中不属于本批的残留行(FULL Replace 后必须为 0)。 public int DimBatchImpurity { get; set; } /// 业务键集合差集。 public S0DimBizKeyDiff BizKeyDiff { get; set; } = new(); /// 三层业务键质量。 public List BizKeyQuality { get; set; } = []; /// 源侧属性校验和。 public string SourceChecksum { get; set; } = "0"; /// dim 侧属性校验和。 public string DimChecksum { get; set; } = "0"; /// dim 业务键重复组数。 public int DimDuplicateBizKeys { get; set; } /// 镜像唯一性违例组数(未声明时为 -1 表示不适用)。 public int MirrorUniqueViolations { get; set; } = -1; /// /// 孤儿子行**真实总数**(WARN,不阻断;未声明父维度时为 0)。 /// 与 不同:后者受 OrphanSampleLimit 截断,只是样本。 /// public int OrphanChildCount { get; set; } /// 孤儿子行业务键**样本**(最多 OrphanSampleLimit 条)。总数看 public List OrphanChildren { get; set; } = []; /// 阻断级失败原因(为空表示通过)。 public List Failures { get; set; } = []; /// 告警(不阻断)。 public List Warnings { get; set; } = []; /// 无阻断级失败。 public bool IsPass => Failures.Count == 0; } /// 单个维度的物化结果。 public sealed class S0DimMaterializeResult { /// 本轮实际执行过刷新且写后对账通过。 public const string StatusSuccess = "SUCCESS"; /// 本轮失败,dim 停在上一轮的完整快照。 public const string StatusFailed = "FAILED"; /// /// 本轮**验证过** source 与 dim 完全一致,因此**没有执行**刷新。 /// /// 属「非失败」,但**不是** : /// SUCCESS 表示「跑过并验证成功」,本状态表示「验证后确认无需跑」。 /// 两者混同会让「自动刷新是否真的在工作」不可观测。 /// public const string StatusSkippedNoChange = "SKIPPED_NO_CHANGE"; /// 跳过原因:源与 dim 的 (count, checksum) 完全一致。 public const string SkipReasonNoChange = "NO_CHANGE"; /// 维度 Key。 public string Key { get; set; } = ""; /// 租户。 public long TenantId { get; set; } /// 批次。 public string BatchId { get; set; } = ""; /// purge 掉的 staging 行数。 public int StagingPurged { get; set; } /// 本批拉取到的源行数。 public int SourcePulled { get; set; } /// 本批写入 staging 的行数。 public int StagingWritten { get; set; } /// 写入 dim 的行数。 public int DimRows { get; set; } /// 对账结果。跳过时为 null —— 本轮没有写入,也就没有写后对账。 public S0DimReconcileResult? Reconcile { get; set; } /// /// 写前闸门读到的 source / dim 校验和读数。闸门执行过即非 null(无论是否跳过), /// 外化进 summary_json 供事后复核「当时凭什么判定无变化」。 /// public S0DimChecksumPair? Checksum { get; set; } /// 跳过原因(仅 时非 null)。 public string? SkipReason { get; set; } /// / / public string Status { get; set; } = StatusSuccess; /// 失败信息。 public string? Error { get; set; } } /// 一次刷新(可含多个维度)的整体结果。 public sealed class S0DimRefreshResult { /// 批次(同时是 stg/dim 的 sync_batch_id 与 run log 的 batch_id)。 public string BatchId { get; set; } = ""; /// 租户。 public long TenantId { get; set; } /// /// SUCCESS / SUCCESS_WITH_WARNING / SKIPPED_NO_CHANGE / FAILED / CANCELED。 /// SKIPPED_NO_CHANGE 仅在**全部**维度都被写前闸门跳过时出现。 /// public string Status { get; set; } = "SUCCESS"; /// 逐维度结果。 public List Items { get; set; } = []; /// 耗时(毫秒)。 public int DurationMs { get; set; } /// run log 主键。 public long RunLogId { get; set; } }