S8OrderDeliveryRuleDefinitions.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  2. using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
  3. using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess.Providers;
  4. namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
  5. /// <summary>
  6. /// S8-RULE02-ORDER-DELIVERY-WARNING-1:订单交付延期<b>预警</b>规则定义。
  7. ///
  8. /// <para><b>与 Rule 01 的本质区别</b>:Rule 01(采购交期延误)判的是「到期日已过而事情没做完」——
  9. /// 事后。本规则判的是「客户交期还没到,但系统自己算出来的建议交期已经晚于它」——事前。
  10. /// 前者用 TIMEOUT(时间轴过了没有),后者用 OUT_OF_RANGE(两个日期之差越了界)。</para>
  11. ///
  12. /// <para><b>为什么是 OUT_OF_RANGE 而不是新造一个 evaluator</b>:本规则的判据展开后就是
  13. /// 「<c>measured_value</c>(延期天数)&gt; 上限 0」,正是 OUT_OF_RANGE 的既有语义。
  14. /// 仓内此前没有任何 OUT_OF_RANGE 规则定义(见 <see cref="S8OutOfRangeRuleEvaluator"/> 类注释),
  15. /// <b>本规则是第一条</b>;evaluator 本身一行未改。</para>
  16. /// </summary>
  17. public sealed class S8OrderDeliveryRuleDefinitions : IS8RuleDefinitionSource
  18. {
  19. /// <summary>
  20. /// Rule 02 编码。命名沿用 Rule 01 的 <c>RULE_{模块}_{业务}</c> 格式。
  21. /// <b>参与 dedup_key,全链身份</b>:任何地方引用必须用本常量,不写字面量。
  22. /// </summary>
  23. public const string OrderDeliveryDelayWarningCode = "RULE_S1_ORDER_DELIVERY_DELAY_WARNING";
  24. /// <summary>
  25. /// 建单使用的异常类型。<b>已存在于种子</b>
  26. /// (<c>S8ExceptionTypeSeedData</c>:订单交付延期预警 / scene S1 / SLA 240 分钟 / 默认 FOLLOW),
  27. /// 因此本批<b>不新建异常类型、不写 migration</b>。
  28. /// </summary>
  29. public const string OrderDeliveryDelayWarningExceptionTypeCode = "ORDER_DELIVERY_DELAY_WARNING";
  30. /// <summary>
  31. /// 源对象类型。<b>参与 dedup_key</b>。
  32. /// <para>是「订单行」而不是「订单」:交期与数量都挂在行上,同一张订单的不同行交期可以不同。
  33. /// 用订单号做身份会让同单多行只留下一条异常,其余行的风险被静默吞掉。</para>
  34. /// </summary>
  35. public const string SalesOrderLineObjectType = "SALES_ORDER_LINE";
  36. public IEnumerable<S8RuleDefinition> GetDefinitions() => new[]
  37. {
  38. new S8RuleDefinition
  39. {
  40. RuleCode = OrderDeliveryDelayWarningCode,
  41. DisplayName = "订单交付延期预警",
  42. Description =
  43. "订单行的系统建议交期晚于客户要求交期,且客户交期尚未到达时提报预警。" +
  44. "延期天数由系统建议交期与客户要求交期相减得到,不做任何推算;" +
  45. "尚未做过评审资源检查(没有系统建议交期)的订单行不参与判定。",
  46. DatasetCode = S8OrderDeliveryDatasetDefinitions.OrderDeliveryRiskCode,
  47. RuleType = S8OutOfRangeRuleEvaluator.RuleTypeCode,
  48. RuleMechanism = "DATE",
  49. SourceObjectType = SalesOrderLineObjectType,
  50. SceneCode = S8ModuleCode.S1,
  51. StageCode = S8ModuleCode.S1,
  52. // 风险在「评审/排产/测算」环节产生:系统建议交期正是该环节的资源检查结果。
  53. OrderFlowCode = "ORDER_REVIEW_PLAN_CALC",
  54. ExceptionTypeCode = OrderDeliveryDelayWarningExceptionTypeCode,
  55. // ────────────────────────────────────────────────────────────────
  56. // S8-RULE-LIFECYCLE-CREATE-GATE-1:「未到期」是<b>立案</b>条件,不是<b>风险</b>条件。
  57. //
  58. // 三条语义各归各位,不再挤在一个「本轮是否命中」里:
  59. // 立案:ETA 晚于交期 且 交期尚未到达 ← 只有这一条要求"未到期"
  60. // 持续:ETA 晚于交期 ← 跨过交期照样成立,异常保持 active
  61. // 恢复:ETA 回到交期以内 ← 唯一合法的恢复
  62. //
  63. // 修的是一个已实测复现的假恢复:时间跨过客户交期时,风险其实是**兑现**了,
  64. // 而旧实现(数据集里带 plan_date >= NOW())会让这一行从结果集消失,
  65. // 恢复判定看到的与"风险解除"完全一样,于是把它标成已恢复。
  66. //
  67. // 代价是数据集会持续返回已过交期的风险行(真库当前 7 条)。它们照常命中、
  68. // 照常保护既有异常不被误判恢复,只是拿不到首次建单资格 —— 那些风险早已兑现,
  69. // 不是预警;接手它们的应当是将来的「订单交期已延误」规则,不是本规则。
  70. // ────────────────────────────────────────────────────────────────
  71. CreateOnlyBeforeDueAt = true,
  72. // 面向业务管理员的只读说明。刻意不出现表名 / 列名 / DATEDIFF ——
  73. // 配置页要回答的是「这条规则按什么判」,不是「它怎么实现的」。
  74. JudgementSummary =
  75. "系统按产能测算出的建议交期晚于客户要求的交期,且客户交期尚未到达时,提报预警;"
  76. + "晚出来的天数即预警的严重程度依据。"
  77. + "预警提出后,即使客户交期已经过去,只要建议交期仍然晚于要求交期,预警继续有效;"
  78. + "只有重新评审把建议交期拉回到要求交期以内,才算风险解除。"
  79. + "订单已关闭、或该订单行还没有系统建议交期时,不参与判定。",
  80. DedupIdentitySummary = "销售订单号 + 订单行号",
  81. // ────────────────────────────────────────────────────────────────
  82. // 判定语义。
  83. //
  84. // measured_value = 系统建议交期 - 客户要求交期(天)。由 Provider 用
  85. // DATEDIFF 算好后放在 canonical 的 measured_value 列,evaluator 只做越界比较。
  86. //
  87. // 上限 0 的含义:只要建议交期比要求交期晚哪怕一天就预警。
  88. // 这不是拍脑袋定的阈值 —— 0 是「不延期」这件事本身的定义。
  89. // 真正可调的宽容度留给 ToleranceAbs(见下),管理员要"晚 3 天以内不报"时
  90. // 改的是容差,而不是重新定义什么叫延期。
  91. //
  92. // 不设 LowerBound:建议交期<b>早于</b>要求交期是好事,不构成异常。
  93. // ────────────────────────────────────────────────────────────────
  94. OutOfRange = new S8OutOfRangeSemantics
  95. {
  96. MeasuredValueColumn = S8CanonicalColumns.MeasuredValue,
  97. // 去重身份 = bill_no + '#' + entry_seq,由 Provider 拼好放进 source_object_id。
  98. // 真库证据:crm_seorderentry 390 行,(bill_no, entry_seq) 重复组为 0。
  99. SourceObjectIdColumn = S8CanonicalColumns.SourceObjectId,
  100. RelatedObjectCodeColumn = S8CanonicalColumns.RelatedObjectCode,
  101. UpperBound = 0m,
  102. // ToleranceAbs = 0:本批没有任何业务依据说「晚几天以内可以不报」。
  103. // 凭空填一个 3 会在没有拍板的情况下把一批真实风险静默过滤掉。
  104. // 需要宽容度时由业务给出天数,再改这一处。
  105. ToleranceAbs = 0m,
  106. // ToleranceRatio 对本规则无意义:上限为 0 时 evaluator 会跳过比例判断
  107. // (避免除零),写非 0 值只会造成"配了但不生效"的错觉。
  108. ToleranceRatio = 0m
  109. },
  110. Parameters = new S8RuleParameterPolicy
  111. {
  112. // 数据是「天」粒度,且只在订单评审时变化。5 分钟一轮(Rule 01 的默认)
  113. // 对本规则没有任何信息增益,只是徒增数据库负载。
  114. PollIntervalSecondsDefault = 3600,
  115. TriggerCountRequiredDefault = 1,
  116. // 订单重新评审时系统建议交期会被改写,期间可能短暂缺失。
  117. // 连续两轮不命中才判恢复,避免一次重评审把预警冲掉又建回来。
  118. RecoverCountRequiredDefault = 2,
  119. // 宽限分钟是 TIMEOUT 的概念,OUT_OF_RANGE evaluator 根本不读它。
  120. // 保持 0,不给管理员一个改了不生效的旋钮。
  121. GraceMinutesDefault = 0,
  122. // 预警不是事故:客户交期还没到,还有处置窗口。
  123. // 与异常类型自身的默认严重度(FOLLOW)一致。
  124. SeverityDefault = S8SeverityCode.Follow,
  125. AllowsDepartmentDefaults = true,
  126. // S8-RULE-READINESS-1:本规则的数据集<b>没有部门列</b> ——
  127. // crm_seorderentry / crm_seorder 都不带发生部门或责任部门,
  128. // 命中行带不出 occurrence / responsible,建单时只能取这两个运行参数。
  129. // 不配就是「启用成功、调度成功、异常一条建不出来」的静默失败。
  130. RequiresDepartmentDefaultsForEnable = true,
  131. // 以下三条不是照抄 Rule 01,是按本规则异常的实际去向逐条确认的:
  132. //
  133. // · 处理池:异常带 source_rule_code,认领 / 派单走规则处理池。空池 =
  134. // 预警源源不断建出来、躺在列表里没人认领,而调度器每轮都报成功。
  135. //
  136. // · 复核池:S8VerifierEligibility 对「有来源规则」的异常,检验人候选<b>只</b>取
  137. // 该规则的复核池。空池 → 候选列表为空 → 处理人点不了「提交复核」,
  138. // 单据永远停在 IN_PROGRESS。
  139. //
  140. // · 升级池:本异常类型的 SLA 为 240 分钟 → 建单即写 sla_deadline →
  141. // 必然进入 S8TimeoutAutoEscalationService 的扫描范围。空池时该服务只会记一行
  142. // reason=escalation_pool_empty 然后跳过,超时无人知晓。
  143. RequiresHandlerPoolForEnable = true,
  144. RequiresVerification = true,
  145. SupportsTimeoutEscalation = true
  146. }
  147. }
  148. };
  149. }