using Admin.NET.Plugin.AiDOP.Infrastructure.S8; using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess; using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess.Providers; namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions; /// /// S8-RULE02-ORDER-DELIVERY-WARNING-1:订单交付延期预警规则定义。 /// /// 与 Rule 01 的本质区别:Rule 01(采购交期延误)判的是「到期日已过而事情没做完」—— /// 事后。本规则判的是「客户交期还没到,但系统自己算出来的建议交期已经晚于它」——事前。 /// 前者用 TIMEOUT(时间轴过了没有),后者用 OUT_OF_RANGE(两个日期之差越了界)。 /// /// 为什么是 OUT_OF_RANGE 而不是新造一个 evaluator:本规则的判据展开后就是 /// 「measured_value(延期天数)> 上限 0」,正是 OUT_OF_RANGE 的既有语义。 /// 仓内此前没有任何 OUT_OF_RANGE 规则定义(见 类注释), /// 本规则是第一条;evaluator 本身一行未改。 /// public sealed class S8OrderDeliveryRuleDefinitions : IS8RuleDefinitionSource { /// /// Rule 02 编码。命名沿用 Rule 01 的 RULE_{模块}_{业务} 格式。 /// 参与 dedup_key,全链身份:任何地方引用必须用本常量,不写字面量。 /// public const string OrderDeliveryDelayWarningCode = "RULE_S1_ORDER_DELIVERY_DELAY_WARNING"; /// /// 建单使用的异常类型。已存在于种子 /// (S8ExceptionTypeSeedData:订单交付延期预警 / scene S1 / SLA 240 分钟 / 默认 FOLLOW), /// 因此本批不新建异常类型、不写 migration。 /// public const string OrderDeliveryDelayWarningExceptionTypeCode = "ORDER_DELIVERY_DELAY_WARNING"; /// /// 源对象类型。参与 dedup_key。 /// 是「订单行」而不是「订单」:交期与数量都挂在行上,同一张订单的不同行交期可以不同。 /// 用订单号做身份会让同单多行只留下一条异常,其余行的风险被静默吞掉。 /// public const string SalesOrderLineObjectType = "SALES_ORDER_LINE"; public IEnumerable GetDefinitions() => new[] { new S8RuleDefinition { RuleCode = OrderDeliveryDelayWarningCode, DisplayName = "订单交付延期预警", Description = "订单行的系统建议交期晚于客户要求交期,且客户交期尚未到达时提报预警。" + "延期天数由系统建议交期与客户要求交期相减得到,不做任何推算;" + "尚未做过评审资源检查(没有系统建议交期)的订单行不参与判定。", DatasetCode = S8OrderDeliveryDatasetDefinitions.OrderDeliveryRiskCode, RuleType = S8OutOfRangeRuleEvaluator.RuleTypeCode, RuleMechanism = "DATE", SourceObjectType = SalesOrderLineObjectType, SceneCode = S8ModuleCode.S1, StageCode = S8ModuleCode.S1, // 风险在「评审/排产/测算」环节产生:系统建议交期正是该环节的资源检查结果。 OrderFlowCode = "ORDER_REVIEW_PLAN_CALC", ExceptionTypeCode = OrderDeliveryDelayWarningExceptionTypeCode, // ──────────────────────────────────────────────────────────────── // S8-RULE-LIFECYCLE-CREATE-GATE-1:「未到期」是立案条件,不是风险条件。 // // 三条语义各归各位,不再挤在一个「本轮是否命中」里: // 立案:ETA 晚于交期 且 交期尚未到达 ← 只有这一条要求"未到期" // 持续:ETA 晚于交期 ← 跨过交期照样成立,异常保持 active // 恢复:ETA 回到交期以内 ← 唯一合法的恢复 // // 修的是一个已实测复现的假恢复:时间跨过客户交期时,风险其实是**兑现**了, // 而旧实现(数据集里带 plan_date >= NOW())会让这一行从结果集消失, // 恢复判定看到的与"风险解除"完全一样,于是把它标成已恢复。 // // 代价是数据集会持续返回已过交期的风险行(真库当前 7 条)。它们照常命中、 // 照常保护既有异常不被误判恢复,只是拿不到首次建单资格 —— 那些风险早已兑现, // 不是预警;接手它们的应当是将来的「订单交期已延误」规则,不是本规则。 // ──────────────────────────────────────────────────────────────── CreateOnlyBeforeDueAt = true, // 面向业务管理员的只读说明。刻意不出现表名 / 列名 / DATEDIFF —— // 配置页要回答的是「这条规则按什么判」,不是「它怎么实现的」。 JudgementSummary = "系统按产能测算出的建议交期晚于客户要求的交期,且客户交期尚未到达时,提报预警;" + "晚出来的天数即预警的严重程度依据。" + "预警提出后,即使客户交期已经过去,只要建议交期仍然晚于要求交期,预警继续有效;" + "只有重新评审把建议交期拉回到要求交期以内,才算风险解除。" + "订单已关闭、或该订单行还没有系统建议交期时,不参与判定。", DedupIdentitySummary = "销售订单号 + 订单行号", // ──────────────────────────────────────────────────────────────── // 判定语义。 // // measured_value = 系统建议交期 - 客户要求交期(天)。由 Provider 用 // DATEDIFF 算好后放在 canonical 的 measured_value 列,evaluator 只做越界比较。 // // 上限 0 的含义:只要建议交期比要求交期晚哪怕一天就预警。 // 这不是拍脑袋定的阈值 —— 0 是「不延期」这件事本身的定义。 // 真正可调的宽容度留给 ToleranceAbs(见下),管理员要"晚 3 天以内不报"时 // 改的是容差,而不是重新定义什么叫延期。 // // 不设 LowerBound:建议交期早于要求交期是好事,不构成异常。 // ──────────────────────────────────────────────────────────────── OutOfRange = new S8OutOfRangeSemantics { MeasuredValueColumn = S8CanonicalColumns.MeasuredValue, // 去重身份 = bill_no + '#' + entry_seq,由 Provider 拼好放进 source_object_id。 // 真库证据:crm_seorderentry 390 行,(bill_no, entry_seq) 重复组为 0。 SourceObjectIdColumn = S8CanonicalColumns.SourceObjectId, RelatedObjectCodeColumn = S8CanonicalColumns.RelatedObjectCode, UpperBound = 0m, // ToleranceAbs = 0:本批没有任何业务依据说「晚几天以内可以不报」。 // 凭空填一个 3 会在没有拍板的情况下把一批真实风险静默过滤掉。 // 需要宽容度时由业务给出天数,再改这一处。 ToleranceAbs = 0m, // ToleranceRatio 对本规则无意义:上限为 0 时 evaluator 会跳过比例判断 // (避免除零),写非 0 值只会造成"配了但不生效"的错觉。 ToleranceRatio = 0m }, Parameters = new S8RuleParameterPolicy { // 数据是「天」粒度,且只在订单评审时变化。5 分钟一轮(Rule 01 的默认) // 对本规则没有任何信息增益,只是徒增数据库负载。 PollIntervalSecondsDefault = 3600, TriggerCountRequiredDefault = 1, // 订单重新评审时系统建议交期会被改写,期间可能短暂缺失。 // 连续两轮不命中才判恢复,避免一次重评审把预警冲掉又建回来。 RecoverCountRequiredDefault = 2, // 宽限分钟是 TIMEOUT 的概念,OUT_OF_RANGE evaluator 根本不读它。 // 保持 0,不给管理员一个改了不生效的旋钮。 GraceMinutesDefault = 0, // 预警不是事故:客户交期还没到,还有处置窗口。 // 与异常类型自身的默认严重度(FOLLOW)一致。 SeverityDefault = S8SeverityCode.Follow, AllowsDepartmentDefaults = true, // S8-RULE-READINESS-1:本规则的数据集没有部门列 —— // crm_seorderentry / crm_seorder 都不带发生部门或责任部门, // 命中行带不出 occurrence / responsible,建单时只能取这两个运行参数。 // 不配就是「启用成功、调度成功、异常一条建不出来」的静默失败。 RequiresDepartmentDefaultsForEnable = true, // 以下三条不是照抄 Rule 01,是按本规则异常的实际去向逐条确认的: // // · 处理池:异常带 source_rule_code,认领 / 派单走规则处理池。空池 = // 预警源源不断建出来、躺在列表里没人认领,而调度器每轮都报成功。 // // · 复核池:S8VerifierEligibility 对「有来源规则」的异常,检验人候选取 // 该规则的复核池。空池 → 候选列表为空 → 处理人点不了「提交复核」, // 单据永远停在 IN_PROGRESS。 // // · 升级池:本异常类型的 SLA 为 240 分钟 → 建单即写 sla_deadline → // 必然进入 S8TimeoutAutoEscalationService 的扫描范围。空池时该服务只会记一行 // reason=escalation_pool_empty 然后跳过,超时无人知晓。 RequiresHandlerPoolForEnable = true, RequiresVerification = true, SupportsTimeoutEscalation = true } } }; }