S8RuleDefinition.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  2. using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
  3. namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
  4. /// <summary>
  5. /// S8-RULE-GOVERNANCE-BATCH1:监控规则的**代码定义**。
  6. ///
  7. /// <para><b>它解决什么</b>:在此之前,一条规则的全部业务语义都存在 <c>ado_s8_watch_rule</c> 里——
  8. /// <c>rule_type</c> / <c>dataset_code</c> / <c>source_object_type</c> 是列,
  9. /// 而 <c>due_at</c> 取哪一列、哪些状态算已完成、去重身份是什么、建什么异常类型,
  10. /// 全部埋在 <c>params_json</c> 这个自由文本里。于是「改判定口径」与「改轮询间隔」
  11. /// 走的是同一个配置接口、同一份权限、同一次保存 —— 业务用户可以在页面上把
  12. /// <c>completedStates</c> 改成 <c>["OPEN"]</c>,规则立刻换一套业务含义,而没有任何评审。</para>
  13. ///
  14. /// <para><b>本类的定位</b>:规则的业务语义从此**只存在于代码里**,随 git 走,随 release 发。
  15. /// 数据库上那几个同名列降级为 <b>provisioning 维护的只读投影</b>:它们仍然被 SQL 谓词与既有索引使用
  16. /// (如 <c>PickReadyRulesAsync</c> 的 <c>dataset_code != ''</c>),但**不再是运行时语义的真源**。
  17. /// 有人手工改了那几个列,规则的行为不变。</para>
  18. ///
  19. /// <para><b>为什么镜像 <see cref="S8DatasetDefinition"/> 而不是另造一套</b>:
  20. /// 数据集侧早已是「代码声明定义 + Catalog 聚合 + 运行期按声明校验」,
  21. /// 规则侧是同一个问题的同一种解法。仓内不应出现两种风格的 Catalog。</para>
  22. ///
  23. /// <para><b>刻意不做的事</b>:不引入 DSL、不引入表达式、不引入
  24. /// <c>Dictionary&lt;string, object&gt;</c>。定义是强类型的 —— 它要能被编译器和单元测试检查,
  25. /// 而不是被"运行到那一行才知道配错了"检查。</para>
  26. /// </summary>
  27. public sealed class S8RuleDefinition
  28. {
  29. /// <summary>规则人读编码。全链身份:dedup_key / detection_log.rule_code / exception.source_rule_code 都引用它。</summary>
  30. public string RuleCode { get; init; } = string.Empty;
  31. /// <summary>业务名称。<c>ado_s8_watch_rule</c> 无此列,配置页的「规则名称」只能来自这里。</summary>
  32. public string DisplayName { get; init; } = string.Empty;
  33. /// <summary>业务说明。同上,配置页「业务说明」的唯一来源。面向业务读者,不写表名 / 列名 / SQL。</summary>
  34. public string Description { get; init; } = string.Empty;
  35. /// <summary>取数数据集编码。必须在 <see cref="IS8DatasetCatalog"/> 中已定义。</summary>
  36. public string DatasetCode { get; init; } = string.Empty;
  37. /// <summary>规则类型 TIMEOUT / SHORTAGE / OUT_OF_RANGE。调度器据此分派 evaluator。</summary>
  38. public string RuleType { get; init; } = string.Empty;
  39. /// <summary>报警机制 MANUAL_REPORT / DATE / RATIO / VALUE_RANGE。由 <see cref="RuleType"/> 决定,不独立选择。</summary>
  40. public string RuleMechanism { get; init; } = string.Empty;
  41. /// <summary>源对象类型。**参与 dedup_key**,改动会让历史异常与新异常断代。</summary>
  42. public string SourceObjectType { get; init; } = string.Empty;
  43. /// <summary>场景编码 S1–S7。</summary>
  44. public string SceneCode { get; init; } = string.Empty;
  45. /// <summary>S_STAGE 维度节点。由 <see cref="SceneCode"/> 派生,不独立选择。</summary>
  46. public string StageCode { get; init; } = string.Empty;
  47. /// <summary>ORDER_FLOW 维度节点;可空。</summary>
  48. public string? OrderFlowCode { get; init; }
  49. /// <summary>建单时使用的异常类型编码。决定 SLA / 通知分层 / Workflow 绑定,因此必须是定义而非参数。</summary>
  50. public string ExceptionTypeCode { get; init; } = string.Empty;
  51. /// <summary>TIMEOUT 判定语义。<see cref="RuleType"/> = TIMEOUT 时必填。</summary>
  52. public S8TimeoutSemantics? Timeout { get; init; }
  53. /// <summary>SHORTAGE 判定语义。<see cref="RuleType"/> = SHORTAGE 时必填。</summary>
  54. public S8ShortageSemantics? Shortage { get; init; }
  55. /// <summary>OUT_OF_RANGE 判定语义。<see cref="RuleType"/> = OUT_OF_RANGE 时必填。</summary>
  56. public S8OutOfRangeSemantics? OutOfRange { get; init; }
  57. /// <summary>本规则开放给租户调整的运行参数白名单与取值域。</summary>
  58. public S8RuleParameterPolicy Parameters { get; init; } = new();
  59. }
  60. /// <summary>
  61. /// TIMEOUT 判定语义。
  62. ///
  63. /// <para><b>这些是列名,但不是"让用户填的列名"</b>:它们描述 canonical 行契约里哪一列承载到期时间、
  64. /// 哪一列承载状态。Provider 按 <see cref="S8CanonicalColumns"/> 产出行,因此默认值就是 canonical 名;
  65. /// 声明出来是为了让「判定读了哪一列」这件事有一处可被测试断言的书面记录,
  66. /// 而不是散在 evaluator 的 if 分支里。</para>
  67. /// </summary>
  68. public sealed class S8TimeoutSemantics
  69. {
  70. /// <summary>到期时间列。</summary>
  71. public string DueAtColumn { get; init; } = S8CanonicalColumns.DueAt;
  72. /// <summary>状态列。</summary>
  73. public string StatusColumn { get; init; } = S8CanonicalColumns.Status;
  74. /// <summary>去重身份列(写入 <c>exception.source_object_id</c> 并参与 dedup_key)。</summary>
  75. public string SourceObjectIdColumn { get; init; } = S8CanonicalColumns.SourceObjectId;
  76. /// <summary>关联单号列(写入 <c>exception.related_object_code</c>)。</summary>
  77. public string RelatedObjectCodeColumn { get; init; } = S8CanonicalColumns.RelatedObjectCode;
  78. /// <summary>
  79. /// 视为「已完成、不再超期」的状态集合。比对**忽略大小写**(沿用既有 evaluator 口径)。
  80. /// 这是最典型的「看起来像参数、实际是业务定义」的字段:改一个值,同一条规则就换了一套业务含义。
  81. /// </summary>
  82. public IReadOnlyList<string> CompletedStates { get; init; } = Array.Empty<string>();
  83. }
  84. /// <summary>SHORTAGE 判定语义。当前无任何规则使用(仓内尚无 SHORTAGE 定义),保留以保证三类 evaluator 结构一致。</summary>
  85. public sealed class S8ShortageSemantics
  86. {
  87. public string TargetQtyColumn { get; init; } = S8CanonicalColumns.TargetQty;
  88. public string ActualQtyColumn { get; init; } = S8CanonicalColumns.ActualQty;
  89. public string SourceObjectIdColumn { get; init; } = S8CanonicalColumns.SourceObjectId;
  90. public string RelatedObjectCodeColumn { get; init; } = S8CanonicalColumns.RelatedObjectCode;
  91. /// <summary>绝对容差。缺口需**大于**该值才命中。</summary>
  92. public decimal ToleranceAbs { get; init; }
  93. /// <summary>比例容差(0–1)。缺口占目标的比例需**大于**该值才命中。</summary>
  94. public decimal ToleranceRatio { get; init; }
  95. }
  96. /// <summary>OUT_OF_RANGE 判定语义。当前无任何规则使用,同上。</summary>
  97. public sealed class S8OutOfRangeSemantics
  98. {
  99. public string MeasuredValueColumn { get; init; } = S8CanonicalColumns.MeasuredValue;
  100. public string SourceObjectIdColumn { get; init; } = S8CanonicalColumns.SourceObjectId;
  101. public string RelatedObjectCodeColumn { get; init; } = S8CanonicalColumns.RelatedObjectCode;
  102. /// <summary>行内下限列;为空表示使用 <see cref="LowerBound"/> 固定值。</summary>
  103. public string? LowerBoundColumn { get; init; }
  104. /// <summary>行内上限列;为空表示使用 <see cref="UpperBound"/> 固定值。</summary>
  105. public string? UpperBoundColumn { get; init; }
  106. public decimal? LowerBound { get; init; }
  107. public decimal? UpperBound { get; init; }
  108. public decimal ToleranceAbs { get; init; }
  109. public decimal ToleranceRatio { get; init; }
  110. }
  111. /// <summary>
  112. /// 运行参数白名单与取值域。
  113. ///
  114. /// <para>这里回答的是一个很窄的问题:<b>租户管理员能改什么,改到什么范围</b>。
  115. /// 不在本策略里的字段,就没有任何 API 能改到——不是"前端没做入口",是写模型里根本不存在该字段。</para>
  116. ///
  117. /// <para>取值域随定义走而不是写死在 service:不同规则对「宽限多久算合理」的判断本就不同,
  118. /// 而把它写在 service 的 if 里,等于把业务口径藏进了实现。</para>
  119. /// </summary>
  120. public sealed class S8RuleParameterPolicy
  121. {
  122. public int PollIntervalSecondsMin { get; init; } = 60;
  123. public int PollIntervalSecondsMax { get; init; } = 86400;
  124. public int PollIntervalSecondsDefault { get; init; } = 300;
  125. public int TriggerCountRequiredMin { get; init; } = 1;
  126. public int TriggerCountRequiredMax { get; init; } = 10;
  127. public int TriggerCountRequiredDefault { get; init; } = 1;
  128. public int RecoverCountRequiredMin { get; init; } = 1;
  129. public int RecoverCountRequiredMax { get; init; } = 10;
  130. public int RecoverCountRequiredDefault { get; init; } = 1;
  131. /// <summary>
  132. /// 宽限分钟下限恒为 0。**刻意不设业务上限**:本批没有依据判断「多久算过长」,
  133. /// 凭空设一个上限会在没有任何证据的情况下否掉合法配置。
  134. /// </summary>
  135. public int GraceMinutesMin { get; init; } = 0;
  136. public int GraceMinutesMax { get; init; } = int.MaxValue;
  137. public int GraceMinutesDefault { get; init; } = 0;
  138. /// <summary>
  139. /// 允许的严重度。**刻意只有两值**:<c>S8SeverityCode.IsValid</c> 是宽松六值版
  140. /// (含 LOW/MEDIUM/HIGH/CRITICAL),那是给 legacy 查询参数兼容用的,
  141. /// 拿来当写入门禁会直接放行 legacy 值(DB 里 severity='HIGH' 那一行正是这样进来的)。
  142. /// </summary>
  143. public IReadOnlyList<string> AllowedSeverities { get; init; } =
  144. new[] { S8SeverityCode.Follow, S8SeverityCode.Serious };
  145. public string SeverityDefault { get; init; } = S8SeverityCode.Follow;
  146. /// <summary>是否允许配置发生 / 责任部门兜底。</summary>
  147. public bool AllowsDepartmentDefaults { get; init; } = true;
  148. }