using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
///
/// S8-RULE-GOVERNANCE-BATCH1:监控规则的**代码定义**。
///
/// 它解决什么:在此之前,一条规则的全部业务语义都存在 ado_s8_watch_rule 里——
/// rule_type / dataset_code / source_object_type 是列,
/// 而 due_at 取哪一列、哪些状态算已完成、去重身份是什么、建什么异常类型,
/// 全部埋在 params_json 这个自由文本里。于是「改判定口径」与「改轮询间隔」
/// 走的是同一个配置接口、同一份权限、同一次保存 —— 业务用户可以在页面上把
/// completedStates 改成 ["OPEN"],规则立刻换一套业务含义,而没有任何评审。
///
/// 本类的定位:规则的业务语义从此**只存在于代码里**,随 git 走,随 release 发。
/// 数据库上那几个同名列降级为 provisioning 维护的只读投影:它们仍然被 SQL 谓词与既有索引使用
/// (如 PickReadyRulesAsync 的 dataset_code != ''),但**不再是运行时语义的真源**。
/// 有人手工改了那几个列,规则的行为不变。
///
/// 为什么镜像 而不是另造一套:
/// 数据集侧早已是「代码声明定义 + Catalog 聚合 + 运行期按声明校验」,
/// 规则侧是同一个问题的同一种解法。仓内不应出现两种风格的 Catalog。
///
/// 刻意不做的事:不引入 DSL、不引入表达式、不引入
/// Dictionary<string, object>。定义是强类型的 —— 它要能被编译器和单元测试检查,
/// 而不是被"运行到那一行才知道配错了"检查。
///
public sealed class S8RuleDefinition
{
/// 规则人读编码。全链身份:dedup_key / detection_log.rule_code / exception.source_rule_code 都引用它。
public string RuleCode { get; init; } = string.Empty;
/// 业务名称。ado_s8_watch_rule 无此列,配置页的「规则名称」只能来自这里。
public string DisplayName { get; init; } = string.Empty;
/// 业务说明。同上,配置页「业务说明」的唯一来源。面向业务读者,不写表名 / 列名 / SQL。
public string Description { get; init; } = string.Empty;
/// 取数数据集编码。必须在 中已定义。
public string DatasetCode { get; init; } = string.Empty;
/// 规则类型 TIMEOUT / SHORTAGE / OUT_OF_RANGE。调度器据此分派 evaluator。
public string RuleType { get; init; } = string.Empty;
/// 报警机制 MANUAL_REPORT / DATE / RATIO / VALUE_RANGE。由 决定,不独立选择。
public string RuleMechanism { get; init; } = string.Empty;
/// 源对象类型。**参与 dedup_key**,改动会让历史异常与新异常断代。
public string SourceObjectType { get; init; } = string.Empty;
/// 场景编码 S1–S7。
public string SceneCode { get; init; } = string.Empty;
/// S_STAGE 维度节点。由 派生,不独立选择。
public string StageCode { get; init; } = string.Empty;
/// ORDER_FLOW 维度节点;可空。
public string? OrderFlowCode { get; init; }
/// 建单时使用的异常类型编码。决定 SLA / 通知分层 / Workflow 绑定,因此必须是定义而非参数。
public string ExceptionTypeCode { get; init; } = string.Empty;
/// TIMEOUT 判定语义。 = TIMEOUT 时必填。
public S8TimeoutSemantics? Timeout { get; init; }
/// SHORTAGE 判定语义。 = SHORTAGE 时必填。
public S8ShortageSemantics? Shortage { get; init; }
/// OUT_OF_RANGE 判定语义。 = OUT_OF_RANGE 时必填。
public S8OutOfRangeSemantics? OutOfRange { get; init; }
/// 本规则开放给租户调整的运行参数白名单与取值域。
public S8RuleParameterPolicy Parameters { get; init; } = new();
}
///
/// TIMEOUT 判定语义。
///
/// 这些是列名,但不是"让用户填的列名":它们描述 canonical 行契约里哪一列承载到期时间、
/// 哪一列承载状态。Provider 按 产出行,因此默认值就是 canonical 名;
/// 声明出来是为了让「判定读了哪一列」这件事有一处可被测试断言的书面记录,
/// 而不是散在 evaluator 的 if 分支里。
///
public sealed class S8TimeoutSemantics
{
/// 到期时间列。
public string DueAtColumn { get; init; } = S8CanonicalColumns.DueAt;
/// 状态列。
public string StatusColumn { get; init; } = S8CanonicalColumns.Status;
/// 去重身份列(写入 exception.source_object_id 并参与 dedup_key)。
public string SourceObjectIdColumn { get; init; } = S8CanonicalColumns.SourceObjectId;
/// 关联单号列(写入 exception.related_object_code)。
public string RelatedObjectCodeColumn { get; init; } = S8CanonicalColumns.RelatedObjectCode;
///
/// 视为「已完成、不再超期」的状态集合。比对**忽略大小写**(沿用既有 evaluator 口径)。
/// 这是最典型的「看起来像参数、实际是业务定义」的字段:改一个值,同一条规则就换了一套业务含义。
///
public IReadOnlyList CompletedStates { get; init; } = Array.Empty();
}
/// SHORTAGE 判定语义。当前无任何规则使用(仓内尚无 SHORTAGE 定义),保留以保证三类 evaluator 结构一致。
public sealed class S8ShortageSemantics
{
public string TargetQtyColumn { get; init; } = S8CanonicalColumns.TargetQty;
public string ActualQtyColumn { get; init; } = S8CanonicalColumns.ActualQty;
public string SourceObjectIdColumn { get; init; } = S8CanonicalColumns.SourceObjectId;
public string RelatedObjectCodeColumn { get; init; } = S8CanonicalColumns.RelatedObjectCode;
/// 绝对容差。缺口需**大于**该值才命中。
public decimal ToleranceAbs { get; init; }
/// 比例容差(0–1)。缺口占目标的比例需**大于**该值才命中。
public decimal ToleranceRatio { get; init; }
}
/// OUT_OF_RANGE 判定语义。当前无任何规则使用,同上。
public sealed class S8OutOfRangeSemantics
{
public string MeasuredValueColumn { get; init; } = S8CanonicalColumns.MeasuredValue;
public string SourceObjectIdColumn { get; init; } = S8CanonicalColumns.SourceObjectId;
public string RelatedObjectCodeColumn { get; init; } = S8CanonicalColumns.RelatedObjectCode;
/// 行内下限列;为空表示使用 固定值。
public string? LowerBoundColumn { get; init; }
/// 行内上限列;为空表示使用 固定值。
public string? UpperBoundColumn { get; init; }
public decimal? LowerBound { get; init; }
public decimal? UpperBound { get; init; }
public decimal ToleranceAbs { get; init; }
public decimal ToleranceRatio { get; init; }
}
///
/// 运行参数白名单与取值域。
///
/// 这里回答的是一个很窄的问题:租户管理员能改什么,改到什么范围。
/// 不在本策略里的字段,就没有任何 API 能改到——不是"前端没做入口",是写模型里根本不存在该字段。
///
/// 取值域随定义走而不是写死在 service:不同规则对「宽限多久算合理」的判断本就不同,
/// 而把它写在 service 的 if 里,等于把业务口径藏进了实现。
///
public sealed class S8RuleParameterPolicy
{
public int PollIntervalSecondsMin { get; init; } = 60;
public int PollIntervalSecondsMax { get; init; } = 86400;
public int PollIntervalSecondsDefault { get; init; } = 300;
public int TriggerCountRequiredMin { get; init; } = 1;
public int TriggerCountRequiredMax { get; init; } = 10;
public int TriggerCountRequiredDefault { get; init; } = 1;
public int RecoverCountRequiredMin { get; init; } = 1;
public int RecoverCountRequiredMax { get; init; } = 10;
public int RecoverCountRequiredDefault { get; init; } = 1;
///
/// 宽限分钟下限恒为 0。**刻意不设业务上限**:本批没有依据判断「多久算过长」,
/// 凭空设一个上限会在没有任何证据的情况下否掉合法配置。
///
public int GraceMinutesMin { get; init; } = 0;
public int GraceMinutesMax { get; init; } = int.MaxValue;
public int GraceMinutesDefault { get; init; } = 0;
///
/// 允许的严重度。**刻意只有两值**:S8SeverityCode.IsValid 是宽松六值版
/// (含 LOW/MEDIUM/HIGH/CRITICAL),那是给 legacy 查询参数兼容用的,
/// 拿来当写入门禁会直接放行 legacy 值(DB 里 severity='HIGH' 那一行正是这样进来的)。
///
public IReadOnlyList AllowedSeverities { get; init; } =
new[] { S8SeverityCode.Follow, S8SeverityCode.Serious };
public string SeverityDefault { get; init; } = S8SeverityCode.Follow;
/// 是否允许配置发生 / 责任部门兜底。
public bool AllowsDepartmentDefaults { get; init; } = true;
}