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