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
}
}
};
}