S8RuleDefinition.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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>
  52. /// 判定基准的**业务语言**说明,供配置页只读展示。
  53. ///
  54. /// <para>刻意与 <see cref="S8TimeoutSemantics.DueAtColumn"/> 分开:那是 canonical 列名(技术契约),
  55. /// 直接摆给规则管理员看等于泄漏实现细节,而且 <c>due_at</c> 三个字也回答不了
  56. /// 「这个日期到底是谁承诺的」。这里写的是业务读者能据以判断"口径对不对"的那句话。</para>
  57. ///
  58. /// <para>不得写入表名 / 列名 / SQL 片段 —— 配置页面向的是业务管理员,不是 DBA。</para>
  59. /// </summary>
  60. public string JudgementSummary { get; init; } = string.Empty;
  61. /// <summary>
  62. /// 去重身份的业务语言说明(如"采购订单号 + 行号")。
  63. /// 让管理员能判断"同一个对象反复报警会不会被合并",而不必理解 dedup_key 的拼接格式。
  64. /// </summary>
  65. public string DedupIdentitySummary { get; init; } = string.Empty;
  66. /// <summary>TIMEOUT 判定语义。<see cref="RuleType"/> = TIMEOUT 时必填。</summary>
  67. public S8TimeoutSemantics? Timeout { get; init; }
  68. /// <summary>SHORTAGE 判定语义。<see cref="RuleType"/> = SHORTAGE 时必填。</summary>
  69. public S8ShortageSemantics? Shortage { get; init; }
  70. /// <summary>OUT_OF_RANGE 判定语义。<see cref="RuleType"/> = OUT_OF_RANGE 时必填。</summary>
  71. public S8OutOfRangeSemantics? OutOfRange { get; init; }
  72. /// <summary>本规则开放给租户调整的运行参数白名单与取值域。</summary>
  73. public S8RuleParameterPolicy Parameters { get; init; } = new();
  74. }
  75. /// <summary>
  76. /// TIMEOUT 判定语义。
  77. ///
  78. /// <para><b>这些是列名,但不是"让用户填的列名"</b>:它们描述 canonical 行契约里哪一列承载到期时间、
  79. /// 哪一列承载状态。Provider 按 <see cref="S8CanonicalColumns"/> 产出行,因此默认值就是 canonical 名;
  80. /// 声明出来是为了让「判定读了哪一列」这件事有一处可被测试断言的书面记录,
  81. /// 而不是散在 evaluator 的 if 分支里。</para>
  82. /// </summary>
  83. public sealed class S8TimeoutSemantics
  84. {
  85. /// <summary>到期时间列。</summary>
  86. public string DueAtColumn { get; init; } = S8CanonicalColumns.DueAt;
  87. /// <summary>状态列。</summary>
  88. public string StatusColumn { get; init; } = S8CanonicalColumns.Status;
  89. /// <summary>去重身份列(写入 <c>exception.source_object_id</c> 并参与 dedup_key)。</summary>
  90. public string SourceObjectIdColumn { get; init; } = S8CanonicalColumns.SourceObjectId;
  91. /// <summary>关联单号列(写入 <c>exception.related_object_code</c>)。</summary>
  92. public string RelatedObjectCodeColumn { get; init; } = S8CanonicalColumns.RelatedObjectCode;
  93. /// <summary>
  94. /// 视为「已完成、不再超期」的状态集合。比对**忽略大小写**(沿用既有 evaluator 口径)。
  95. /// 这是最典型的「看起来像参数、实际是业务定义」的字段:改一个值,同一条规则就换了一套业务含义。
  96. /// </summary>
  97. public IReadOnlyList<string> CompletedStates { get; init; } = Array.Empty<string>();
  98. }
  99. /// <summary>SHORTAGE 判定语义。当前无任何规则使用(仓内尚无 SHORTAGE 定义),保留以保证三类 evaluator 结构一致。</summary>
  100. public sealed class S8ShortageSemantics
  101. {
  102. public string TargetQtyColumn { get; init; } = S8CanonicalColumns.TargetQty;
  103. public string ActualQtyColumn { get; init; } = S8CanonicalColumns.ActualQty;
  104. public string SourceObjectIdColumn { get; init; } = S8CanonicalColumns.SourceObjectId;
  105. public string RelatedObjectCodeColumn { get; init; } = S8CanonicalColumns.RelatedObjectCode;
  106. /// <summary>绝对容差。缺口需**大于**该值才命中。</summary>
  107. public decimal ToleranceAbs { get; init; }
  108. /// <summary>比例容差(0–1)。缺口占目标的比例需**大于**该值才命中。</summary>
  109. public decimal ToleranceRatio { get; init; }
  110. }
  111. /// <summary>OUT_OF_RANGE 判定语义。当前无任何规则使用,同上。</summary>
  112. public sealed class S8OutOfRangeSemantics
  113. {
  114. public string MeasuredValueColumn { get; init; } = S8CanonicalColumns.MeasuredValue;
  115. public string SourceObjectIdColumn { get; init; } = S8CanonicalColumns.SourceObjectId;
  116. public string RelatedObjectCodeColumn { get; init; } = S8CanonicalColumns.RelatedObjectCode;
  117. /// <summary>行内下限列;为空表示使用 <see cref="LowerBound"/> 固定值。</summary>
  118. public string? LowerBoundColumn { get; init; }
  119. /// <summary>行内上限列;为空表示使用 <see cref="UpperBound"/> 固定值。</summary>
  120. public string? UpperBoundColumn { get; init; }
  121. public decimal? LowerBound { get; init; }
  122. public decimal? UpperBound { get; init; }
  123. public decimal ToleranceAbs { get; init; }
  124. public decimal ToleranceRatio { get; init; }
  125. }
  126. /// <summary>
  127. /// 运行参数白名单与取值域。
  128. ///
  129. /// <para>这里回答的是一个很窄的问题:<b>租户管理员能改什么,改到什么范围</b>。
  130. /// 不在本策略里的字段,就没有任何 API 能改到——不是"前端没做入口",是写模型里根本不存在该字段。</para>
  131. ///
  132. /// <para>取值域随定义走而不是写死在 service:不同规则对「宽限多久算合理」的判断本就不同,
  133. /// 而把它写在 service 的 if 里,等于把业务口径藏进了实现。</para>
  134. /// </summary>
  135. public sealed class S8RuleParameterPolicy
  136. {
  137. public int PollIntervalSecondsMin { get; init; } = 60;
  138. public int PollIntervalSecondsMax { get; init; } = 86400;
  139. public int PollIntervalSecondsDefault { get; init; } = 300;
  140. public int TriggerCountRequiredMin { get; init; } = 1;
  141. public int TriggerCountRequiredMax { get; init; } = 10;
  142. public int TriggerCountRequiredDefault { get; init; } = 1;
  143. public int RecoverCountRequiredMin { get; init; } = 1;
  144. public int RecoverCountRequiredMax { get; init; } = 10;
  145. public int RecoverCountRequiredDefault { get; init; } = 1;
  146. /// <summary>
  147. /// 宽限分钟下限恒为 0。**刻意不设业务上限**:本批没有依据判断「多久算过长」,
  148. /// 凭空设一个上限会在没有任何证据的情况下否掉合法配置。
  149. /// </summary>
  150. public int GraceMinutesMin { get; init; } = 0;
  151. public int GraceMinutesMax { get; init; } = int.MaxValue;
  152. public int GraceMinutesDefault { get; init; } = 0;
  153. /// <summary>
  154. /// 允许的严重度。**刻意只有两值**:<c>S8SeverityCode.IsValid</c> 是宽松六值版
  155. /// (含 LOW/MEDIUM/HIGH/CRITICAL),那是给 legacy 查询参数兼容用的,
  156. /// 拿来当写入门禁会直接放行 legacy 值(DB 里 severity='HIGH' 那一行正是这样进来的)。
  157. /// </summary>
  158. public IReadOnlyList<string> AllowedSeverities { get; init; } =
  159. new[] { S8SeverityCode.Follow, S8SeverityCode.Serious };
  160. public string SeverityDefault { get; init; } = S8SeverityCode.Follow;
  161. /// <summary>是否允许配置发生 / 责任部门兜底。</summary>
  162. public bool AllowsDepartmentDefaults { get; init; } = true;
  163. /// <summary>
  164. /// S8-RULE-READINESS-1:这两个部门参数是否为<b>启用的前置条件</b>。
  165. ///
  166. /// <para>默认 <c>false</c>,且刻意如此:这不是 S8 的全局规则,而是<b>逐规则</b>的声明。
  167. /// 数据集自带部门列的规则(命中行能直接给出 occurrence / responsible)不需要它们,
  168. /// 在全局写死"必须配部门"会把那类规则一起误伤。</para>
  169. ///
  170. /// <para>置 <c>true</c> 的判据只有一条:<b>该规则的数据集拿不出部门</b>,
  171. /// 因而不配这两项就必然建单失败。Rule 01 正是如此 ——
  172. /// <c>dwd_supplier_delivery</c> 没有任何部门列。</para>
  173. ///
  174. /// <para>由 <see cref="S8RuleReadinessGate"/> 在 Enable / RunNow / Scheduler 三条入口统一消费。
  175. /// 与 <see cref="AllowsDepartmentDefaults"/> 是两件事:后者管"能不能配",本项管"必须配"。</para>
  176. /// </summary>
  177. public bool RequiresDepartmentDefaultsForEnable { get; init; }
  178. /// <summary>
  179. /// S8-RESPONSIBILITY-POOL-1:启用前是否必须已配置<b>处理账号池</b>。
  180. ///
  181. /// <para><b>本项是把既有判据显式化,不是新增门禁。</b>处理池判据原先写在
  182. /// <see cref="S8RuleReadinessGate"/> 里、却位于 <see cref="RequiresDepartmentDefaultsForEnable"/>
  183. /// 的提前返回<b>之后</b> —— 于是它事实上只对"必须配部门"的规则生效,
  184. /// 对其余规则被静默跳过。这种「两件事被一个开关捆在一起」的耦合,
  185. /// 在新增复核 / 升级两条判据时会立刻放大成三重耦合。</para>
  186. ///
  187. /// <para>拆出独立开关后,各条判据由各自的声明驱动;当前取值刻意与拆分前的
  188. /// <b>实际行为完全一致</b>(仅 Rule 01 为 <c>true</c>),本批不顺带改变任何规则的启用条件。</para>
  189. /// </summary>
  190. public bool RequiresHandlerPoolForEnable { get; init; }
  191. /// <summary>
  192. /// S8-RESPONSIBILITY-POOL-1:该规则产生的异常,处理流程是否<b>包含复核环节</b>。
  193. ///
  194. /// <para>为 <c>true</c> 时,启用前必须已配置至少一名有效的<b>复核账号</b> ——
  195. /// 否则处理人做完点「提交复核」会发现候选人列表是空的,单据卡在
  196. /// IN_PROGRESS 出不去,而规则本身一路显示正常。</para>
  197. ///
  198. /// <para><b>不做成全局要求</b>:将来若有规则处理完即闭环、根本不走复核,
  199. /// 强制它配复核人只会逼出一个占位账号 —— 那比没有配置更危险。</para>
  200. /// </summary>
  201. public bool RequiresVerification { get; init; }
  202. /// <summary>
  203. /// S8-RESPONSIBILITY-POOL-1:该规则的异常是否参与<b>超时自动升级</b>。
  204. ///
  205. /// <para>为 <c>true</c> 时,启用前必须已配置至少一名有效的<b>升级账号</b>,
  206. /// 且超时升级作业按<b>本规则的升级账号池</b>解析收件人。</para>
  207. ///
  208. /// <para><b>取代 <c>exception_type.escalate_role_code</c> 成为 S8 规则链路的升级 authority</b>:
  209. /// 旧字段是跨租户扫描时按异常类型查的,实测会把 B 租户的 <c>escalate_role_code</c>
  210. /// 用到 A 租户的异常上(见 <c>S8TimeoutAutoEscalationService</c> 原注释)。
  211. /// 责任池天然带 <c>tenant_id</c>,不存在这条越界路径。旧字段保留供其它模块兼容,
  212. /// S8 规则链路不再依赖。</para>
  213. /// </summary>
  214. public bool SupportsTimeoutEscalation { get; init; }
  215. }