rust: Chain of Responsibility Pattern

项目结构:

rust 复制代码
//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : arithmetic.rs
//! 整数算术:全工程**唯一**的舍入与比率口径。
//!
//! ## 为什么单独一个文件
//!
//! 「算账的除法」在工程里只能有一处实现。散在各处写 `a / b` 会带来两个后果:
//! ① 整数除法向零截断,与手工口径差 1 分,而这种差 1 分在报表上**看不出来**;
//! ② 以后想统一改成「银行家舍入」要改十几个地方。
//! 因此本文件收口,其余各层一律调用 [`divide_rounded`]。
//!
//! ## 为什么用 `i128` 做中间量
//!
//! 金额用「分」存储,金重用「毫克」,比率用「万分点」。
//! 「分 × 万分点」已经是 10^6 量级,再乘一个金重(毫克,可达 10^6)
//! 会到 10^12------`i64` 还剩得下,但**再来一次乘法就悬了**。
//! 中间量一律 `i128`,出口处再受控地转回 `i64`。
//!
//! ## ★ 舍入方向:远离零
//!
//! `divide_rounded(-a, b) == -divide_rounded(a, b)` 是本层的恒等式,
//! 工程验收会真的跑它(`main.rs` 的体检表)。
//! 向零截断(Rust 的 `/` 默认行为)**不满足**这条恒等式,
//! 于是「退 1 分」与「收 1 分」的舍入方向不一致,差额会在合计里累积。

/// 把 `numerator / denominator` 按**远离零方向四舍五入**成一个 `i64`。
///
/// # 参数
/// - `numerator`:分子(可为负)。
/// - `denominator`:分母(**必须非 0**)。
///
/// # Panics
/// 分母为 0 时 panic。这是**调用方的逻辑错误**,不是可以降级的业务情况------
/// 返回 0 会让「这一项除不出来」变成「这一项是 0 元」,而报表看起来完全正常。
pub fn divide_rounded(numerator: i128, denominator: i128) -> i64 {
    assert!(
        denominator != 0,
        "除数为零:这不是可降级的业务情况,静默返回 0 会让报表看起来正常"
    );
    // 符号单独记:只在最后乘回去,中间全按正数算,避免负号干扰取余。
    let is_negative: bool = (numerator < 0) != (denominator < 0);
    // ⚠️ 用 `unsigned_abs` 而不是 `abs`:`i128::MIN` 的 `abs()` 会溢出 panic。
    let dividend: u128 = numerator.unsigned_abs();
    let divisor: u128 = denominator.unsigned_abs();
    let quotient: u128 = dividend / divisor;
    let remainder: u128 = dividend % divisor;
    // 远离零方向:余数达到除数的一半就进 1。
    let adjusted: u128 = if remainder * 2 >= divisor {
        quotient + 1
    } else {
        quotient
    };
    let magnitude: i128 = i128::try_from(adjusted).expect("除法结果超出 i128 可表示范围");
    let signed: i128 = if is_negative { -magnitude } else { magnitude };
    i64::try_from(signed).expect("除法结果超出 i64 可表示范围")
}

/// 比率转万分点:`numerator / denominator × 10000`,四舍五入。
///
/// ## 为什么比率一律用「万分点」整数
///
/// 毛利率 13.00% 与 13.005% 在浮点里都能表示,但**能不能精确比较**是另一回事:
/// 两个「看起来相等」的浮点比率在报表上可能因 1e-15 的差而分叉。
/// 万分点整数没有这个问题,且 `1300` 这种数字读者能直接读成 `13.00%`。
pub fn ratio_basis_points(numerator: i128, denominator: i128) -> i64 {
    divide_rounded(numerator * 10_000, denominator)
}

/// 按整数比例缩放:`value × numerator / denominator`,四舍五入。
///
/// 「金重(毫克) × 金价(分/克) / 1000」这类算式一律走这里,
/// 不在业务代码里裸写乘除。
pub fn scale_rounded(value: i64, numerator: i128, denominator: i128) -> i64 {
    divide_rounded(i128::from(value) * numerator, denominator)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : authority_level.rs
//! 处置权限档位:**哪一级有权批多大金额**。
//!
//! ## 为什么是开放型标签(`const fn new`)
//!
//! 判据同 [`super::ticket_type::TicketType`]:
//!
//! - 权限层级**会**不断增加(今天三级,明天加「大区总监」档很自然);
//! - 但**不**被当作查找键(链是**线性**查找的,见机制层文档);
//! - 因此 `const fn new` + 不实现 `PartialEq`/`Ord`/`Hash`。
//!
//! ## ★ 为什么「金额上限」放在档位上而不是放在处理器上
//!
//! 处理器(机制层的 `Handler`)**不应该知道**「门店权限是 ¥5,000」------
//! 那是业务口径。放在这里,机制层就只是一个「按序问、问到能处理为止」的壳,
//! 换一家公司只要换这张档位表,一个字符都不用改机制层。
//! (机制层**零 `crate::` 依赖**正是为了让这一点可被依赖图证明。)

/// 一级处置权限。
///
/// # 字段说明
/// - `code`:机读编码。
/// - `chinese_name`:中文名(如「门店店长」)。
/// - `ceiling_minor_units`:★ 金额上限(**整数分**)。
///   涉及金额 `<=` 上限即属于本档。
#[derive(Clone, Copy, Debug)]
pub struct AuthorityLevel {
    /// 机读编码。
    code: &'static str,
    /// 中文名。
    chinese_name: &'static str,
    /// 金额上限(整数分)。
    ceiling_minor_units: i64,
}

impl AuthorityLevel {
    /// 声明一级权限。
    ///
    /// # 参数
    /// - `code`:机读编码。
    /// - `chinese_name`:中文名。
    /// - `ceiling_minor_units`:金额上限(整数分,`>= 0`)。
    pub const fn new(code: &'static str, chinese_name: &'static str, ceiling_minor_units: i64) -> Self {
        Self {
            code,
            chinese_name,
            ceiling_minor_units,
        }
    }

    /// 机读编码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

    /// 中文名。
    pub const fn chinese_name(&self) -> &'static str {
        self.chinese_name
    }

    /// 金额上限(整数分)。
    pub const fn ceiling_minor_units(&self) -> i64 {
        self.ceiling_minor_units
    }

    /// ★ 本档能否批这个金额。
    ///
    /// 判据是 `amount <= ceiling`(**含等号**:恰好等于上限归本级,
    /// 这是「权限」而非「阈值」的语义,写反了边界上就会差一张单)。
    ///
    /// # 参数
    /// - `amount_minor_units`:涉及金额(整数分)。
    pub const fn covers(&self, amount_minor_units: i64) -> bool {
        amount_minor_units <= self.ceiling_minor_units
    }

    /// 上限文本(`¥5,000.00` 形式,交给 [`super::money::Money`] 格式化)。
    pub fn ceiling_text(&self) -> String {
        super::money::Money::new(self.ceiling_minor_units, super::currency_code::CNY).chinese_text()
    }
}

/// 门店店长:上限 ¥5,000.00(= 500000 分)。
pub const AUTHORITY_STORE: AuthorityLevel = AuthorityLevel::new("STORE", "门店店长", 500_000);
/// 区域中心经理:上限 ¥80,000.00(= 8000000 分)。
pub const AUTHORITY_REGIONAL: AuthorityLevel = AuthorityLevel::new("REGIONAL", "区域中心经理", 8_000_000);
/// 总部售后总监:上限 ¥500,000.00(= 50000000 分)。
pub const AUTHORITY_HEADQUARTER: AuthorityLevel =
    AuthorityLevel::new("HEADQUARTER", "总部售后总监", 50_000_000);

/// 内置权限档位清单(**按权限从小到大**,链就是按这个顺序装配的)。
///
/// ⚠️ 顺序本身就是业务知识:把「总部」放在「门店」前面,
/// 所有单都会被总部批掉------而代码**照样能编译、照样能跑**。
/// 本工程在幕九专门做了「换序对照」来证明这一点。
pub const BUILTIN_AUTHORITY_LEVELS: [AuthorityLevel; 3] = [
    AUTHORITY_STORE,
    AUTHORITY_REGIONAL,
    AUTHORITY_HEADQUARTER,
];

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : category_code.rs
//! 品类编码:开放型标签,并自带一个**影响算法分支**的属性。
//!
//! ## ★ `needs_metal_pricing` 为什么放在编码上
//!
//! 黄金、钻石(镶嵌托)的**成本要按成色 × 金价**算;
//! 翡翠、珍珠按件计价,没有金属成本这一项。
//! 这个差异不是「某个报表的选择」,而是**这个品类本身的性质**,
//! 所以它属于品类编码,而不是散在工厂函数里写 `if category == ...`。
//! 散着写的结果一定是:新增一个品类时,有地方忘了补这个判断,
//! 而漏掉的那处会算出「金属成本 = 0」,报表看起来完全正常。

/// 品类编码。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct CategoryCode {
    /// 机器编码,如 `"GOLD"`。
    code: &'static str,
    /// 中文名称,如 `"黄金饰品"`。
    chinese_name: &'static str,
    /// ★ 该品类的成本是否要按「成色 × 金价」计算。
    needs_metal_pricing: bool,
}

impl CategoryCode {
    /// 声明一个品类。
    pub const fn new(code: &'static str, chinese_name: &'static str, needs_metal_pricing: bool) -> Self {
        Self {
            code,
            chinese_name,
            needs_metal_pricing,
        }
    }

    /// 机器编码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

    /// 中文名称。
    pub const fn chinese_name(&self) -> &'static str {
        self.chinese_name
    }

    /// 是否按「成色 × 金价」计算金属成本。
    pub const fn needs_metal_pricing(&self) -> bool {
        self.needs_metal_pricing
    }
}

/// 黄金饰品:按成色 × 金价计价。
pub const CATEGORY_GOLD: CategoryCode = CategoryCode::new("GOLD", "黄金饰品", true);
/// 钻石饰品:镶嵌托是 18K 金,同样按成色 × 金价计价。
pub const CATEGORY_DIAMOND: CategoryCode = CategoryCode::new("DIAMOND", "钻石饰品", true);
/// 翡翠饰品:按件计价,无金属成本。
pub const CATEGORY_JADE: CategoryCode = CategoryCode::new("JADE", "翡翠饰品", false);

/// 内置品类全集。
pub const BUILTIN_CATEGORY_CODES: [CategoryCode; 3] = [CATEGORY_GOLD, CATEGORY_DIAMOND, CATEGORY_JADE];

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : currency_code.rs
//! 币种:开放型标签(`const fn new` 结构体,不是 `enum`)。
//!
//! ## ★ 判据:会不会被当作查找键 + 会不会不断增加
//!
//! - **会不断增加**(接新地区就多一个币种)→ 开放型结构体,
//!   工程外新增只需在装配点声明一个 `const`,**不用回来改本文件**;
//! - **会被当作查找键** → 实现 `PartialEq`(汇率表按币种查);
//! - 但**不实现 `Ord` / `Hash`**:本工程从不排序币种、也不拿币种做哈希键,
//!   不实现用不上的 trait 本身就是一种防错(多实现一个,就多一处「看起来能用」的用法)。
//!
//! ## ⚠️ 为什么字段是 `&'static str`
//!
//! `String` 会让 `CurrencyCode` 不能 `Copy`,于是 `Money` 也不能 `Copy`,
//! 而金额在本工程里到处按值传递。币种编码是编译期常量,
//! 用 `&'static str` 换 `Copy` 是划算的。

/// 币种:`code` 是机器编码,`chinese_name` 给人读,`symbol` 用于排版。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct CurrencyCode {
    /// 机器编码,如 `"CNY"`。
    code: &'static str,
    /// 中文名称,如 `"人民币"`。
    chinese_name: &'static str,
    /// 金额符号,如 `"¥"`。
    symbol: &'static str,
}

impl CurrencyCode {
    /// 声明一个币种。`const fn` 使得它可以写进 `const` 常量里。
    pub const fn new(code: &'static str, chinese_name: &'static str, symbol: &'static str) -> Self {
        Self {
            code,
            chinese_name,
            symbol,
        }
    }

    /// 机器编码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

    /// 中文名称。
    pub const fn chinese_name(&self) -> &'static str {
        self.chinese_name
    }

    /// 金额符号。
    pub const fn symbol(&self) -> &'static str {
        self.symbol
    }
}

/// 人民币:集团合并口径的记账本位币。
pub const CNY: CurrencyCode = CurrencyCode::new("CNY", "人民币", "¥");
/// 港币:中国香港门店的本位币。
pub const HKD: CurrencyCode = CurrencyCode::new("HKD", "港币", "HK$");
/// 美元:跨境采购结算币。
pub const USD: CurrencyCode = CurrencyCode::new("USD", "美元", "US$");

/// 集团允许出现的币种全集。
///
/// ⚠️ 这张表只用于**校验**(流水的币种必须在这里面),
/// 不用于「枚举有哪些币种」------那会把新增币种变成改领域层文件。
pub const BUILTIN_CURRENCY_CODES: [CurrencyCode; 3] = [CNY, HKD, USD];


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : disposition.rs
//! 处置结论:**一张工单最后是怎么了结的**。
//!
//! ## 为什么 `Disposition` 是封闭 `enum`
//!
//! 判据与 [`super::ticket_type::TicketType`] 恰好相反:
//!
//! - 处置结论**不会**不断增加(就这七种,加一种意味着流程本身变了);
//! - 报表要对它做**穷尽**处理(每一种都要有一列说明、一条审计规则);
//! - 因此要**编译期的穷尽性检查**:漏掉一种,`match` 就编译不过去。
//!
//! 这正是「开放型 vs 封闭」的判据:**会不会被当作查找键 + 会不会不断增加**。

/// ★ 处置结论的类型(**封闭集合**)。
///
/// ⚠️ 顺序不是随便排的:它同时是**严重度**的顺序(越靠后越严重),
/// 报表里按这个顺序输出,读者就能一眼看出「最严重的在最后」。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Disposition {
    /// 门店直接处置(金额在门店权限内)。
    StoreHandled,
    /// 区域中心处置(金额在区域权限内)。
    RegionalHandled,
    /// 总部处置(金额在总部权限内)。
    HeadquarterHandled,
    /// 保修期内免费维修(**不看金额**)。
    WarrantyRepair,
    /// 超出所有权限 → 上报总裁办。
    Escalated,
    /// 拒单(无凭证 / 超窗口 / 人为损坏不在保修内)。
    Rejected,
    /// ★ 链上**没有任何一个处理器**接下它。
    ///
    /// 这一档必须存在,理由见 [`super::authority_level`] 之外更基本的道理:
    /// 「没人处理」若用 `Option::None` 表示,调用方就会写出
    /// `if let Some(r) = result { ... }` 然后**什么都不做**------
    /// 那时一张工单消失了,而账上没有任何一个数字会说「少了一张」。
    Unhandled,
}

impl Disposition {
    /// 中文名。
    ///
    /// ⚠️ 全工程「处置结论叫什么」只有这一处产生:
    /// 若每张表各写一次,一处写「门店处置」、一处写「门店处理」,
    /// 读者只会以为那是两件事。
    pub const fn chinese_text(&self) -> &'static str {
        match self {
            Disposition::StoreHandled => "门店直接处置",
            Disposition::RegionalHandled => "区域中心处置",
            Disposition::HeadquarterHandled => "总部处置",
            Disposition::WarrantyRepair => "保修免费维修",
            Disposition::Escalated => "上报总裁办",
            Disposition::Rejected => "拒单",
            Disposition::Unhandled => "无人处置",
        }
    }

    /// 机读编码(写进指纹)。
    pub const fn machine_text(&self) -> &'static str {
        match self {
            Disposition::StoreHandled => "STORE",
            Disposition::RegionalHandled => "REGIONAL",
            Disposition::HeadquarterHandled => "HEADQUARTER",
            Disposition::WarrantyRepair => "WARRANTY",
            Disposition::Escalated => "ESCALATED",
            Disposition::Rejected => "REJECTED",
            Disposition::Unhandled => "UNHANDLED",
        }
    }

    /// ★ 是否算「有人接下了」(处置掉了,哪怕结论是拒单)。
    ///
    /// ⚠️ 拒单**也算有人处理**:拒单是一个明确的业务结论,
    /// 「没人处理」才是系统缺陷。把两者混为一谈,
    /// 「链尾没人接」这个真问题就会被「我们拒了很多单」掩盖掉。
    pub const fn is_settled(&self) -> bool {
        !matches!(self, Disposition::Unhandled)
    }

    /// 是否属于「赔钱了」的处置(用于统计赔付总额)。
    ///
    /// 拒单与无人处置不产生赔付;保修维修由厂商承担,赔付为 0
    /// (但仍要走统一路径,理由见 [`super::money::Money::zero`])。
    pub const fn bears_compensation(&self) -> bool {
        matches!(
            self,
            Disposition::StoreHandled
                | Disposition::RegionalHandled
                | Disposition::HeadquarterHandled
                | Disposition::Escalated
        )
    }

    /// 是否走到了「上报」这一级(本工程用来做体检的**存在性**检查)。
    pub const fn is_escalated(&self) -> bool {
        matches!(self, Disposition::Escalated)
    }

    /// 是否被拒。
    pub const fn is_rejected(&self) -> bool {
        matches!(self, Disposition::Rejected)
    }

    /// 全部取值(**穷尽**,用于报表与自检)。
    pub const fn all() -> [Disposition; 7] {
        [
            Disposition::StoreHandled,
            Disposition::RegionalHandled,
            Disposition::HeadquarterHandled,
            Disposition::WarrantyRepair,
            Disposition::Escalated,
            Disposition::Rejected,
            Disposition::Unhandled,
        ]
    }
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : money.rs
//! 金额:整数最小单位(分)+ 自带币种。
//!
//! ## ★ 自带币种是类型安全,不是装饰
//!
//! 跨币种相加(人民币 + 港币)在类型上是**两个数相加**,编译器看不出错,
//! 报表也看不出错------只有做集团合并的人会在月底发现差了几万。
//! 因此 [`Money::plus`] 里放一条 `debug_assert!`:
//! 币种不同就在调试构建里立刻 panic,而不是返回一个「看起来对」的数。
//!
//! ## ★ 规范文本形式留在领域层
//!
//! `chinese_text()`(`¥12,345.67`)之所以不放表示层,
//! 判据是「这个文本形式是不是所有层都要用」:
//! 机制层报错、分析层结论、表示层表格都要把金额嵌进句子里。
//! 若搬去 `app`,`pool` / `analysis` 就得反向依赖 `app`------那才是真违规。
//!
//! ## ⚠️ 全程不出现浮点
//!
//! 千分位用整数反复除 10 取位拼出来。借道 `f64` 会在 10^15 量级上
//! 出现「同一笔金额两种写法」。

use crate::domain::arithmetic::scale_rounded;
use crate::domain::CurrencyCode;

/// 千分位分隔符。
const THOUSANDS_SEPARATOR: char = ',';

/// 一笔金额:**分**为单位的整数 + 币种。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Money {
    /// 以「分」为单位的整数金额。
    minor_units: i64,
    /// 币种。
    currency: CurrencyCode,
}

impl Money {
    /// 由「分」整数与币种构造。
    pub const fn new(minor_units: i64, currency: CurrencyCode) -> Self {
        Self {
            minor_units,
            currency,
        }
    }

    /// 零额。
    ///
    /// ⚠️ 「零」也必须走统一构造路径:散着写 `Money::new(0, c)` 时,
    /// 币种迟早会被写错一次,而 0 元在报表上**看不出币种错了**。
    pub const fn zero(currency: CurrencyCode) -> Self {
        Self::new(0, currency)
    }

    /// 以「分」为单位的整数。
    pub const fn minor_units(&self) -> i64 {
        self.minor_units
    }

    /// 币种。
    pub const fn currency(&self) -> CurrencyCode {
        self.currency
    }

    /// 是否为零额。
    pub const fn is_zero(&self) -> bool {
        self.minor_units == 0
    }

    /// 同币种相加。
    ///
    /// # Panics
    /// 币种不同时 `debug_assert!` 失败。
    /// 这是**接线错误**(把两本账加在了一起),不是业务情况。
    pub fn plus(&self, other: Money) -> Money {
        debug_assert!(
            self.currency == other.currency,
            "跨币种相加:{} + {} ------ 这两本账不能相加,先折算再求和",
            self.currency.code(),
            other.currency.code()
        );
        Money::new(self.minor_units + other.minor_units, self.currency)
    }

    /// 同币种相减(`self - other`)。
    pub fn minus(&self, other: Money) -> Money {
        debug_assert!(
            self.currency == other.currency,
            "跨币种相减:{} - {}",
            self.currency.code(),
            other.currency.code()
        );
        Money::new(self.minor_units - other.minor_units, self.currency)
    }

    /// 取相反数(用于把「成本」折成「对毛利的贡献」)。
    pub fn negate(&self) -> Money {
        Money::new(-self.minor_units, self.currency)
    }

    /// 按万分点缩放:`self × basis_points / 10000`。
    ///
    /// 用于「按占比分摊」------分摊口径也走唯一舍入口径,
    /// 不会出现「分摊后各份之和 ≠ 总额」之外的第二种差法。
    pub fn scale_basis_points(&self, basis_points: i64) -> Money {
        Money::new(
            scale_rounded(self.minor_units, i128::from(basis_points), 10_000),
            self.currency,
        )
    }

    /// 按整数比例缩放:`self × numerator / denominator`。
    pub fn scale_ratio(&self, numerator: i128, denominator: i128) -> Money {
        Money::new(
            scale_rounded(self.minor_units, numerator, denominator),
            self.currency,
        )
    }

    /// 纯数字文本(`12,345.67`),不含币种符号。
    ///
    /// 用于「金额 ÷ 克数」这类已经把币种写进列头的场合。
    pub fn amount_text(&self) -> String {
        // 符号单独处理:负号要放在最前面,不能夹在千分位里。
        // ⚠️ 先升到 i128 再取绝对值:`i64::MIN` 的 `abs()` 会溢出。
        let is_negative: bool = self.minor_units < 0;
        let magnitude: i128 = i128::from(self.minor_units).unsigned_abs() as i128;
        let yuan: i128 = magnitude / 100;
        let cents: i128 = magnitude % 100;
        let sign: &str = if is_negative { "-" } else { "" };
        format!("{}{}.{:02}", sign, group_thousands(yuan), cents)
    }

    /// 规范文本(`¥12,345.67`):符号 + 千分位。
    pub fn chinese_text(&self) -> String {
        format!("{}{}", self.currency.symbol(), self.amount_text())
    }

    /// 两个金额是否相等(含币种)。
    ///
    /// 用于审计比对:金额相等但币种不同,在本工程里是**不相等**。
    pub fn is_same_as(&self, other: &Money) -> bool {
        self.minor_units == other.minor_units && self.currency == other.currency
    }
}

/// 把一个非负整数的十进制表示插上千分位。
///
/// ⚠️ 用整数反复除 10 取位,全程不出现浮点:
/// `f64` 在 10^15 以上会开始丢低位,而金额合计确实会到那个量级。
fn group_thousands(value: i128) -> String {
    let digits: Vec<char> = value.to_string().chars().collect();
    let mut buffer: Vec<char> = Vec::with_capacity(digits.len() + digits.len() / 3);
    for (index, character) in digits.iter().enumerate() {
        let from_right: usize = digits.len() - index;
        if index > 0 && from_right % 3 == 0 {
            buffer.push(THOUSANDS_SEPARATOR);
        }
        buffer.push(*character);
    }
    buffer.into_iter().collect()
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : month_key.rs
//! 会计月份:`年 + 月` 的值对象。
//!
//! ## 为什么不用 `CalendarDate`
//!
//! 月度经营分析的粒度是**月**,不是日。用「某月 1 号」代表一个月会带来
//! 「代表日是哪天」这种本不该存在的问题(月末调整单会挂在代表日上)。
//! 因此单独一个 `MonthKey`。
//!
//! ## ★ `month_index` 是排序与比较的唯一口径
//!
//! 跨年比较不能写 `year < year || (year == year && month < month)`------
//! 这种写法迟早会被复制漏改一处。折成 `year * 12 + (month - 1)` 一个整数后,
//! 「先后」「相差几个月」都是一次减法。

/// 会计月份。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct MonthKey {
    /// 年份(如 `2026`)。
    year: i32,
    /// 月份(`1..=12`)。
    month: u32,
}

impl MonthKey {
    /// 声明一个会计月份。
    pub const fn new(year: i32, month: u32) -> Self {
        Self { year, month }
    }

    /// 年份。
    pub const fn year(&self) -> i32 {
        self.year
    }

    /// 月份。
    pub const fn month(&self) -> u32 {
        self.month
    }

    /// 月份是否合法(`1..=12`)。
    pub const fn is_valid(&self) -> bool {
        self.month >= 1 && self.month <= 12
    }

    /// 是否与另一个月份相同。
    ///
    /// ⚠️ 单独一个 `const fn` 而不是用 `==`:
    /// `PartialEq` 的 `==` **不是 const fn**,在 `const fn` 里用不了。
    pub const fn is_same(&self, other: &MonthKey) -> bool {
        self.year == other.year && self.month == other.month
    }

    /// ★ 折成一个整数的月份序号:`year * 12 + (month - 1)`。
    ///
    /// 「先后」「相差几个月」全部由它算,不写复合比较。
    pub const fn month_index(&self) -> i32 {
        self.year * 12 + (self.month as i32) - 1
    }

    /// 上一个会计月份。
    pub fn previous(&self) -> MonthKey {
        let index: i32 = self.month_index() - 1;
        MonthKey::new(index.div_euclid(12), index.rem_euclid(12) as u32 + 1)
    }

    /// 下一个会计月份。
    pub fn next(&self) -> MonthKey {
        let index: i32 = self.month_index() + 1;
        MonthKey::new(index.div_euclid(12), index.rem_euclid(12) as u32 + 1)
    }

    /// 去年同月(同比的分母所在月份)。
    pub const fn same_month_last_year(&self) -> MonthKey {
        MonthKey::new(self.year - 1, self.month)
    }

    /// ★ 往后推 `count` 个月(校准到期月顺延用)。
    ///
    /// ⚠️ 为什么不用 `year*12+month` 手算再拆:
    /// 那种算式在跨年处容易漏 `-1`,而漏了之后**只在 12 月出错**------
    /// 一年只错一次的错误最难被发现。复用 `month_index` 是唯一的写法。
    ///
    /// # 参数
    /// - `count`:要往后推几个月(`0` 表示就是当月)。
    pub fn plus_months(&self, count: u32) -> MonthKey {
        let index: i32 = self.month_index() + count as i32;
        MonthKey::new(index.div_euclid(12), index.rem_euclid(12) as u32 + 1)
    }

    /// 与另一个月份相差几个月(`self - other`)。
    pub const fn months_since(&self, other: &MonthKey) -> i32 {
        self.month_index() - other.month_index()
    }

    /// 是否早于另一个月份。
    pub const fn is_before(&self, other: &MonthKey) -> bool {
        self.month_index() < other.month_index()
    }

    /// 机器文本(`2026-10`)。定宽,可直接进表格列。
    pub fn machine_text(&self) -> String {
        format!("{:04}-{:02}", self.year, self.month)
    }

    /// 中文文本(`2026年10月`)。
    pub fn chinese_text(&self) -> String {
        format!("{}年{}月", self.year, self.month)
    }

    /// 该月的自然天数(用于把「月」折算成「日均」这类派生口径)。
    ///
    /// ⚠️ 闰年判据走 `support` 层,不在本文件重写一遍------
    /// 重写一次就多一处「两处算法不一致」的风险。
    pub fn days(&self) -> u32 {
        crate::support::days_in_month(self.year, self.month)
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : service_ticket.rs
//! 售后工单:一张单进来时**带着什么**。
//!
//! ## ★ 为什么判据(能不能退、在不在保)写在工单上,而不是写在处理器里
//!
//! 处理器的职责是「**我能不能接这一单**」,
//! 而「这一单能不能退」是**这一单自己的属性**。
//! 若把判据写进处理器,那么同一个问题(比如「有没有凭证」)
//! 会在单据校验、保修判定、权限审批三处各写一遍------
//! 三处写错成两种口径时,报表上不会有任何异常,只会少几张单。
//!
//! ## 为什么工单是**不可变**的
//!
//! 工单一旦受理,它的属性就不该变(金额、购买月、有没有凭证都是历史事实)。
//! 处置结论是**另一件东西**(见 [`super::ticket_resolution::TicketResolution`]),
//! 不写回工单。这样「同一批工单跑两条不同的链」才真的可比------
//! 本工程幕九的换序对照正是靠这一点成立的。

use super::category_code::CategoryCode;
use super::month_key::MonthKey;
use super::money::Money;
use super::store_code::StoreCode;
use super::ticket_type::TicketType;

/// ★ 保修期(月)。
///
/// 为什么是 24:珠宝的镶嵌牢固度与电镀层保修通常给两年。
///
/// ⚠️ 这个常量必须被「真的走到」:本工程的演示数据里
/// 既要有**在保**的维修单,也要有**过保**的维修单,
/// 否则「保修判定」这个处理器在报表上永远只走一个分支------
/// 那就没法证明它真的在判定。
pub const WARRANTY_MONTHS: i32 = 24;

/// 一张售后工单。
///
/// # 字段说明
/// - `ticket_number`:工单号(**唯一**,写进指纹)。
/// - `store`:受理门店。
/// - `category`:涉及品类。
/// - `ticket_type`:工单类型(决定走哪条判定)。
/// - `involved_amount`:涉及金额(**整数分**,决定走哪级权限)。
/// - `purchase_month`:购买月份。
/// - `accept_month`:受理月份(本工程的「当前月」)。
/// - `has_receipt`:是否附购买凭证。
/// - `is_man_made_damage`:是否人为损坏(**人为损坏不在保修内**)。
#[derive(Clone, Debug)]
pub struct ServiceTicket {
    /// 工单号。
    ticket_number: String,
    /// 受理门店。
    store: StoreCode,
    /// 涉及品类。
    category: CategoryCode,
    /// 工单类型。
    ticket_type: TicketType,
    /// 涉及金额(整数分)。
    involved_amount: Money,
    /// 购买月份。
    purchase_month: MonthKey,
    /// 受理月份。
    accept_month: MonthKey,
    /// 是否附购买凭证。
    has_receipt: bool,
    /// 是否人为损坏。
    is_man_made_damage: bool,
}

impl ServiceTicket {
    /// 登记一张工单。
    ///
    /// # 参数
    /// - `ticket_number`:工单号(**必须唯一**,重号会让「逐单对照」失去意义)。
    /// - `store` / `category` / `ticket_type`:维度。
    /// - `involved_amount`:涉及金额。
    /// - `purchase_month` / `accept_month`:购买月与受理月。
    /// - `has_receipt` / `is_man_made_damage`:两个影响判定的事实。
    pub fn new(
        ticket_number: &str,
        store: StoreCode,
        category: CategoryCode,
        ticket_type: TicketType,
        involved_amount: Money,
        purchase_month: MonthKey,
        accept_month: MonthKey,
        has_receipt: bool,
        is_man_made_damage: bool,
    ) -> Self {
        Self {
            ticket_number: ticket_number.to_string(),
            store,
            category,
            ticket_type,
            involved_amount,
            purchase_month,
            accept_month,
            has_receipt,
            is_man_made_damage,
        }
    }

    /// 工单号。
    pub fn ticket_number(&self) -> &str {
        self.ticket_number.as_str()
    }

    /// 受理门店。
    pub const fn store(&self) -> StoreCode {
        self.store
    }

    /// 涉及品类。
    pub const fn category(&self) -> CategoryCode {
        self.category
    }

    /// 工单类型。
    pub const fn ticket_type(&self) -> TicketType {
        self.ticket_type
    }

    /// 涉及金额。
    pub const fn involved_amount(&self) -> Money {
        self.involved_amount
    }

    /// 购买月份。
    pub const fn purchase_month(&self) -> MonthKey {
        self.purchase_month
    }

    /// 受理月份。
    pub const fn accept_month(&self) -> MonthKey {
        self.accept_month
    }

    /// 是否附购买凭证。
    pub const fn has_receipt(&self) -> bool {
        self.has_receipt
    }

    /// 是否人为损坏。
    pub const fn is_man_made_damage(&self) -> bool {
        self.is_man_made_damage
    }

    /// ★ 自购买月到受理月过了几个月(**含 0**:当月购买当月受理 = 0)。
    pub const fn months_elapsed(&self) -> i32 {
        self.accept_month.months_since(&self.purchase_month)
    }

    /// ★ 是否还在退换窗口内。
    ///
    /// 三个条件**同时**成立才算:
    /// ① 这类工单**适用**退换窗口(维修/投诉不适用);
    /// ② 有购买凭证;
    /// ③ 月数 `<=` 窗口。
    ///
    /// ⚠️ 条件①不能省:投诉**没有凭证**也要受理,
    /// 若只看 ②,所有无凭证投诉都会被误拒------
    /// 而「投诉被拒」在报表上长得跟「退货无凭证被拒」一模一样。
    pub const fn is_within_return_window(&self) -> bool {
        self.ticket_type.has_return_window()
            && self.has_receipt
            && self.months_elapsed() <= self.ticket_type.return_window_months() as i32
    }

    /// ★ 是否还在保修期内。
    ///
    /// 三个条件:① 这类工单走保修判定;② **非**人为损坏;③ 月数 `<=` 保修月数。
    pub const fn is_within_warranty(&self) -> bool {
        self.ticket_type.needs_warranty()
            && !self.is_man_made_damage
            && self.months_elapsed() <= WARRANTY_MONTHS
    }

    /// ★ 是否**连受理资格都没有**(无凭证 / 超窗口)。
    ///
    /// 这是「单据校验」处理器的判据,放在这里是为了让
    /// **同一条判据**在工单清单表与处置结果表里给出同一个答案。
    pub const fn is_inadmissible(&self) -> bool {
        (self.ticket_type.needs_receipt() && !self.has_receipt)
            || (self.ticket_type.has_return_window() && !self.is_within_return_window())
    }

    /// ★ 拒单原因(**只在 [`ServiceTicket::is_inadmissible`] 为真时才有意义**)。
    ///
    /// ⚠️ 为什么不用 `Option<String>`:
    /// 报表要的是「一张表里每格都有字」,`None` 会逼着表示层再写一个「---」,
    /// 那时「---」有两处来源,改一处漏一处就分裂了。
    pub fn inadmissible_reason_text(&self) -> String {
        if self.ticket_type.needs_receipt() && !self.has_receipt {
            return format!("{} 需凭购买凭证受理(本单无凭证)", self.ticket_type.chinese_name());
        }
        if self.ticket_type.has_return_window() && !self.is_within_return_window() {
            return format!(
                "{} 窗口 {} 个月,本单已过 {} 个月(凭证:{})",
                self.ticket_type.chinese_name(),
                self.ticket_type.return_window_months(),
                self.months_elapsed(),
                if self.has_receipt { "有" } else { "无" }
            );
        }
        "---".to_string()
    }

    /// ★ 指纹:一张工单的**身份**。
    ///
    /// 格式:`工单号|门店|品类|类型|金额分|购买月|受理月|凭证|人为`
    ///
    /// 用途:幕九的「两条链逐单对照」比对的是指纹而不是金额,
    /// 这样「处置者变了」与「金额变了」是两件可分开看的事。
    pub fn fingerprint_text(&self) -> String {
        format!(
            "{}|{}|{}|{}|{}|{}|{}|{}|{}",
            self.ticket_number,
            self.store.code(),
            self.category.code(),
            self.ticket_type.code(),
            self.involved_amount.minor_units(),
            self.purchase_month.machine_text(),
            self.accept_month.machine_text(),
            if self.has_receipt { "R" } else { "N" },
            if self.is_man_made_damage { "M" } else { "W" }
        )
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : store_code.rs
//! 门店编码:开放型标签。
//!
//! ## 为什么是 `const fn new` 结构体而不是 `enum`
//!
//! 判据只有一条:**这个维度会不会不断增加**。
//! 门店会(开新店就多一个),所以必须开放------
//! 否则工程外新增一家店要回来改领域层文件,
//! 而「可扩展性必须被实证」这条硬纪律要求:**扩展只能发生在装配点**。
//!
//! ## ⚠️ 为什么不实现 `Ord`
//!
//! 本工程从不对门店排序,只按**声明顺序**输出(声明顺序稳定 → 两次运行逐字节一致)。
//! 实现一个用不上的 `Ord`,等于给「按门店排序」这种没被设计过的用法开了口子。

/// 门店编码。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct StoreCode {
    /// 机器编码,如 `"STORE-CN-01"`。
    code: &'static str,
    /// 中文名称,如 `"广州天河店"`。
    chinese_name: &'static str,
}

impl StoreCode {
    /// 声明一家门店。
    pub const fn new(code: &'static str, chinese_name: &'static str) -> Self {
        Self { code, chinese_name }
    }

    /// 机器编码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

    /// 中文名称。
    pub const fn chinese_name(&self) -> &'static str {
        self.chinese_name
    }
}

/// 广州天河店(内置)。
pub const STORE_CN_01: StoreCode = StoreCode::new("STORE-CN-01", "广州天河店");
/// 香港铜锣湾店(内置)。
pub const STORE_HK_01: StoreCode = StoreCode::new("STORE-HK-01", "香港铜锣湾店");

/// 内置门店全集。
///
/// ⚠️ 这张表只用于**装配**(建图时遍历),不用于「校验某店是否合法」------
/// 那会把工程外新增门店挡在门外。
pub const BUILTIN_STORE_CODES: [StoreCode; 2] = [STORE_CN_01, STORE_HK_01];

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : ticket_resolution.rs
//! 处置结论:一张工单被了结之后**留下了什么**。
//!
//! ## ★ 为什么结论里必须带上「谁处置的」
//!
//! 责任链最容易被误用成「反正有人处理了」。
//! 若结论里没有处理者名字,那么「门店处置」与「总部处置」
//! 在报表上都只是一行「已处置」------
//! 而**恰恰是「哪一级处置的」**才是这个模式要说的事
//! (把 80% 的单拦在门店,总部才能只处理真正的大单)。
//!
//! 因此 `handler_name` 是**必填**,不是可选。

use super::disposition::Disposition;
use super::money::Money;

/// 一张工单的处置结论。
///
/// # 字段说明
/// - `disposition`:结论类型。
/// - `handler_name`:★ 处置者(处理器名;无人处置时为「---」)。
/// - `compensation`:★ 公司承担的赔付金额(整数分;拒单/保修为 0)。
/// - `note`:一句话说明(**必须能解释为什么是这个结论**)。
#[derive(Clone, Debug)]
pub struct TicketResolution {
    /// 结论类型。
    disposition: Disposition,
    /// 处置者名。
    handler_name: String,
    /// 赔付金额。
    compensation: Money,
    /// 说明。
    note: String,
}

impl TicketResolution {
    /// 产生一条结论。
    ///
    /// # 参数
    /// - `disposition`:结论类型。
    /// - `handler_name`:处置者名(**不允许空串**,空串会让报表出现空单元格)。
    /// - `compensation`:赔付金额。
    /// - `note`:说明。
    pub fn new(
        disposition: Disposition,
        handler_name: &str,
        compensation: Money,
        note: &str,
    ) -> Self {
        assert!(
            !handler_name.is_empty(),
            "处置结论必须写明处置者:否则「谁处理了这张单」就不可知"
        );
        Self {
            disposition,
            handler_name: handler_name.to_string(),
            compensation,
            note: note.to_string(),
        }
    }

    /// 结论类型。
    pub const fn disposition(&self) -> Disposition {
        self.disposition
    }

    /// 处置者名。
    pub fn handler_name(&self) -> &str {
        self.handler_name.as_str()
    }

    /// 赔付金额。
    pub const fn compensation(&self) -> Money {
        self.compensation
    }

    /// 说明。
    pub fn note(&self) -> &str {
        self.note.as_str()
    }

    /// 结论中文名(转发 [`Disposition::chinese_text`])。
    pub const fn disposition_text(&self) -> &'static str {
        self.disposition.chinese_text()
    }

    /// ★ 指纹:只比**可核对**的三件事。
    ///
    /// ⚠️ `note` **不进指纹**:说明是给人看的,措辞改了不该让恒等检查失败------
    /// 否则「改进了一句文案」会被报表报成「两条路径结果不一致」。
    pub fn fingerprint_text(&self) -> String {
        format!(
            "{}|{}|{}",
            self.disposition.machine_text(),
            self.handler_name,
            self.compensation.minor_units()
        )
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : ticket_type.rs
//! 工单类型:一张售后工单「是什么事」。
//!
//! ## 为什么是开放型标签(`const fn new`)而不是 `enum`
//!
//! 判据是「会不会被当作查找键 + 会不会不断增加」:
//!
//! - 工单类型**会**不断增加(今天五种,明天加「以旧换新争议」是很自然的事);
//! - 但**不会**被当作查找键(没有 `HashMap<TicketType, _>`,只有线性查找);
//! - 因此按既有工程的判据:`const fn new` 结构体 + 自带业务参数,
//!   **不实现 `PartialEq` / `Ord` / `Hash`**(不实现的本身就是防错:
//!   用不上的 trait 一旦实现,将来就有人说「反正有 `Ord`,排个序吧」)。
//!
//! ⚠️ 注意:本工程**确实**需要「两张工单的类型是不是同一种」这个判断,
//! 但那是**编码相等**,用 `code() == other.code()` 表达(与 `MonthKey::is_same` 同理------
//! `PartialEq` 的 `==` 不是 `const fn`,而 `const fn` 上下文里也要比)。

/// 一张售后工单的类型。
///
/// # 字段说明
/// - `code`:机读编码(**稳定**、不随中文名变化,写进指纹)。
/// - `chinese_name`:中文名(给人看)。
/// - `return_window_months`:★ 退换窗口(自购买月起算,含购买当月)。
///   `0` 表示**不适用**(维修、投诉不看退换窗口)。
/// - `needs_receipt`:★ 是否**必须**有购买凭证才能受理。
/// - `needs_warranty`:★ 是否走**保修**判定(不看金额,只看保修期与是否人为损坏)。
#[derive(Clone, Copy, Debug)]
pub struct TicketType {
    /// 机读编码。
    code: &'static str,
    /// 中文名。
    chinese_name: &'static str,
    /// 退换窗口(月)。`0` = 不适用。
    return_window_months: u32,
    /// 是否必须有购买凭证。
    needs_receipt: bool,
    /// 是否走保修判定。
    needs_warranty: bool,
}

impl TicketType {
    /// 声明一种工单类型。
    ///
    /// # 参数
    /// - `code`:机读编码(**必须是 ASCII**,否则指纹宽度会算错)。
    /// - `chinese_name`:中文名。
    /// - `return_window_months`:退换窗口(月),`0` 表示不适用。
    /// - `needs_receipt`:是否必须有凭证。
    /// - `needs_warranty`:是否走保修判定。
    pub const fn new(
        code: &'static str,
        chinese_name: &'static str,
        return_window_months: u32,
        needs_receipt: bool,
        needs_warranty: bool,
    ) -> Self {
        Self {
            code,
            chinese_name,
            return_window_months,
            needs_receipt,
            needs_warranty,
        }
    }

    /// 机读编码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

    /// 中文名。
    pub const fn chinese_name(&self) -> &'static str {
        self.chinese_name
    }

    /// 退换窗口(月)。`0` 表示不适用。
    pub const fn return_window_months(&self) -> u32 {
        self.return_window_months
    }

    /// 是否必须有购买凭证。
    pub const fn needs_receipt(&self) -> bool {
        self.needs_receipt
    }

    /// 是否走保修判定。
    pub const fn needs_warranty(&self) -> bool {
        self.needs_warranty
    }

    /// ★ 是否**适用**退换窗口(窗口 > 0 才适用)。
    ///
    /// ⚠️ 为什么不直接用 `return_window_months() > 0`:
    /// 「不适用」是一个**业务判断**,写进字段语义里,
    /// 调用方就不必各自去记「0 是什么意思」。
    pub const fn has_return_window(&self) -> bool {
        self.return_window_months > 0
    }

    /// 编码是否相同(**刻意不实现 `PartialEq`**,理由见模块文档)。
    pub const fn is_same(&self, other: &TicketType) -> bool {
        // `const fn` 里不能调用 `str::eq`(不是 const fn),
        // 因此逐字节比------编码都是 ASCII 短串,成本可忽略。
        let left: &[u8] = self.code.as_bytes();
        let right: &[u8] = other.code.as_bytes();
        if left.len() != right.len() {
            return false;
        }
        let mut index: usize = 0;
        while index < left.len() {
            if left[index] != right[index] {
                return false;
            }
            index += 1;
        }
        true
    }
}

/// 退货:必须有凭证,退换窗口 1 个月。
pub const TICKET_RETURN: TicketType = TicketType::new("RETURN", "退货退款", 1, true, false);
/// 换货:必须有凭证,退换窗口 3 个月。
pub const TICKET_EXCHANGE: TicketType = TicketType::new("EXCHANGE", "换货", 3, true, false);
/// 维修:**不需要**凭证,走保修判定(不看退换窗口)。
pub const TICKET_REPAIR: TicketType = TicketType::new("REPAIR", "维修", 0, false, true);
/// 成色复检:不需要凭证,走保修判定。
pub const TICKET_RECHECK: TicketType = TicketType::new("RECHECK", "成色复检", 0, false, true);
/// 投诉:**不需要**凭证,也**不走**保修(纯服务态度类,按金额走权限审批)。
pub const TICKET_COMPLAINT: TicketType = TicketType::new("COMPLAINT", "服务投诉", 0, false, false);

/// 内置工单类型清单。
///
/// ⚠️ 顺序 = 声明顺序(报表按这个顺序输出,不用排序,避免引入 `Ord`)。
pub const BUILTIN_TICKET_TYPES: [TicketType; 5] = [
    TICKET_RETURN,
    TICKET_EXCHANGE,
    TICKET_REPAIR,
    TICKET_RECHECK,
    TICKET_COMPLAINT,
];


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : calendar_date.rs
//! ★ Sakamoto 算法求星期 + 逐日推进 + 工作日推进。
//!
//! ## ⚠️ 既有工程的一条硬教训(本文件照做)
//!
//! **`(26 * (m + 1)) / 10` 不能代替月份偏移表**。它只是 `floor(2.6 * (m + 1))`
//! 的近似,与偏移表 `[0,3,2,5,0,3,5,1,4,6,2,4]` 在 3 月、9 月等多个月份不等,
//! 会导致**整年错一位**。既有工程因此吃过亏。
//! **有公认表格的算法,宁可把表写出来,也不要用「看起来等价」的算式。**
//!
//! ## 为什么「推进」用逐日循环而不是儒略日公式
//!
//! 逐日推进显然正确(只需处理「月末进位」「年末进位」「闰年 2 月」三件事);
//! 儒略日公式是 O(1),但多一个可能写错的公式。
//! 本工程的日期跨度都在一年以内,调用次数是个位数量级,性能无关紧要。
//! **当「明显正确的笨办法」与「可能写错的聪明办法」成本相当时,选笨办法。**

/// ★ 月份偏移量表(Sakamoto 算法)。
///
/// 下标 = 月份 − 1。⚠️ **不要**把它化简成 `(26 * (m + 1)) / 10` 之类,
/// 二者在 3 月、9 月不等价(见模块文档)。
const SAKAMOTO_MONTH_OFFSETS: [i32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];

/// 中文星期名,下标与 [`CalendarDate::weekday_index`] 一致:`0 = 周日`。
const WEEKDAY_CHINESE_NAMES: [&str; 7] = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];

/// 一个公历日期(年 / 月 / 日,三个整数)。
///
/// 刻意**不派生** `Default`:一个「默认日期」在业务上没有意义,
/// 能让调用方写出 `CalendarDate::default()` 只会掩盖「忘了传日期」这类错误。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CalendarDate {
    /// 公历年(如 2026)。用 `i32` 是因为要参与 `year - 1` 的中间运算。
    year: i32,
    /// 公历月,取值 `1..=12`。
    month: u32,
    /// 公历日,取值 `1..=31`(具体上界随年月变化)。
    day: u32,
}

impl CalendarDate {
    /// 构造。**故意不校验**------校验在 [`Self::is_valid`],
    /// 因为「构造一个待校验的日期」本身是合法需求(本工程用它做反例)。
    pub const fn new(year: i32, month: u32, day: u32) -> Self {
        CalendarDate { year, month, day }
    }

    /// 年份。
    pub const fn year(&self) -> i32 {
        self.year
    }

    /// 月份。
    pub const fn month(&self) -> u32 {
        self.month
    }

    /// 日。
    pub const fn day(&self) -> u32 {
        self.day
    }

    /// 判断年月日组合是否落在合法范围内。
    pub fn is_valid(&self) -> bool {
        if self.month < 1 || self.month > 12 {
            return false;
        }
        if self.day < 1 {
            return false;
        }
        self.day <= days_in_month(self.year, self.month)
    }

    /// 返回**星期索引**:`0 = 周日, 1 = 周一, ... 6 = 周六`(Sakamoto 算法)。
    ///
    /// ⚠️ 最后用 `rem_euclid(7)` 而不是 `% 7`:
    /// `%` 对负数会得到负数(Rust 与 C 一致),当作索引用会在公元前的年份上
    /// 直接 panic。虽然本工程用不到那些年份,但 `rem_euclid` 是正确的写法,
    /// **不依赖调用方的取值范围假设**。
    pub fn weekday_index(&self) -> usize {
        let month: i32 = self.month as i32;
        let day: i32 = self.day as i32;

        // ★ 1 月 / 2 月必须借上一年的年份------这一步不可省略。
        let adjusted_year: i32 = if month < 3 { self.year - 1 } else { self.year };

        // 月份偏移量查表(★ 不化简成算式)。
        let month_offset: i32 = SAKAMOTO_MONTH_OFFSETS[(self.month - 1) as usize];

        let raw_index: i32 = adjusted_year
            + adjusted_year / 4
            - adjusted_year / 100
            + adjusted_year / 400
            + month_offset
            + day;

        raw_index.rem_euclid(7) as usize
    }

    /// 中文星期名(如「周三」)。
    pub fn weekday_chinese_text(&self) -> &'static str {
        WEEKDAY_CHINESE_NAMES[self.weekday_index()]
    }

    /// 是否周末(周六或周日)。
    ///
    /// ⚠️ 写成 `index == 0 || index == 6` 而不是 `index >= 5`:
    /// 索引 0 是**周日**,用 `>= 5` 会把周五也算成周末。
    pub fn is_weekend(&self) -> bool {
        let index: usize = self.weekday_index();
        index == 0 || index == 6
    }

    /// 机器格式:`2026-10-07`(定长 10 列,适合进表格)。
    ///
    /// 定长是本函数存在的理由:`{}-{}-{}` 直接拼会让 2026-1-7 变成 8 列,
    /// 与 `2026-10-07`(10 列)宽度不同,表格立刻歪掉。
    pub fn machine_text(&self) -> String {
        format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
    }

    /// 中文格式:`2026年10月7日`(**不定长**,仅用于正文说明,不进定宽列)。
    pub fn chinese_text(&self) -> String {
        format!("{}年{}月{}日", self.year, self.month, self.day)
    }

    /// 中文格式 + 星期:`2026年10月7日(周三)`。
    pub fn chinese_text_with_weekday(&self) -> String {
        format!("{}({})", self.chinese_text(), self.weekday_chinese_text())
    }

    /// 返回当天的前一天 / 后一天(由 `step` 决定,取值 `+1` 或 `-1`)。
    fn step_one_day(&self, step: i32) -> CalendarDate {
        let mut year: i32 = self.year;
        let mut month: u32 = self.month;
        let mut day: i32 = self.day as i32 + step;

        if day < 1 {
            // 向前借一个月。
            month -= 1;
            if month < 1 {
                month = 12;
                year -= 1;
            }
            day = days_in_month(year, month) as i32;
        } else if day > days_in_month(year, month) as i32 {
            // 向后进一个月。
            day = 1;
            month += 1;
            if month > 12 {
                month = 1;
                year += 1;
            }
        }

        CalendarDate::new(year, month, day as u32)
    }

    /// 加 / 减若干**自然日**。
    ///
    /// ⚠️ 循环带硬上限:算错的日期会变成**卡住的程序**,
    /// 那是比「算错」更糟的失败形态(既有工程的教训)。
    pub fn add_days(&self, delta: i32) -> CalendarDate {
        let mut current: CalendarDate = *self;
        let step: i32 = if delta >= 0 { 1 } else { -1 };
        let mut remaining: i32 = delta.abs();
        let mut guard: i32 = 0;
        while remaining > 0 && guard < 100_000 {
            current = current.step_one_day(step);
            remaining -= 1;
            guard += 1;
        }
        current
    }

    /// 推进若干**工作日**(跳过周末)。
    ///
    /// ★ **起点当天不计入**:`add_working_days(1)` 得到「下一个工作日」,
    /// 这正是「受理日的次日开工」的语义。
    ///
    /// ⚠️ 因此「工期 N 个工作日的完成日」要写成 `add_working_days(N - 1)`------
    /// 那个 `− 1` 不是笔误,是**把起点那天收回来**。这两个 `±1` 必须成对理解。
    pub fn add_working_days(&self, delta: i32) -> CalendarDate {
        let mut current: CalendarDate = *self;
        let step: i32 = if delta >= 0 { 1 } else { -1 };
        let mut remaining: i32 = delta.abs();
        let mut guard: i32 = 0;
        while remaining > 0 && guard < 100_000 {
            current = current.step_one_day(step);
            if !current.is_weekend() {
                remaining -= 1;
            }
            guard += 1;
        }
        current
    }
}

/// 是否闰年。
///
/// ★ 三条规则,**第三条最容易被漏**:
/// ① 能被 4 整除;② 不能被 100 整除;③ **除非**能被 400 整除。
/// 因此校验时必须用 **2100 年**做探针(2000 年是闰年,发现不了漏了第三条)。
pub const fn is_leap_year(year: i32) -> bool {
    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

/// 某年某月的天数。
pub const fn days_in_month(year: i32, month: u32) -> u32 {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 => {
            if is_leap_year(year) {
                29
            } else {
                28
            }
        }
        // 非法月份返回 0:让 `is_valid` 判 false,而不是 panic。
        _ => 0,
    }
}

/// 两个日期之间的天数差(`to - from`,可为负)。
///
/// ⚠️ 同样带上界,理由同 [`CalendarDate::add_days`]。
pub fn days_between(from: CalendarDate, to: CalendarDate) -> i64 {
    let mut count: i64 = 0;
    let mut current: CalendarDate = from;
    let mut guard: i64 = 0;
    if from <= to {
        while current < to && guard < 100_000 {
            current = current.step_one_day(1);
            count += 1;
            guard += 1;
        }
    } else {
        while current > to && guard < 100_000 {
            current = current.step_one_day(-1);
            count -= 1;
            guard += 1;
        }
    }
    count
}

/// 把日期渲染成**表格单元格**文本。
///
/// ⚠️ 刻意走 [`CalendarDate::machine_text`](定长 10 列)而不是中文格式:
/// 中文格式不定长(`2026年10月7日` 与 `2026年1月7日` 宽度不同),
/// 进表格会歪。中文格式只在注解里出现。
pub fn report_cell_text(date: CalendarDate) -> String {
    date.machine_text()
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : deterministic_code.rs
//! 确定性编码:FNV-1a 64 位 + base36 定长输出。
//!
//! ## 为什么不用 `DefaultHasher`
//!
//! `std::collections::hash_map::DefaultHasher` 的值**跨 Rust 版本不保证稳定**,
//! 而本工程的验收线是「两次运行逐字节一致」------跨版本不稳定意味着
//! 换一次工具链,输出的编号就全变了。
//! 更实际的问题是:**读者无法复算**。自己实现 FNV-1a 后,
//! 编号的生成规则写在这 80 行里,任何人都能手算验一遍。
//!
//! ## 为什么要段间分隔符
//!
//! 把 `["AB", "C"]` 与 `["A", "BC"]` 直接拼起来都是 `"ABC"`,会**撞哈希**。
//! 段间插入 `0x1f`(ASCII Unit Separator)之后两者不同。
//! 这是既有工程的实测教训:**拼接型哈希必须有分隔符**。

/// FNV-1a 64 位偏移基址。
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
/// FNV-1a 64 位质数。
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
/// 段间分隔符:ASCII Unit Separator(**选它的理由就是它几乎不会出现在文本里**)。
const SEGMENT_SEPARATOR: u8 = 0x1f;

/// 把若干段拼成一个**稳定的 64 位哈希**。
///
/// 段与段之间插入 [`SEGMENT_SEPARATOR`],防止 `["AB","C"]` 与 `["A","BC"]` 碰撞。
pub fn stable_hash(parts: &[&str]) -> u64 {
    let mut hash: u64 = FNV_OFFSET_BASIS;
    for (index, part) in parts.iter().enumerate() {
        // 除第一段外,先喂一个分隔符。
        if index > 0 {
            hash ^= u64::from(SEGMENT_SEPARATOR);
            hash = hash.wrapping_mul(FNV_PRIME);
        }
        for byte in part.as_bytes() {
            hash ^= u64::from(*byte);
            hash = hash.wrapping_mul(FNV_PRIME);
        }
    }
    hash
}

/// base36 字符集(数字 + 大写字母)。
const BASE36_ALPHABET: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

/// 由若干段生成**定长**的 base36 编码。
///
/// 定长是刻意的:编号要进定宽表格列,长度不定会把列顶歪。
///
/// ⚠️ `width` 取值 `1..=13`:64 位无符号数在 base36 下最多 13 位,
/// 超过 13 位高位全是 `0`,写出来是**假精度**(看起来信息量大,其实都是零)。
pub fn deterministic_code(parts: &[&str], width: usize) -> String {
    debug_assert!(width >= 1 && width <= 13, "base36 最多 13 位,width={} 会产出假精度", width);
    let mut value: u64 = stable_hash(parts);
    let mut buffer: Vec<char> = Vec::with_capacity(width);
    for _ in 0..width {
        let digit: usize = (value % 36) as usize;
        buffer.push(BASE36_ALPHABET[digit] as char);
        value /= 36;
    }
    buffer.iter().rev().collect()
}

/// 带前缀的确定性编码:`前缀-XXXXXX`。
///
/// 前缀让编号**可人读**(`CARD-` / `TPL-` / `FIX-`),
/// 后面那段保证唯一且可复算。
pub fn prefixed_code(prefix: &str, parts: &[&str], width: usize) -> String {
    format!("{}-{}", prefix, deterministic_code(parts, width))
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : text_layout.rs
//! 显示宽度与版面工具:**中文占 2 列**这一事实的全部实现都在这里。
//!
//! ## 为什么不能直接用 `{:<N}` 或 `str::len()`
//!
//! `{:<N}` 按**字符数**填充,而终端里一个汉字占 2 个显示列。
//! 于是「工序段」(3 字 = 6 列) 与 `STAGE`(5 列) 在同一格里会错开 1 列,
//! 整张表立刻歪掉------**而且歪得很整齐**,看不出来是错的。
//! `str::len()` 是**字节数**,中文是 3 字节,更离谱。
//!
//! ## 宽度模型:按 `east_asian_width` 的 `W` / `F` 判 2 列
//!
//! 这是 Unicode 的标准属性。⚠️ 但要注意两类**看起来像宽字符其实不是**的:
//! - `─`(U+2500,制表符):属性是 `A`(歧义),本工程按 **1 列**计;
//! - `...`(U+2026,省略号):属性是 `A`,本工程按 **1 列**计。
//!
//! ⚠️ 既有工程踩过的坑:曾凭印象写「U+2500 落在 CJK 区间 `U+2E80..=0x303E` 里」,
//! 而 **U+2500 = 9472 < 0x2E80 = 11904**,根本没落进去。
//! 结论:**有公认表格的判断,宁可让机器算一遍**------所以本文件有
//! [`self_check_width_model`] 这条永久自检。

/// 判断一个字符是否占 **2 个显示列**。
///
/// 区间表按 Unicode `East_Asian_Width` 的 `W` / `F` 摘取,
/// 各区间**互不重叠**(写重叠区间不会有错,但会让读者以为有优先级)。
pub const fn is_wide_character(character: char) -> bool {
    let code: u32 = character as u32;
    matches!(code,
        // CJK 统一汉字及扩展(不含后面单独列出的兼容区)
        0x1100..=0x115F     // 谚文首字母
        | 0x2E80..=0x303E   // CJK 部首、康熙部首、CJK 符号与标点
        | 0x3041..=0x33FF   // 平假名、片假名、注音、谚文兼容、CJK 兼容
        | 0x3400..=0x4DBF   // CJK 扩展 A
        | 0x4E00..=0x9FFF   // CJK 基本区(最常用的汉字都在这里)
        | 0xA000..=0xA4CF   // 彝文
        | 0xAC00..=0xD7A3   // 谚文音节
        | 0xF900..=0xFAFF   // CJK 兼容汉字
        | 0xFE10..=0xFE19   // 竖排标点
        | 0xFE30..=0xFE6F   // CJK 兼容形式
        | 0xFF00..=0xFF60   // 全角 ASCII、全角标点
        | 0xFFE0..=0xFFE6   // 全角符号
        | 0x1F300..=0x1F64F // 表情符号(常用段)
        | 0x1F900..=0x1F9FF // 补充表情符号
        | 0x20000..=0x2FFFD // CJK 扩展 B..F
        | 0x30000..=0x3FFFD // CJK 扩展 G
    )
}

/// 计算一个字符串的**显示宽度**(中文 2 列、英文 1 列)。
pub fn display_width(text: &str) -> usize {
    text.chars().map(|character| if is_wide_character(character) { 2 } else { 1 }).sum()
}

/// 右填充(左对齐):不足补空格,**超出不截断**。
///
/// 不截断是刻意的:调用方要截断请显式调 [`truncate_to_width`],
/// 让「这里可能被截」这件事在代码里看得见。
pub fn pad_right(text: &str, width: usize) -> String {
    let used: usize = display_width(text);
    if used >= width {
        return text.to_string();
    }
    format!("{}{}", text, " ".repeat(width - used))
}

/// 左填充(右对齐):数字列用这个。
pub fn pad_left(text: &str, width: usize) -> String {
    let used: usize = display_width(text);
    if used >= width {
        return text.to_string();
    }
    format!("{}{}", " ".repeat(width - used), text)
}

/// 居中填充:**左侧少、右侧多**(既有多余的一列给右边)。
///
/// 为什么必须定死「奇数给右边」:不定死时,同一个表头在不同行
/// 可能一个偏左一个偏右,看起来像内容变了。
pub fn pad_center(text: &str, width: usize) -> String {
    let used: usize = display_width(text);
    if used >= width {
        return text.to_string();
    }
    let total: usize = width - used;
    let left: usize = total / 2;
    let right: usize = total - left;
    format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
}

/// 按显示宽度截断,**不劈开半个汉字**。
///
/// ⚠️ 返回值宽度可能在 `width - 1 ..= width` 之间浮动:
/// 放不下一个 2 列宽的汉字时,它会少补 1 列。
/// 因此**任何「必须恰好等于 width」的断言都是错的**。
pub fn truncate_to_width(text: &str, width: usize) -> String {
    let mut used: usize = 0;
    let mut result: String = String::new();
    for character in text.chars() {
        let character_width: usize = if is_wide_character(character) { 2 } else { 1 };
        if used + character_width > width {
            break;
        }
        result.push(character);
        used += character_width;
    }
    result
}

/// 可见省略:放不下时截断并追加 `...`,**让读者知道这里被省了**。
///
/// ⚠️ 必须先给 `...` **留 1 列**:写成 `truncate(text, width) + "..."`
/// 会让结果变成 `width + 1` 列,把整行顶歪。
pub fn elide_text(text: &str, width: usize) -> String {
    if display_width(text) <= width {
        return text.to_string();
    }
    // width 为 0 时无法放省略号,退化成空串(不 panic:这是排版,不是逻辑错误)。
    if width == 0 {
        return String::new();
    }
    let keep: usize = width - 1;
    format!("{}...", truncate_to_width(text, keep))
}

/// 生成一条指定宽度的分隔线。
///
/// ⚠️ 拼 `width` 个 `─`,**不是** `width / 2` 个。
/// `─`(U+2500) 按 1 列计(见模块文档),写 `width / 2` 会得到一半长的线。
pub fn horizontal_rule(width: usize) -> String {
    "─".repeat(width)
}

/// ★ 宽度模型的**永久自检**:返回 `(探针名, 是否通过, 实测说明)`。
///
/// 为什么要有它:宽度常量写错时,报表**看起来完全正常**(只是分隔线短一截、
/// 某一列窄一点),肉眼几乎不可能发现。自检把「看起来正常」变成「有数字对不上」。
///
/// 探针的选择:`0 / 1 / 2` 覆盖边界;`17 / 92 / 153` 覆盖奇偶与较大值。
pub fn self_check_width_model() -> Vec<(String, bool, String)> {
    let mut probes: Vec<(String, bool, String)> = Vec::new();

    // 探针一:`horizontal_rule(n)` 的宽度必须**恰好**是 n(制表符按 1 列计)。
    for width in [0usize, 1, 2, 17, 92, 153] {
        let rule: String = horizontal_rule(width);
        let actual: usize = display_width(&rule);
        probes.push((
            format!("分隔线宽度 {}", width),
            actual == width,
            format!("期望 {} 列,实测 {} 列", width, actual),
        ));
    }

    // 探针二:`elide_text` 的结果**绝不超过**目标宽度(省略号占 1 列)。
    for width in [1usize, 2, 17, 92] {
        let source: String = "工序段".repeat(60);
        let elided: String = elide_text(&source, width);
        let actual: usize = display_width(&elided);
        probes.push((
            format!("省略后宽度 ≤ {}", width),
            actual <= width,
            format!("原串 {} 列,省略后 {} 列", display_width(&source), actual),
        ));
    }

    // 探针三:够宽时 `elide_text` 必须**原样返回**(一个字都不能少)。
    let untouched: &str = "镶嵌工艺路线卡";
    let pass_through: String = elide_text(untouched, 40);
    probes.push((
        "够宽时原样返回".to_string(),
        pass_through == untouched,
        format!("原串 {} 列", display_width(untouched)),
    ));

    probes
}


//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : chain_audit.rs
//! 责任链的契约审计:**五条不变量 + 每条的已知失败输入**。
//!
//! ## ★ 恒真的检查没有价值
//!
//! 一条规则若对**任何**输入都成立,它就不是规则,只是一句没被跑过的注释。
//! 因此本模块每条规则都必须写出「已知会失败的输入」------
//! 本工程的幕八会把那个输入真的喂进去,确认它会红。
//!
//! ## ★ 为什么观测是**结构体**而不是「从链上直接读」
//!
//! 若观测只能由链产生,那么规则永远只能看到**链自己写出来的**数据,
//! 于是「链的记账写错了」这类问题永远查不出来。
//! 让观测可以**手工组装**,规则才第一次有可能被喂进一个坏样本。
//! (代价是:手工组装的观测也可能是错的------所以每条规则都配了「已知失败输入」。)
//!
//! ## ⚠️ 本层同样零 `crate::` 依赖
//!
//! 观测里的请求标识只是一个字符串标签(`request_label`),
//! 不是任何业务类型------机制层不需要知道「工单号」是什么。

use super::chain_result::ChainResult;
use super::handler_kind::HandlerKind;

/// ★ 一次投递的观测(**可手工组装**,理由见模块文档)。
///
/// # 字段说明
/// - `request_label`:请求标识(只是个标签,机制层不解释它)。
/// - `settled_by`:★ 处置者名单(正常是 0 个或 1 个;**2 个以上就是违规**)。
/// - `settled_kind`:处置者的种类(无人处置时为 `None`)。
/// - `handler_name_label`:★ 报表上写出来的处置者名(无人处置时应是
///   [`super::chain_result::UNHANDLED_MARK`],**不得是空串**)。
/// - `visited_position`:被访问处理器在链上的位置(按序)。
/// - `declared_total`:链上处理器总数。
#[derive(Clone, Debug)]
pub struct RequestObservation {
    /// 请求标识。
    request_label: String,
    /// 报表上写出来的处置者名。
    handler_name_label: String,
    /// 处置者名单。
    settled_by: Vec<String>,
    /// 处置者种类。
    settled_kind: Option<HandlerKind>,
    /// 被访问处理器在链上的位置(按序)。
    visited_position: Vec<usize>,
    /// 链上处理器总数。
    declared_total: usize,
}

impl RequestObservation {
    /// ★ 手工组装一条观测(**审计的负向用例靠这个入口**)。
    ///
    /// # 参数
    /// - `request_label`:请求标识。
    /// - `settled_by`:处置者名单(可为空、可为多个------后者就是违规样本)。
    /// - `settled_kind`:处置者种类。
    /// - `handler_name_label`:报表上写出来的处置者名(**空串即违规样本**)。
    /// - `visited_position`:被访问位置序列(可乱序------后者也是违规样本)。
    /// - `declared_total`:链上处理器总数。
    pub fn new(
        request_label: &str,
        settled_by: Vec<String>,
        settled_kind: Option<HandlerKind>,
        handler_name_label: &str,
        visited_position: Vec<usize>,
        declared_total: usize,
    ) -> Self {
        Self {
            request_label: request_label.to_string(),
            handler_name_label: handler_name_label.to_string(),
            settled_by,
            settled_kind,
            visited_position,
            declared_total,
        }
    }

    /// 从一次真实投递的结果组装观测。
    ///
    /// # 参数
    /// - `request_label`:请求标识(由调用方给,机制层不知道它是什么)。
    /// - `result`:链返回的结果。
    pub fn from_result<R>(request_label: &str, result: &ChainResult<R>) -> Self {
        let settled_by: Vec<String> = match result.result_ref() {
            Some(_) => vec![result.handler_name().to_string()],
            None => Vec::new(),
        };
        Self {
            request_label: request_label.to_string(),
            handler_name_label: result.handler_name().to_string(),
            settled_by,
            settled_kind: result.handler_kind(),
            visited_position: result.visited_position().to_vec(),
            declared_total: result.declared_total(),
        }
    }

    /// 请求标识。
    pub fn request_label(&self) -> &str {
        self.request_label.as_str()
    }

    /// 是否被处置。
    pub fn is_settled(&self) -> bool {
        !self.settled_by.is_empty()
    }

    /// 报表上写出来的处置者名。
    pub fn handler_name_label(&self) -> &str {
        self.handler_name_label.as_str()
    }

    /// 处置者名单。
    pub fn settled_by(&self) -> &[String] {
        self.settled_by.as_slice()
    }

    /// 处置者种类。
    pub const fn settled_kind(&self) -> Option<HandlerKind> {
        self.settled_kind
    }

    /// 被访问位置序列。
    pub fn visited_position(&self) -> &[usize] {
        self.visited_position.as_slice()
    }

    /// 链上处理器总数。
    pub const fn declared_total(&self) -> usize {
        self.declared_total
    }
}

/// ★ 责任链的契约规则(**封闭集合**:漏一条 `match` 就编译不过去)。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ChainAuditRule {
    /// 每张请求都必须被处置(不允许消失在链尾)。
    EveryRequestSettled,
    /// 一张请求至多一个处置者。
    AtMostOneHandler,
    /// 访问顺序必须**不减**(链不能往回走)。
    ChainRespectsOrder,
    /// ★ 处置者名**不得是空串**(否则报表上就是一个空格子)。
    UnhandledReportsNoHandler,
    /// 过路处理器(`PassThrough`)不出现在处置者里。
    PassThroughNeverSettles,
    /// 被访问的位置必须都在链上(不得越界)。
    VisitWithinDeclared,
}

impl ChainAuditRule {
    /// 规则编码(写进报表)。
    pub const fn code(&self) -> &'static str {
        match self {
            ChainAuditRule::EveryRequestSettled => "SETTLED",
            ChainAuditRule::AtMostOneHandler => "SINGLE",
            ChainAuditRule::ChainRespectsOrder => "ORDERED",
            ChainAuditRule::UnhandledReportsNoHandler => "NO_GHOST",
            ChainAuditRule::PassThroughNeverSettles => "NO_PASSTHROUGH",
            ChainAuditRule::VisitWithinDeclared => "IN_RANGE",
        }
    }

    /// 规则中文名。
    pub const fn chinese_text(&self) -> &'static str {
        match self {
            ChainAuditRule::EveryRequestSettled => "每张请求都有处置结论",
            ChainAuditRule::AtMostOneHandler => "一张请求至多一个处置者",
            ChainAuditRule::ChainRespectsOrder => "访问顺序不回头(不能往回走)",
            ChainAuditRule::UnhandledReportsNoHandler => "处置者名不得是空串",
            ChainAuditRule::PassThroughNeverSettles => "过路处理器不做决定",
            ChainAuditRule::VisitWithinDeclared => "被访问的位置都在链上",
        }
    }

    /// ★ 期望文本(写进报表的「期望」列)。
    pub const fn expectation_text(&self) -> &'static str {
        match self {
            ChainAuditRule::EveryRequestSettled => "处置者数 >= 1",
            ChainAuditRule::AtMostOneHandler => "处置者数 <= 1",
            ChainAuditRule::ChainRespectsOrder => "位置序列单调不减",
            ChainAuditRule::UnhandledReportsNoHandler => "处置者名非空(无人接时为专用标记)",
            ChainAuditRule::PassThroughNeverSettles => "处置者种类 != 过路者",
            ChainAuditRule::VisitWithinDeclared => "所有位置 < 链上处理器数",
        }
    }

    /// ★ 已知会失败的输入(**没有这一条,规则就是一句注释**)。
    pub const fn known_failing_input_text(&self) -> &'static str {
        match self {
            // ⚠️ 措辞里**不许出现业务词**:这一层连「工单」都不认识,
            //    它只知道「请求」。写成「超大额工单」的话,
            //    换一个业务就要改机制层,机制就不再是机制
            //    (`scripts/check_layer_redlines.sh` 的 R9 专门扫这件事)。
            ChainAuditRule::EveryRequestSettled => {
                "把链尾的兜底处理器摘掉,再投一个谁都接不住的请求(无人接手)"
            }
            ChainAuditRule::AtMostOneHandler => {
                "手工造一条观测,把处置者名单写成两个处理器名"
            }
            ChainAuditRule::ChainRespectsOrder => {
                "手工造一条观测,把访问位置写成 [3, 1](先访问了后面的站)"
            }
            ChainAuditRule::UnhandledReportsNoHandler => {
                "手工造一条观测:把处置者名写成空串(报表上就是一个空格子)"
            }
            ChainAuditRule::PassThroughNeverSettles => {
                "手工造一条观测:处置者种类写成过路者,却仍带着处置结论"
            }
            ChainAuditRule::VisitWithinDeclared => {
                "手工造一条观测:访问位置写成 [9](超出链上处理器总数)"
            }
        }
    }

    /// 判断一条观测是否合规。
    ///
    /// # 参数
    /// - `observation`:待判的观测。
    ///
    /// # 返回
    /// `true` = 合规。
    fn conforms(&self, observation: &RequestObservation) -> bool {
        match self {
            ChainAuditRule::EveryRequestSettled => observation.is_settled(),
            ChainAuditRule::AtMostOneHandler => observation.settled_by().len() <= 1,
            ChainAuditRule::ChainRespectsOrder => {
                let mut previous: usize = 0;
                for position in observation.visited_position().iter() {
                    if *position < previous {
                        return false;
                    }
                    previous = *position;
                }
                true
            }
            ChainAuditRule::UnhandledReportsNoHandler => {
                !observation.handler_name_label().trim().is_empty()
            }
            ChainAuditRule::PassThroughNeverSettles => match observation.settled_kind() {
                Some(HandlerKind::PassThrough) => !observation.is_settled(),
                _ => true,
            },
            ChainAuditRule::VisitWithinDeclared => observation
                .visited_position()
                .iter()
                .all(|position| *position < observation.declared_total()),
        }
    }

    /// 全部规则(**穷尽**,用于报表与自检)。
    pub const fn all() -> [ChainAuditRule; 6] {
        [
            ChainAuditRule::EveryRequestSettled,
            ChainAuditRule::AtMostOneHandler,
            ChainAuditRule::ChainRespectsOrder,
            ChainAuditRule::UnhandledReportsNoHandler,
            ChainAuditRule::PassThroughNeverSettles,
            ChainAuditRule::VisitWithinDeclared,
        ]
    }
}

/// 一条规则对一批观测的检查结果。
#[derive(Clone, Debug)]
pub struct RuleCheck {
    /// 规则。
    rule: ChainAuditRule,
    /// 适用条数(**为 0 则不算通过**:恒真的检查没有价值)。
    applicable_total: usize,
    /// 合规条数。
    conforming_total: usize,
    /// 违规的请求标识。
    violating_labels: Vec<String>,
    /// 实测文本。
    actual_text: String,
}

impl RuleCheck {
    /// ★ 是否通过:**适用条数 > 0** 且**全部合规**。
    ///
    /// ⚠️ `applicable_total > 0` 这一半不能省:
    /// 一批空观测会让每条规则都「通过」,而报表上看起来像检查过了。
    pub fn passed(&self) -> bool {
        self.applicable_total > 0 && self.conforming_total == self.applicable_total
    }

    /// 规则。
    pub const fn rule(&self) -> ChainAuditRule {
        self.rule
    }

    /// 适用条数。
    pub const fn applicable_total(&self) -> usize {
        self.applicable_total
    }

    /// 合规条数。
    pub const fn conforming_total(&self) -> usize {
        self.conforming_total
    }

    /// 违规的请求标识。
    pub fn violating_labels(&self) -> &[String] {
        self.violating_labels.as_slice()
    }

    /// 实测文本。
    pub fn actual_text(&self) -> &str {
        self.actual_text.as_str()
    }
}

/// ★ 跑一遍全部规则。
///
/// # 参数
/// - `observations`:观测集合(正常用例或负向用例各跑一次)。
pub fn audit_chain(observations: &[RequestObservation]) -> Vec<RuleCheck> {
    ChainAuditRule::all()
        .iter()
        .map(|rule| {
            let mut applicable_total: usize = 0;
            let mut conforming_total: usize = 0;
            let mut violating_labels: Vec<String> = Vec::new();
            for observation in observations.iter() {
                applicable_total += 1;
                if rule.conforms(observation) {
                    conforming_total += 1;
                } else {
                    violating_labels.push(observation.request_label().to_string());
                }
            }
            RuleCheck {
                rule: *rule,
                applicable_total,
                conforming_total,
                violating_labels,
                actual_text: format!("{}/{}", conforming_total, applicable_total),
            }
        })
        .collect()
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : chain_ledger.rs
//! 链台账:**只增不减**的一次性计数。
//!
//! ## ★ 为什么「只增不减」要单独拿出来说
//!
//! 台账的每一个数都是「从开机到现在累计发生了几次」。
//! 一旦提供 `reset` 或 `set`,报表上就会出现一件无法解释的事:
//! 先渲染的那几行与后渲染的那几行**来自不同时刻**,
//! 而读者只会以为「跑这些实验没有副作用」------事实恰好相反
//! (每跑一次实验,访问次数就涨一截)。
//!
//! 因此本层**不提供任何清零方法**(这也是一条红线,见 `scripts/check_layer_redlines.sh`),
//! 报表一律读 [`ChainLedger::snapshot`]。
//!
//! ## 恒等式(可计算,不是原则)
//!
//! ```text
//! 投递数 = 被接下 + 无人接
//! 被接下 = Σ(每个处理器的接下次数)
//! 访问次数 = 被接下 + Σ(每个处理器的放行次数)
//! ```
//!
//! 这三条一旦有一条差一个数,「某条路径漏记了」就变成一个具体的数字,
//! 而不是一句「应该没问题」。

use std::cell::{Cell, RefCell};

/// 一个处理器累计的尝试情况。
///
/// # 字段说明
/// - `name`:处理器名。
/// - `asked_total`:★ `can_handle` 被调用了几次(**被问了几次**)。
/// - `handled_total`:接下了几次。
/// - `passed_total`:放行了几次。
#[derive(Clone, Debug)]
pub struct HandlerAttempt {
    /// 处理器名。
    name: String,
    /// 被问了几次。
    asked_total: usize,
    /// 接下了几次。
    handled_total: usize,
    /// 放行了几次。
    passed_total: usize,
}

impl HandlerAttempt {
    /// 处理器名。
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// 被问了几次。
    pub const fn asked_total(&self) -> usize {
        self.asked_total
    }

    /// 接下了几次。
    pub const fn handled_total(&self) -> usize {
        self.handled_total
    }

    /// 放行了几次。
    pub const fn passed_total(&self) -> usize {
        self.passed_total
    }

    /// ★ 是否**一次都没被问过**(即「死站」)。
    ///
    /// 死站有两种可能:① 这一站多余(可以删);
    /// ② 前面某一站的判据写宽了,把本该到这一站的单全接走了。
    /// 两种情况报表都值得单独列出来------它们都不会自己说话。
    pub const fn never_asked(&self) -> bool {
        self.asked_total == 0
    }
}

/// 链台账(**只增不减**)。
#[derive(Debug)]
pub struct ChainLedger {
    /// 累计投递数。
    submitted_total: Cell<usize>,
    /// 累计被接下数。
    handled_total: Cell<usize>,
    /// 累计无人接数。
    unhandled_total: Cell<usize>,
    /// 累计访问处理器次数。
    visit_total: Cell<usize>,
    /// ★ 累计**跳过**的判定次数(排在处置者之后、一次都没被调用的站数之和)。
    skipped_total: Cell<usize>,
    /// 每个处理器的尝试情况(**按首次被问的顺序**,线性查找,不用哈希)。
    attempts: RefCell<Vec<HandlerAttempt>>,
    /// 处置顺序(谁接了第几单的**处理器名**序列)。
    settle_order: RefCell<Vec<String>>,
}

impl ChainLedger {
    /// 建一本空台账。
    pub fn new() -> Self {
        Self {
            submitted_total: Cell::new(0),
            handled_total: Cell::new(0),
            unhandled_total: Cell::new(0),
            visit_total: Cell::new(0),
            skipped_total: Cell::new(0),
            attempts: RefCell::new(Vec::new()),
            settle_order: RefCell::new(Vec::new()),
        }
    }

    /// 记一次投递。
    pub fn record_submission(&self) {
        self.submitted_total.set(self.submitted_total.get() + 1);
    }

    /// ★ 记一次「被问」(`can_handle` 被调用)。
    ///
    /// # 参数
    /// - `handler_name`:处理器名(**首次出现时**在这里建档)。
    pub fn record_visit(&self, handler_name: &str) {
        self.visit_total.set(self.visit_total.get() + 1);
        let mut attempts = self.attempts.borrow_mut();
        for attempt in attempts.iter_mut() {
            if attempt.name == handler_name {
                attempt.asked_total += 1;
                return;
            }
        }
        attempts.push(HandlerAttempt {
            name: handler_name.to_string(),
            asked_total: 1,
            handled_total: 0,
            passed_total: 0,
        });
    }

    /// 记一次接下。
    pub fn record_handled(&self, handler_name: &str) {
        self.handled_total.set(self.handled_total.get() + 1);
        let mut attempts = self.attempts.borrow_mut();
        for attempt in attempts.iter_mut() {
            if attempt.name == handler_name {
                attempt.handled_total += 1;
                break;
            }
        }
        self.settle_order.borrow_mut().push(handler_name.to_string());
    }

    /// 记一次放行。
    pub fn record_passed(&self, handler_name: &str) {
        let mut attempts = self.attempts.borrow_mut();
        for attempt in attempts.iter_mut() {
            if attempt.name == handler_name {
                attempt.passed_total += 1;
                return;
            }
        }
        // ⚠️ 走到这里说明「放行」发生在「被问」之前------记账顺序反了。
        // 不静默补一条:那会让「被问次数」凭空少一次,而恒等式正好差 1。
        panic!("处理器 {} 还没被问过就记了一次放行(记账顺序反了)", handler_name);
    }

    /// 记一次无人接手。
    pub fn record_unhandled(&self) {
        self.unhandled_total.set(self.unhandled_total.get() + 1);
    }

    /// 记一次跳过(**整段**跳过多少站)。
    pub fn record_skipped(&self, skipped: usize) {
        self.skipped_total.set(self.skipped_total.get() + skipped);
    }

    /// ★ 快照(**报表一律读快照**,理由见模块文档)。
    pub fn snapshot(&self) -> LedgerSnapshot {
        LedgerSnapshot {
            submitted_total: self.submitted_total.get(),
            handled_total: self.handled_total.get(),
            unhandled_total: self.unhandled_total.get(),
            visit_total: self.visit_total.get(),
            skipped_total: self.skipped_total.get(),
            attempts: self.attempts.borrow().clone(),
            settle_order: self.settle_order.borrow().clone(),
        }
    }

    /// 实时投递数(**只给自检用**;报表请读快照)。
    pub fn submitted_total(&self) -> usize {
        self.submitted_total.get()
    }

    /// 实时被接下数。
    pub fn handled_total(&self) -> usize {
        self.handled_total.get()
    }

    /// 实时无人接数。
    pub fn unhandled_total(&self) -> usize {
        self.unhandled_total.get()
    }

    /// 实时访问次数。
    pub fn visit_total(&self) -> usize {
        self.visit_total.get()
    }

    /// 实时跳过次数。
    pub fn skipped_total(&self) -> usize {
        self.skipped_total.get()
    }
}

impl Default for ChainLedger {
    fn default() -> Self {
        Self::new()
    }
}

/// 台账快照(**不可变**,报表读它)。
#[derive(Clone, Debug)]
pub struct LedgerSnapshot {
    /// 投递数。
    pub submitted_total: usize,
    /// 被接下数。
    pub handled_total: usize,
    /// 无人接数。
    pub unhandled_total: usize,
    /// 访问次数。
    pub visit_total: usize,
    /// ★ 跳过次数(省下的判定次数)。
    pub skipped_total: usize,
    /// 每个处理器的尝试情况。
    pub attempts: Vec<HandlerAttempt>,
    /// 处置顺序。
    pub settle_order: Vec<String>,
}

impl LedgerSnapshot {
    /// ★ 恒等式一:`投递 == 被接下 + 无人接`。
    pub fn submission_identity_gap(&self) -> i64 {
        self.submitted_total as i64 - (self.handled_total + self.unhandled_total) as i64
    }

    /// ★ 恒等式二:`被接下 == Σ(每个处理器接下次数)`。
    pub fn settlement_identity_gap(&self) -> i64 {
        let summed: usize = self.attempts.iter().map(|a| a.handled_total).sum();
        self.handled_total as i64 - summed as i64
    }

    /// ★ 恒等式三:`访问 == 被接下 + Σ(放行次数)`。
    pub fn visit_identity_gap(&self) -> i64 {
        let passed: usize = self.attempts.iter().map(|a| a.passed_total).sum();
        self.visit_total as i64 - (self.handled_total + passed) as i64
    }

    /// ★ 一次都没被问过的处理器(**死站**)。
    pub fn never_asked_handlers(&self) -> Vec<&HandlerAttempt> {
        self.attempts.iter().filter(|a| a.never_asked()).collect()
    }

    /// ★ 被问过但一次都没接下的处理器(**纯过路**)。
    ///
    /// 与死站不同:它确实被问了、也确实做了事,只是从不做决定。
    /// 报表要把这两种分开------「删掉它」的结论只对死站成立。
    pub fn asked_but_never_handled_handlers(&self) -> Vec<&HandlerAttempt> {
        self
            .attempts
            .iter()
            .filter(|a| !a.never_asked() && a.handled_total == 0)
            .collect()
    }

    /// ★ 全量判定次数(**若不用责任链、每个处理器都问一遍**会是多少次)。
    ///
    /// 这是「省下多少」的分母:`投递数 × 处理器数`。
    pub fn judgement_total_if_broadcast(&self) -> usize {
        self.submitted_total * self.attempts.len()
    }

    /// ★ 省下的判定次数(全量 − 实际访问)。
    pub fn saved_judgement_total(&self) -> i64 {
        self.judgement_total_if_broadcast() as i64 - self.visit_total as i64
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : chain_result.rs
//! 一次投递的完整结果:**谁接的、经过了谁、结论是什么**。
//!
//! ## ★ 为什么结果里必须带上「经过了谁」
//!
//! 只给结论(`Option<R>`)的话,责任链与一个巨大的 `if-else` 在外部**完全无法区分**------
//! 两者都只是「给一个输入、得一个输出」。
//!
//! 带上「经过了谁」之后,下面这些话才第一次变得可说:
//! - 「这张单在第 2 站就被拦下了,后面 4 站一次都没被调用」→ **短路的价值**;
//! - 「把第 3 站挪到第 1 站,有 3 张单的处置者变了」→ **顺序是业务知识**;
//! - 「第 5 站一次都没被访问过」→ **这一站是死的**(要么多余,要么前面的判据写宽了)。

use super::handler_kind::HandlerKind;

/// ★ 无人接手时 [`ChainResult::handler_name`] 的取值。
///
/// ⚠️ 为什么不用空串:空串在报表上是一个**空格子**,
/// 读者分不清「没人接」与「忘了填」。
/// 一个明确的标记让「无人处置」在表格上是一个**词**。
pub const UNHANDLED_MARK: &str = "(无人接手)";

/// 一次投递的结果。
///
/// # 字段说明
/// - `result`:结论(`None` = 链上没人接)。
/// - `handler_name`:处置者名(无人接时为 [`UNHANDLED_MARK`])。
/// - `handler_kind`:处置者的种类(无人接时为 `None`)。
/// - `visited`:★ 被访问过的处理器名(**按访问顺序**)。
/// - `visited_position`:★ 被访问处理器在链上的**位置**(与 `visited` 一一对应,
///   用于检查「链是否按顺序走」)。
/// - `declared_total`:链上处理器总数(用于算「后面几站没被调用」)。
#[derive(Clone, Debug)]
pub struct ChainResult<R> {
    /// 结论。
    result: Option<R>,
    /// 处置者名。
    handler_name: String,
    /// 处置者种类。
    handler_kind: Option<HandlerKind>,
    /// 被访问过的处理器名(按序)。
    visited: Vec<String>,
    /// 被访问处理器在链上的位置(按序,与 `visited` 一一对应)。
    visited_position: Vec<usize>,
    /// 链上处理器总数。
    declared_total: usize,
}

impl<R> ChainResult<R> {
    /// 组装一次结果。
    ///
    /// # 参数
    /// - `result`:结论(`None` 表示无人接)。
    /// - `handler_name`:处置者名(无人接时传 [`UNHANDLED_MARK`])。
    /// - `handler_kind`:处置者种类。
    /// - `visited` / `visited_position`:访问轨迹(**两者长度必须相等**)。
    /// - `declared_total`:链上处理器总数。
    ///
    /// # Panics
    /// `visited` 与 `visited_position` 长度不等时 panic------
    /// 轨迹对不上说明机制层的记账写岔了,静默继续会让顺序检查永远通过。
    pub fn new(
        result: Option<R>,
        handler_name: &str,
        handler_kind: Option<HandlerKind>,
        visited: Vec<String>,
        visited_position: Vec<usize>,
        declared_total: usize,
    ) -> Self {
        assert_eq!(
            visited.len(),
            visited_position.len(),
            "访问轨迹的「名字」与「位置」必须一一对应:前者 {} 个、后者 {} 个",
            visited.len(),
            visited_position.len()
        );
        Self {
            result,
            handler_name: handler_name.to_string(),
            handler_kind,
            visited,
            visited_position,
            declared_total,
        }
    }

    /// 是否有人接手。
    pub const fn is_handled(&self) -> bool {
        self.result.is_some()
    }

    /// 处置者名(无人接时为 [`UNHANDLED_MARK`])。
    pub fn handler_name(&self) -> &str {
        self.handler_name.as_str()
    }

    /// 处置者种类(无人接时为 `None`)。
    pub const fn handler_kind(&self) -> Option<HandlerKind> {
        self.handler_kind
    }

    /// 结论(借用)。
    pub fn result_ref(&self) -> Option<&R> {
        self.result.as_ref()
    }

    /// 结论(移动)。
    pub fn into_result(self) -> Option<R> {
        self.result
    }

    /// 被访问过的处理器名(按序)。
    pub fn visited(&self) -> &[String] {
        self.visited.as_slice()
    }

    /// 被访问处理器在链上的位置(按序)。
    pub fn visited_position(&self) -> &[usize] {
        self.visited_position.as_slice()
    }

    /// ★ 被访问了几站(= 有几个处理器被调用了 `can_handle`)。
    pub fn visited_total(&self) -> usize {
        self.visited.len()
    }

    /// ★ 放行了几次(= 被访问 − 接下了 1 次)。
    ///
    /// 这是「短路」的量度:走了 3 站才处理掉 = 放行了 2 次。
    pub fn passed_total(&self) -> usize {
        self.visited_total().saturating_sub(if self.is_handled() { 1 } else { 0 })
    }

    /// ★ 链上**排在处置者之后、因而一次都没被调用**的站数。
    ///
    /// ⚠️ 这才是「责任链省下了什么」的可数形式:
    /// 不是省了时间(不可复现),而是**省了 N 次判定**。
    pub fn skipped_total(&self) -> usize {
        match self.visited_position().last() {
            Some(last_position) => self.declared_total.saturating_sub(*last_position + 1),
            None => self.declared_total,
        }
    }

    /// 链上处理器总数。
    pub const fn declared_total(&self) -> usize {
        self.declared_total
    }

    /// ★ 访问轨迹是否**单调不减**(即确实按链的顺序走)。
    ///
    /// 恒为真的检查没有价值;但这条的**输入**可以是手工造出来的观测
    /// (见 [`super::chain_audit`] 的已知失败用例),因此它真的会红。
    pub fn is_ordered(&self) -> bool {
        let mut previous: usize = 0;
        for position in self.visited_position().iter() {
            if *position < previous {
                return false;
            }
            previous = *position;
        }
        true
    }

    /// 轨迹文本(`登记 → 单据校验 → 门店权限` 形式)。
    pub fn visited_text(&self) -> String {
        if self.visited().is_empty() {
            return "(空链)".to_string();
        }
        self.visited().join(" → ")
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : chain_tail_policy.rs
//! 链尾策略:**走到最后都没人接,算不算错**。
//!
//! ## ★ 为什么这是一个策略而不是一个 `if`
//!
//! 「没人接」有两种完全正当的立场:
//!
//! - [`ChainTailPolicy::AllowUnhandled`]:有些流程本来就该有单子漏出去
//!   (比如「无人认领的咨询」,第二天会有客服再捞一遍);
//! - [`ChainTailPolicy::RequireHandled`]:有些流程**绝不允许**
//!   (本工程的售后工单:一张单消失在链尾,客户会一直等)。
//!
//! 若写成一个 `if`,这两种立场就变成代码里的一个布尔量,
//! 而**没人会在改装配时想起它**。
//! 写成一个有名字、有中文说明的枚举,它才能在报表上被列出来、
//! 才能被审计规则盯住。
//!
//! ⚠️ 唯一不可接受的是「不知道自己用的是哪一种」。

/// 链尾策略。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ChainTailPolicy {
    /// 允许无人接手(结论为 `None`,由调用方决定怎么办)。
    AllowUnhandled,
    /// ★ 必须有人接手:走到链尾还没人接就 **panic**。
    ///
    /// 为什么是 panic 而不是返回错误:`RequireHandled` 的语义是
    /// 「这件事**在装配上**不该发生」------
    /// 缺了兜底处理器是装配错误,不是业务分支。
    /// 返回 `Result` 会逼着每个调用点写 `?`,
    /// 而真正该做的是**把兜底处理器装上去**。
    RequireHandled,
}

impl ChainTailPolicy {
    /// 中文名。
    pub const fn chinese_text(&self) -> &'static str {
        match self {
            ChainTailPolicy::AllowUnhandled => "允许无人接手",
            ChainTailPolicy::RequireHandled => "必须有人接手(否则视为装配错误)",
        }
    }

    /// 机读编码。
    pub const fn machine_text(&self) -> &'static str {
        match self {
            ChainTailPolicy::AllowUnhandled => "ALLOW_UNHANDLED",
            ChainTailPolicy::RequireHandled => "REQUIRE_HANDLED",
        }
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : handler.rs
//! 处理器契约:链上的一个环节必须会做的三件事。
//!
//! ## ★ 为什么分成 `can_handle` 与 `handle` 两步
//!
//! 只写一个 `handle(&self, q) -> Handling<R>` 也能跑(接不了就返回 `Pass`)。
//! 但那样**「能不能接」这件事就没有任何一个地方能说清**:
//! 报表想知道「这单为什么给它而不是给下一个」,
//! 只能去读处理器的代码。
//!
//! 拆成两步之后:
//! - `can_handle` 是**判据**(纯问题,不该有副作用);
//! - `handle` 是**动作**(只在 `can_handle` 为真时被调用);
//! - 台账能分别记「问了几次」与「接了几次」,
//!   于是「判据写宽了导致某级接走了本不该接的单」变成一个可算的数。
//!
//! ## ★ 为什么没有 `set_next`
//!
//! 教科书里常见的写法是 `handler.set_next(next)`------**处理器持有下一个处理器**。
//! 那样的话:
//! - 链的形状散落在各个处理器里,没有一个地方能列出「这条链长什么样」;
//! - 换序要改每个处理器的装配代码;
//! - 「第 3 站被跳过了」无法被观测(跳过是处理器内部的事)。
//!
//! 本工程把**顺序收进链对象**([`super::handler_chain::HandlerChain`]),
//! 处理器彼此互不知情------这正是「顺序是业务知识、传递是机制」的分界。
//!
//! ## ⚠️ 为什么泛型参数放在 trait 上而不是方法上
//!
//! `dyn Handler<Q, R>` 要能用,trait 就**不能**有泛型方法
//! (有泛型方法就不是对象安全的,`dyn` 用不了)。
//! 把 `Q`(请求类型)与 `R`(结论类型)放在 trait 上,
//! 每个具体组合各自单态化,`dyn Handler<ServiceTicket, TicketResolution>` 完全合法。

use super::handling::Handling;
use super::handler_kind::HandlerKind;

/// 链上的一个处理器。
///
/// # 类型参数
/// - `Q`:请求类型。
/// - `R`:结论类型。
pub trait Handler<Q, R> {
    /// ★ 处理器名(**链内唯一**,写进台账与报表)。
    ///
    /// 为什么唯一很重要:台账按名字聚合计数,
    /// 两个同名处理器会被合成一行,于是「哪一级接得多」看不出来。
    fn name(&self) -> &str;

    /// 处理器种类(应答者 / 过路者)。
    fn kind(&self) -> HandlerKind;

    /// ★ 判据:这个请求归我管吗?
    ///
    /// ⚠️ 必须**无副作用**(只问不做事):
    /// 链会先问一遍再决定调不调 `handle`,
    /// 若这里偷偷记了数,「问了几次」与「做了几次」就会对不上。
    fn can_handle(&self, request: &Q) -> bool;

    /// 动作:处理它(**只在 [`Handler::can_handle`] 为真时被调用**)。
    ///
    /// 返回 [`Handling::Pass`] 表示「我做完了我那部分,交给下一个」。
    fn handle(&self, request: &Q) -> Handling<R>;
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : handler_chain.rs
//! 责任链本体:**按序问、问到能接为止**。
//!
//! ## ★ 顺序在这里,不在处理器里
//!
//! 教科书里常见的 `handler.set_next(next)` 把链的形状**散进每个处理器**,
//! 于是没有一个地方能列出「这条链长什么样」。
//! 本工程把顺序收进这个对象:
//!
//! - 换序 = 换这里的装配顺序(处理器本身一行不改);
//! - 「第 k 站被跳过了」可观测(跳过是链的事,不是处理器的事);
//! - 同一批处理器可以装成**两条不同的链**------本工程幕九正是靠这一点做换序对照。
//!
//! ## ★ 链完全不知道请求是什么
//!
//! `Q` / `R` 是泛型参数,本层**零 `crate::` 依赖**(与 `support` 同秩)。
//! 这不是洁癖:一旦这里写了 `use crate::domain::ServiceTicket`,
//! 「换一个业务要用责任链」就得改机制层------那时它就不是机制了。

use std::rc::Rc;

use super::chain_ledger::ChainLedger;
use super::chain_result::{ChainResult, UNHANDLED_MARK};
use super::chain_tail_policy::ChainTailPolicy;
use super::handling::Handling;
use super::handler::Handler;
use super::handler_kind::HandlerKind;

/// 一条责任链。
///
/// # 类型参数
/// - `Q`:请求类型。
/// - `R`:结论类型。
pub struct HandlerChain<Q, R> {
    /// 链名(写在报表上)。
    name: String,
    /// 链尾策略。
    policy: ChainTailPolicy,
    /// 处理器序列(**顺序即业务知识**)。
    handlers: Vec<Box<dyn Handler<Q, R>>>,
    /// 共享台账(**只增不减**)。
    ledger: Rc<ChainLedger>,
}

impl<Q, R> HandlerChain<Q, R> {
    /// 建一条空链。
    ///
    /// # 参数
    /// - `name`:链名。
    /// - `policy`:链尾策略(**必须显式给**,不允许默认值------
    ///   「不知道自己用的是哪一种」正是本工程要防的事)。
    /// - `ledger`:共享台账(多条链可以共用一本,以便合计)。
    pub fn new(name: &str, policy: ChainTailPolicy, ledger: Rc<ChainLedger>) -> Self {
        Self {
            name: name.to_string(),
            policy,
            handlers: Vec::new(),
            ledger,
        }
    }

    /// ★ 追加一个处理器(**链式装配**:`new(...).with_handler(a).with_handler(b)`)。
    ///
    /// 顺序 = 调用顺序。这是**唯一的**装配入口,
    /// 因此「这条链长什么样」只需看这一段代码。
    pub fn with_handler(mut self, handler: Box<dyn Handler<Q, R>>) -> Self {
        self.handlers.push(handler);
        self
    }

    /// 链名。
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// 链尾策略。
    pub const fn policy(&self) -> ChainTailPolicy {
        self.policy
    }

    /// 处理器总数。
    pub fn handler_total(&self) -> usize {
        self.handlers.len()
    }

    /// 处理器名清单(**按链上顺序**)。
    pub fn handler_names(&self) -> Vec<String> {
        self.handlers.iter().map(|h| h.name().to_string()).collect()
    }

    /// 处理器名与种类(**按链上顺序**)。
    pub fn handler_kinds(&self) -> Vec<(String, HandlerKind)> {
        self.handlers
            .iter()
            .map(|h| (h.name().to_string(), h.kind()))
            .collect()
    }

    /// 共享台账。
    pub fn ledger(&self) -> &Rc<ChainLedger> {
        &self.ledger
    }

    /// ★ 投递一个请求。
    ///
    /// ## 逐步做什么
    ///
    /// 1. 记一次投递;
    /// 2. **按序**问每个处理器 `can_handle`;
    ///    - 答「不能」→ 记一次放行,问下一个;
    ///    - 答「能」→ 调 `handle`:
    ///      - 返回 `Handled` → 记一次接下、**记跳过后面几站**、链终止;
    ///      - 返回 `Pass` → 记一次放行,问下一个;
    /// 3. 走完链尾:按 [`ChainTailPolicy`] 处理。
    ///
    /// # 参数
    /// - `request`:请求。
    ///
    /// # Panics
    /// 策略为 [`ChainTailPolicy::RequireHandled`] 且无人接手时 panic------
    /// 缺兜底处理器是**装配错误**,不是业务分支
    /// (理由见 [`super::chain_tail_policy`])。
    pub fn submit(&self, request: &Q) -> ChainResult<R> {
        self.ledger.record_submission();
        let mut visited: Vec<String> = Vec::new();
        let mut visited_position: Vec<usize> = Vec::new();

        for (position, handler) in self.handlers.iter().enumerate() {
            // ⚠️ 先记「被问」再问:否则一个 panic 的处理器的访问数会少一次,
            //    而恒等式三正好差 1,且差在报表上没有任何解释。
            self.ledger.record_visit(handler.name());
            visited.push(handler.name().to_string());
            visited_position.push(position);

            if !handler.can_handle(request) {
                self.ledger.record_passed(handler.name());
                continue;
            }

            match handler.handle(request) {
                Handling::Handled(result) => {
                    self.ledger.record_handled(handler.name());
                    // ★ 后面这几站**一次都没被调用**------这就是短路的可数形式。
                    let skipped: usize = self.handlers.len().saturating_sub(position + 1);
                    self.ledger.record_skipped(skipped);
                    return ChainResult::new(
                        Some(result),
                        handler.name(),
                        Some(handler.kind()),
                        visited,
                        visited_position,
                        self.handlers.len(),
                    );
                }
                Handling::Pass => {
                    self.ledger.record_passed(handler.name());
                }
            }
        }

        match self.policy {
            ChainTailPolicy::RequireHandled => panic!(
                "链「{}」要求每张请求都必须有人接手,但链上 {} 个处理器都没接下它\
                 (链尾缺少兜底处理器:这是装配错误,不是业务分支)",
                self.name,
                self.handlers.len()
            ),
            ChainTailPolicy::AllowUnhandled => {
                self.ledger.record_unhandled();
                ChainResult::new(
                    None,
                    UNHANDLED_MARK,
                    None,
                    visited,
                    visited_position,
                    self.handlers.len(),
                )
            }
        }
    }

    /// ★ 广播式投递:**每个处理器都问一遍,但仍是第一个接的算数**。
    ///
    /// ## 为什么要有这个方法
    ///
    /// 「责任链省了什么」需要一个**干净的对照**。
    /// 不用责任链的写法是「把每张单交给每一个环节各看一遍」(广播),
    /// 那时判定次数 = `投递数 × 处理器数`,与单子被谁接走无关。
    ///
    /// ⚠️ 这里刻意做成「**第一个接的算数**」:
    /// 于是广播式与责任链跑同一批处理器会得到**完全相同的结论**,
    /// 唯一的差别就是**判定次数**。
    /// 那样「省下 N 次判定」才是干净的------
    /// 若结论也不同,读者就无法判断差的值来自短路还是来自语义。
    ///
    /// # 参数
    /// - `request`:请求。
    /// - `ledger`:★ **独立的**台账。
    ///
    ///   ⚠️ 为什么必须另给一本:广播式的访问次数必然是 `N × 站数`,
    ///   若记进主台账,「省下多少次判定」这个数就被自己的对照实验污染了。
    ///   (既有工程的纪律:**每个用途各建一张图/一本账**。)
    pub fn submit_broadcast_first(&self, request: &Q, ledger: &Rc<ChainLedger>) -> ChainResult<R> {
        ledger.record_submission();
        let mut visited: Vec<String> = Vec::new();
        let mut visited_position: Vec<usize> = Vec::new();
        let mut first_handled: Option<(String, HandlerKind, R)> = None;

        for (position, handler) in self.handlers.iter().enumerate() {
            ledger.record_visit(handler.name());
            visited.push(handler.name().to_string());
            visited_position.push(position);

            match handler.handle(request) {
                Handling::Handled(result) => {
                    if first_handled.is_none() {
                        // 第一个接下的算数(与责任链同语义)。
                        ledger.record_handled(handler.name());
                        first_handled = Some((handler.name().to_string(), handler.kind(), result));
                    } else {
                        // ★ 后面这些**已经知道答案了却还在判**------这就是广播式的代价。
                        ledger.record_passed(handler.name());
                    }
                }
                Handling::Pass => {
                    ledger.record_passed(handler.name());
                }
            }
        }

        match first_handled {
            Some((name, kind, result)) => ChainResult::new(
                Some(result),
                name.as_str(),
                Some(kind),
                visited,
                visited_position,
                self.handlers.len(),
            ),
            None => {
                ledger.record_unhandled();
                ChainResult::new(
                    None,
                    UNHANDLED_MARK,
                    None,
                    visited,
                    visited_position,
                    self.handlers.len(),
                )
            }
        }
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : handler_kind.rs
//! 处理器种类:**接了就终止**还是**做完了继续**。
//!
//! ## ★ 这一层区别正是「责任链」与「流水线」的分界
//!
//! - [`HandlerKind::Responder`]:能接就接,**接了链就停**------这是责任链。
//! - [`HandlerKind::PassThrough`]:**永远放行**,但在放行前做一件必做的事
//!   (登记台账、通知客户、留痕)------这是流水线的一环。
//!
//! 两者常常**串在同一条链上**(本工程就是:先登记,再逐级审批),
//! 因此不能靠「是不是责任链」来二选一,而要靠这一个字段把两者分开:
//!
//! - 审计要能说「`PassThrough` 处理器从不产生结论」;
//! - 报表要能说「这一站只做登记,不做决定」。
//!
//! 若没有这个字段,这两句话都只能靠**看代码**才知道,无法被检查。

/// 处理器的种类。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum HandlerKind {
    /// ★ 应答者:能接就接,接了链终止。
    Responder,
    /// ★ 过路者:永远放行,只在路过时做一件事。
    PassThrough,
}

impl HandlerKind {
    /// 中文名。
    pub const fn chinese_text(&self) -> &'static str {
        match self {
            HandlerKind::Responder => "应答者(接了就终止)",
            HandlerKind::PassThrough => "过路者(做完继续)",
        }
    }

    /// ★ 短中文名(`应答者` / `过路者`)。
    ///
    /// ⚠️ 为什么需要两种文本:站点清单表里「种类」这一列只有 8 列的预算
    /// (九个列要挤进 140 列文档宽度),放不下 `应答者(接了就终止)`。
    /// 完整语义由「凭什么接」那一列与表尾注解承担。
    pub const fn short_text(&self) -> &'static str {
        match self {
            HandlerKind::Responder => "应答者",
            HandlerKind::PassThrough => "过路者",
        }
    }

    /// 机读编码。
    pub const fn machine_text(&self) -> &'static str {
        match self {
            HandlerKind::Responder => "RESPONDER",
            HandlerKind::PassThrough => "PASSTHROUGH",
        }
    }

    /// ★ 是否**允许**产生结论(即:接下请求)。
    ///
    /// `PassThrough` 返回 `false`------一条审计规则会检查这一点,
    /// 于是「某个过路处理器偷偷做了决定」会变成一个可计算的数字。
    pub const fn may_settle(&self) -> bool {
        matches!(self, HandlerKind::Responder)
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : handling.rs
//! 单次处理的结果:**接下了**还是**放行**。
//!
//! ## ★ 为什么只有两种而不是三种
//!
//! 常见的写法是 `{ 处理, 放行, 拒绝 }`。
//! 但「拒绝」与「处理」在**链的行为上完全一样**------都会终止传递。
//! 区别只在结论的内容里(`Disposition::Rejected` 还是 `StoreHandled`)。
//!
//! 若把「拒绝」做成第三种枚举值,就会出现一件荒唐事:
//! 一个处理器返回 `Reject`,而它的结论对象里却写着 `StoreHandled`------
//! 两个地方各说一次「这单怎么样了」,不一致时毫无迹象。
//!
//! 因此:**枚举只表达链的走向**(终止 / 放行),
//! **结论内容**统一由 `R` 携带,一处说话。

/// 一个处理器对一次请求的处理结果。
///
/// - [`Handling::Handled`]:**接下了**,链到此终止(`R` 即结论)。
/// - [`Handling::Pass`]:**不归我管**(或我做完了该做的部分),交给下一个。
#[derive(Clone, Debug)]
pub enum Handling<R> {
    /// 接下了(携带结论,链终止)。
    Handled(R),
    /// 放行(交给下一个处理器)。
    Pass,
}

impl<R> Handling<R> {
    /// 是否接下了。
    pub const fn is_handled(&self) -> bool {
        matches!(self, Handling::Handled(_))
    }

    /// 是否放行了。
    pub const fn is_pass(&self) -> bool {
        matches!(self, Handling::Pass)
    }

    /// 取出结论(**只有接下时才有**)。
    pub fn result(self) -> Option<R> {
        match self {
            Handling::Handled(result) => Some(result),
            Handling::Pass => None,
        }
    }

    /// 借用结论(不移动)。
    pub fn result_ref(&self) -> Option<&R> {
        match self {
            Handling::Handled(result) => Some(result),
            Handling::Pass => None,
        }
    }
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : closure_handler.rs
//! 闭包处理器:**一个处理器就是两条闭包 + 一个名字**。
//!
//! ## ★ 为什么处理器做成「数据」而不是「每种一个 struct」
//!
//! 若每一站都写一个 `struct XxxHandler`,那么:
//! - 链的装配代码里会出现七个类型名,「这条链长什么样」要连着看七行 `Box::new`;
//! - 新增一站要写一个新类型(扩展的代价被抬高了);
//! - 「这两个处理器判据相同、只是结论不同」这类重复看不出来。
//!
//! 做成闭包处理器之后,一站 = 一个 `(名字, 种类, 判据, 动作)` 四元组,
//! 于是「这条链长什么样」可以列成一张**表**,
//! 而「换序」就是调整这张表的顺序。
//!
//! ## ★ 为什么 `action` 内部必须**再判一次**
//!
//! 责任链保证「只在 `can_handle` 为真时调 `handle`」。
//! 但广播式([`super::super::chain::HandlerChain::submit_broadcast_first`])
//! 会**无条件**调 `handle`。
//! 若 `action` 自己不判,那么同一批处理器在两种投递方式下会给出不同结论,
//! 「省下的判定次数」这个对照就被污染了------
//! 读者会以为是短路改变了结果,其实是处理器不够结实。
//!
//! 因此本工程的每个 `action` 都是
//! `if !判据 { return Pass }`(自我保护),责任链下这行永远多余、广播式下它是救命的。

use crate::chain::{Handling, Handler, HandlerKind};

/// 一个由闭包构成的处理器。
///
/// # 类型参数
/// - `Q`:请求类型。
/// - `R`:结论类型。
pub struct ClosureHandler<Q, R> {
    /// 处理器名(**链内唯一**)。
    name: String,
    /// 处理器种类。
    kind: HandlerKind,
    /// 判据:这个请求归我管吗(**必须无副作用**)。
    predicate: Box<dyn Fn(&Q) -> bool>,
    /// 动作:处理它(返回 [`Handling::Pass`] 表示放行)。
    action: Box<dyn Fn(&Q) -> Handling<R>>,
    /// 一句话说明(写进报表,说明这一站凭什么接)。
    description: String,
}

impl<Q, R> ClosureHandler<Q, R> {
    /// 组装一个处理器。
    ///
    /// # 参数
    /// - `name`:处理器名(链内唯一)。
    /// - `kind`:种类(应答者 / 过路者)。
    /// - `predicate`:判据闭包(**无副作用**)。
    /// - `action`:动作闭包(**内部必须自己再判一次**,理由见模块文档)。
    /// - `description`:一句话说明(**传 `String`** 而不是 `&'static str`:
    ///   说明里往往要带上金额上限这类**算出来的**文本,写不进常量)。
    pub fn new(
        name: &str,
        kind: HandlerKind,
        predicate: Box<dyn Fn(&Q) -> bool>,
        action: Box<dyn Fn(&Q) -> Handling<R>>,
        description: String,
    ) -> Self {
        Self {
            name: name.to_string(),
            kind,
            predicate,
            action,
            description,
        }
    }

    /// 一句话说明(这一站凭什么接)。
    pub fn description(&self) -> &str {
        self.description.as_str()
    }
}

impl<Q, R> Handler<Q, R> for ClosureHandler<Q, R> {
    fn name(&self) -> &str {
        self.name.as_str()
    }

    fn kind(&self) -> HandlerKind {
        self.kind
    }

    fn can_handle(&self, request: &Q) -> bool {
        (self.predicate)(request)
    }

    fn handle(&self, request: &Q) -> Handling<R> {
        (self.action)(request)
    }
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : ticket_chain.rs
//! 链的装配:**把处理器按一个顺序串起来**。
//!
//! ## ★ 顺序是数据,不是代码
//!
//! 本模块的核心是一个 [`&[&str]`](站名序列)------
//! 链的装配函数按它去造处理器。
//! 于是「换序」就是换一个数组,**处理器本身一行不改**。
//!
//! 这带来两件别处做不到的事:
//! - 幕九的换序对照是**同一批处理器**的两条链,
//!   因此结论的差别**只**可能来自顺序;
//! - 「这条链长什么样」可以印成一张表(站名 + 种类 + 凭什么接),
//!   而不是散在七个 `Box::new` 里。
//!
//! ## ⚠️ 两条链不能共用同一台处理器实例
//!
//! 处理器持有 `Box<dyn Fn>`,不能复制。
//! 因此每次装配都**重新造一批**。
//! 这不是浪费:本工程的处理器都是**无状态**的(判据与动作只读取工单),
//! 若某天某个处理器带了可变状态,共用实例会让两条链互相污染------
//! 而那时「换序对照」的结论就不再是顺序造成的了。

use std::rc::Rc;

use crate::chain::{ChainLedger, ChainTailPolicy, Handler, HandlerChain};
use crate::domain::{ServiceTicket, TicketResolution};

use super::ticket_handler_factory::{
    build_handler_by_name, HANDLER_ADMISSION_NAME, HANDLER_ESCALATION_NAME,
    HANDLER_HEADQUARTER_NAME, HANDLER_LOGGING_NAME, HANDLER_REGIONAL_NAME, HANDLER_STORE_NAME,
    HANDLER_WARRANTY_NAME,
};

/// ★ 正常顺序:登记 → 校验 → **保修** → 门店 → 区域 → 总部 → 兜底。
///
/// 保修在金额之前:**在保的维修不看金额**,
/// 若先走金额,小额维修会被门店按「门店处置」接走,客户就得自付。
pub const HANDLER_ORDER_NORMAL: [&str; 7] = [
    HANDLER_LOGGING_NAME,
    HANDLER_ADMISSION_NAME,
    HANDLER_WARRANTY_NAME,
    HANDLER_STORE_NAME,
    HANDLER_REGIONAL_NAME,
    HANDLER_HEADQUARTER_NAME,
    HANDLER_ESCALATION_NAME,
];

/// ★ 换序顺序:把「保修判定」挪到**金额权限之后**。
///
/// 用于幕九的对照:同一批工单、同一批处理器,只换这一处,
/// 看有几张单的处置者变了------那个数就是「顺序作为业务知识」的量度。
pub const HANDLER_ORDER_SWAPPED: [&str; 7] = [
    HANDLER_LOGGING_NAME,
    HANDLER_ADMISSION_NAME,
    HANDLER_STORE_NAME,
    HANDLER_REGIONAL_NAME,
    HANDLER_HEADQUARTER_NAME,
    HANDLER_WARRANTY_NAME,
    HANDLER_ESCALATION_NAME,
];

/// ★ 缺兜底顺序(**负向用例**):摘掉第 7 站。
///
/// 用于幕八:确认 `SETTLED` 那条规则真的会红。
/// 摘掉之后,超出总部权限的单会消失在链尾。
pub const HANDLER_ORDER_NO_FALLBACK: [&str; 6] = [
    HANDLER_LOGGING_NAME,
    HANDLER_ADMISSION_NAME,
    HANDLER_WARRANTY_NAME,
    HANDLER_STORE_NAME,
    HANDLER_REGIONAL_NAME,
    HANDLER_HEADQUARTER_NAME,
];

/// ★ 按站名序列装配一条链。
///
/// # 参数
/// - `name`:链名。
/// - `policy`:链尾策略。
/// - `ledger`:共享台账(**只增不减**)。
/// - `order`:站名序列(顺序即业务知识)。
///
/// # Panics
/// 序列里出现不认识的站名时 panic------
/// 「写错站名」若静默跳过,链就少了一站,而报表上只会少几张单。
pub fn build_ticket_chain(
    name: &str,
    policy: ChainTailPolicy,
    ledger: Rc<ChainLedger>,
    order: &[&str],
) -> HandlerChain<ServiceTicket, TicketResolution> {
    let mut chain: HandlerChain<ServiceTicket, TicketResolution> =
        HandlerChain::new(name, policy, ledger);
    for station_name in order.iter() {
        chain = chain.with_handler(build_handler_by_name(station_name));
    }
    chain
}

/// ★ 装配一条**工程外扩展**的链:在 `order` 之后追加扩展站。
///
/// 这就是「完全可扩展」的落点:扩展既不改机制层、也不改内置工厂,
/// 只在这一层把新站挂到序列末尾。
///
/// # 参数
/// - `name`:链名。
/// - `policy`:链尾策略。
/// - `ledger`:共享台账。
/// - `order`:内置站名序列。
/// - `extra_handlers`:★ 扩展站(**由 `main.rs` 的工程外扩展区给出**)。
pub fn build_ticket_chain_with_extensions(
    name: &str,
    policy: ChainTailPolicy,
    ledger: Rc<ChainLedger>,
    order: &[&str],
    extra_handlers: Vec<Box<dyn Handler<ServiceTicket, TicketResolution>>>,
) -> HandlerChain<ServiceTicket, TicketResolution> {
    let mut chain: HandlerChain<ServiceTicket, TicketResolution> =
        HandlerChain::new(name, policy, ledger);
    for station_name in order.iter() {
        chain = chain.with_handler(build_handler_by_name(station_name));
    }
    for handler in extra_handlers.into_iter() {
        chain = chain.with_handler(handler);
    }
    chain
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : ticket_handler_factory.rs
//! 处置处理器的建造厂:**每一站凭什么接、接了给什么结论**。
//!
//! ## ★ 为什么「凭什么接」在这里而不是在机制层
//!
//! 机制层只知道「按序问、问到能接为止」;
//! 「有没有凭证」「在不在保修期」「金额超没超权限」全是**业务口径**。
//! 放在这一层,机制层才能零 `crate::` 依赖(可被测出来),
//! 换一家公司只需换这一张表。
//!
//! ## 链上各站(**顺序即业务知识**)
//!
//! | # | 站名 | 种类 | 凭什么接 |
//! |---|---|---|---|
//! | 1 | 工单登记留痕 | 过路者 | 恒接(**永远放行**) |
//! | 2 | 单据合规校验 | 应答者 | 工单**不可受理**(无凭证 / 超窗口) |
//! | 3 | 保修判定 | 应答者 | 走保修且**在保**且非人为损坏 |
//! | 4 | 门店店长审批 | 应答者 | 金额 ≤ 门店权限 |
//! | 5 | 区域经理审批 | 应答者 | 金额 ≤ 区域权限 |
//! | 6 | 总部总监审批 | 应答者 | 金额 ≤ 总部权限 |
//! | 7 | 总裁办兜底 | 应答者 | 恒接(**保证链尾不空**) |
//!
//! ⚠️ 第 7 站不是「可以省」的:摘掉它,
//! 超大额工单就会消失在链尾------本工程幕八**真的摘过一次**,
//! 为的是确认 `SETTLED` 那条规则会红。

use crate::chain::{Handling, Handler, HandlerKind};
use crate::domain::{
    AuthorityLevel, Disposition, Money, ServiceTicket, TicketResolution, CNY,
};

use super::closure_handler::ClosureHandler;

/// 工单登记留痕(**过路者**)。
pub const HANDLER_LOGGING_NAME: &str = "工单登记留痕";
/// 单据合规校验。
pub const HANDLER_ADMISSION_NAME: &str = "单据合规校验";
/// 保修判定。
pub const HANDLER_WARRANTY_NAME: &str = "保修判定";
/// 门店店长审批。
pub const HANDLER_STORE_NAME: &str = "门店店长审批";
/// 区域经理审批。
pub const HANDLER_REGIONAL_NAME: &str = "区域经理审批";
/// 总部总监审批。
pub const HANDLER_HEADQUARTER_NAME: &str = "总部总监审批";
/// 总裁办兜底。
pub const HANDLER_ESCALATION_NAME: &str = "总裁办兜底";

/// ★ 第 1 站:登记留痕(**恒放行**)。
///
/// 这一站存在的意义:证明「责任链上可以有不做决定的站」。
/// 它**永远**返回 [`Handling::Pass`],但每次投递都会经过它------
/// 于是台账上它的「被问次数 = 投递数、接下次数 = 0」,
/// 这正是 [`crate::chain::HandlerAttempt::never_asked`] 之外的另一种「纯过路」。
pub fn build_logging_handler() -> Box<ClosureHandler<ServiceTicket, TicketResolution>> {
    let predicate: Box<dyn Fn(&ServiceTicket) -> bool> = Box::new(|_ticket| true);
    let action: Box<dyn Fn(&ServiceTicket) -> Handling<TicketResolution>> = Box::new(|ticket| {
        // ⚠️ 过路者**永远**放行:它只做登记,不做决定。
        //    (审计规则 `NO_PASSTHROUGH` 会盯住这一点:过路者不许出现在处置者里。)
        let _ = ticket.ticket_number();
        Handling::Pass
    });
    Box::new(ClosureHandler::new(
        HANDLER_LOGGING_NAME,
        HandlerKind::PassThrough,
        predicate,
        action,
        "每一张工单都要留痕,但不做任何决定".to_string(),
    ))
}

/// ★ 第 2 站:单据合规校验。
///
/// 判据直接问工单自己([`ServiceTicket::is_inadmissible`]),
/// 不在这一站重写一遍------「能不能受理」只有一个地方说了算。
pub fn build_admission_handler() -> Box<ClosureHandler<ServiceTicket, TicketResolution>> {
    let predicate: Box<dyn Fn(&ServiceTicket) -> bool> =
        Box::new(|ticket| ticket.is_inadmissible());
    let action: Box<dyn Fn(&ServiceTicket) -> Handling<TicketResolution>> = Box::new(|ticket| {
        // ★ 自我保护:广播式会无条件调 `handle`,因此这里要再判一次。
        if !ticket.is_inadmissible() {
            return Handling::Pass;
        }
        let resolution: TicketResolution = TicketResolution::new(
            Disposition::Rejected,
            HANDLER_ADMISSION_NAME,
            Money::zero(CNY),
            ticket.inadmissible_reason_text().as_str(),
        );
        Handling::Handled(resolution)
    });
    Box::new(ClosureHandler::new(
        HANDLER_ADMISSION_NAME,
        HandlerKind::Responder,
        predicate,
        action,
        "无凭证或超窗口的工单在此拒单(不占上级审批资源)".to_string(),
    ))
}

/// ★ 第 3 站:保修判定。
///
/// ⚠️ 这一站**不看金额**:一张 30 元的维修在保就该免费修,
/// 若它要先走金额权限,那么「小额维修」会因为门店权限而变成「门店处置」,
/// 客户就得自付------而保修判定本该在金额之前。
/// 这正是「顺序是业务知识」最具体的一处证据(幕九换序会证明它)。
pub fn build_warranty_handler() -> Box<ClosureHandler<ServiceTicket, TicketResolution>> {
    let predicate: Box<dyn Fn(&ServiceTicket) -> bool> =
        Box::new(|ticket| ticket.is_within_warranty());
    let action: Box<dyn Fn(&ServiceTicket) -> Handling<TicketResolution>> = Box::new(|ticket| {
        if !ticket.is_within_warranty() {
            return Handling::Pass;
        }
        let note: String = format!(
            "购买 {} 个月,在 {} 个月保修期内且非人为损坏,免费维修(厂商承担)",
            ticket.months_elapsed(),
crate::domain::WARRANTY_MONTHS
        );
        let resolution: TicketResolution = TicketResolution::new(
            Disposition::WarrantyRepair,
            HANDLER_WARRANTY_NAME,
            Money::zero(CNY),
            note.as_str(),
        );
        Handling::Handled(resolution)
    });
    Box::new(ClosureHandler::new(
        HANDLER_WARRANTY_NAME,
        HandlerKind::Responder,
        predicate,
        action,
        "在保且非人为损坏的维修/复检,免费处理(不看金额)".to_string(),
    ))
}

/// ★ 第 4~6 站:按权限档位审批。
///
/// # 参数
/// - `level`:权限档位(上限金额 + 中文名 + 对应结论)。
///
/// # Panics
/// 档位编码不认识时 panic------
/// 「加了一级权限却忘了给它配结论」必须在装配时炸,
/// 不能等到报表上出现一个空白的处置结论。
pub fn build_authority_handler(
    level: AuthorityLevel,
) -> Box<ClosureHandler<ServiceTicket, TicketResolution>> {
    let disposition: Disposition = disposition_for_authority(level);
    let handler_name: String = level.chinese_name().to_string();
    let ceiling: i64 = level.ceiling_minor_units();
    let predicate: Box<dyn Fn(&ServiceTicket) -> bool> = Box::new(move |ticket| {
        level.covers(ticket.involved_amount().minor_units())
    });
    let action: Box<dyn Fn(&ServiceTicket) -> Handling<TicketResolution>> =
        Box::new(move |ticket| {
            let amount: i64 = ticket.involved_amount().minor_units();
            if !level.covers(amount) {
                return Handling::Pass;
            }
            let note: String = format!(
                "涉及 {} <= {} 权限上限 {},由{}处置",
                ticket.involved_amount().chinese_text(),
                handler_name,
                Money::new(ceiling, CNY).chinese_text(),
                handler_name
            );
            let resolution: TicketResolution =
                TicketResolution::new(disposition, handler_name.as_str(), ticket.involved_amount(), note.as_str());
            Handling::Handled(resolution)
        });
    Box::new(ClosureHandler::new(
        level.chinese_name(),
        HandlerKind::Responder,
        predicate,
        action,
        format!(
            "金额 ≤ {} 的单由{}直接处置",
            Money::new(ceiling, CNY).chinese_text(),
            level.chinese_name()
        ),
    ))
}

/// ★ 第 7 站:总裁办兜底(**恒接**)。
///
/// 存在的理由只有一句:**链尾不允许空**。
/// 摘掉它,`SETTLED` 那条规则会红------本工程真的摘过一次。
pub fn build_escalation_handler() -> Box<ClosureHandler<ServiceTicket, TicketResolution>> {
    let predicate: Box<dyn Fn(&ServiceTicket) -> bool> = Box::new(|_ticket| true);
    let action: Box<dyn Fn(&ServiceTicket) -> Handling<TicketResolution>> = Box::new(|ticket| {
        let note: String = format!(
            "涉及 {} 超出全部审批权限,上报总裁办(按涉及金额计入待赔付)",
            ticket.involved_amount().chinese_text()
        );
        let resolution: TicketResolution = TicketResolution::new(
            Disposition::Escalated,
            HANDLER_ESCALATION_NAME,
            ticket.involved_amount(),
            note.as_str(),
        );
        Handling::Handled(resolution)
    });
    Box::new(ClosureHandler::new(
        HANDLER_ESCALATION_NAME,
        HandlerKind::Responder,
        predicate,
        action,
        "超出所有权限的单在此收口,保证链尾不空".to_string(),
    ))
}

/// ★ 权限档位 → 处置结论的映射。
///
/// ⚠️ 为什么不用「链上位置」来推(第 4 站 = 门店、第 5 站 = 区域):
/// 那样换序就会换掉结论的语义------把总部挪到第 4 站,
/// 它给出的结论会变成「门店处置」,而报表上看不出这是错的。
/// 按**档位编码**映射,结论才跟着权限走而不是跟着位置走。
fn disposition_for_authority(level: AuthorityLevel) -> Disposition {
    match level.code() {
        "STORE" => Disposition::StoreHandled,
        "REGIONAL" => Disposition::RegionalHandled,
        "HEADQUARTER" => Disposition::HeadquarterHandled,
        "SUPERVISOR" => Disposition::Escalated,
        other => panic!("权限档位 {} 没有配置对应的处置结论(加档位时要在这里补)", other),
    }
}

/// 门店权限站的建造函数(**把档位烧进函数指针**,便于写进规格表)。
pub fn build_store_authority_handler() -> Box<ClosureHandler<ServiceTicket, TicketResolution>> {
    build_authority_handler(crate::domain::AUTHORITY_STORE)
}

/// 区域权限站的建造函数。
pub fn build_regional_authority_handler() -> Box<ClosureHandler<ServiceTicket, TicketResolution>> {
    build_authority_handler(crate::domain::AUTHORITY_REGIONAL)
}

/// 总部权限站的建造函数。
pub fn build_headquarter_authority_handler() -> Box<ClosureHandler<ServiceTicket, TicketResolution>> {
    build_authority_handler(crate::domain::AUTHORITY_HEADQUARTER)
}

/// ★ 内置处理器的**规格表**(**按链上顺序**)。
///
/// 返回 `(站名, 种类, 建造函数)` 三元组。
///
/// ## ★ 为什么规格表里**没有**「凭什么接」
///
/// 「这一站凭什么接」这句话要出现在两个地方:装配链时、印站点清单时。
/// 若两处各写一遍,改一处漏一处就会分裂------
/// 而那种分裂在报表上**看起来完全正常**(两处都是通顺的句子)。
///
/// 因此说明**只写在每台处理器自己的 `description` 里**(`build_*` 函数内),
/// 报表要印时现造一台实例读出来(见 [`station_descriptions`])。
/// 规格表只管「有哪些站、什么种类、怎么造」。
///
/// ⚠️ 建造函数的返回类型是**具体类型** `Box<ClosureHandler<..>>` 而不是
/// `Box<dyn Handler<..>>`:报表要读 `description`,而 trait 对象读不到它
/// (`description` 是 `ClosureHandler` 的固有方法,不在 `Handler` trait 上------
/// 机制层不该知道「说明」这回事)。
pub fn builtin_handler_specs() -> Vec<(
    &'static str,
    HandlerKind,
    fn() -> Box<ClosureHandler<ServiceTicket, TicketResolution>>,
)> {
    vec![
        (HANDLER_LOGGING_NAME, HandlerKind::PassThrough, build_logging_handler),
        (HANDLER_ADMISSION_NAME, HandlerKind::Responder, build_admission_handler),
        (HANDLER_WARRANTY_NAME, HandlerKind::Responder, build_warranty_handler),
        (HANDLER_STORE_NAME, HandlerKind::Responder, build_store_authority_handler),
        (HANDLER_REGIONAL_NAME, HandlerKind::Responder, build_regional_authority_handler),
        (
            HANDLER_HEADQUARTER_NAME,
            HandlerKind::Responder,
            build_headquarter_authority_handler,
        ),
        (HANDLER_ESCALATION_NAME, HandlerKind::Responder, build_escalation_handler),
    ]
}

/// ★ 站名 → 「凭什么接」的清单(**现造一台实例读它的 `description`**)。
///
/// ⚠️ 为了印一张表而造一批处理器看起来浪费,但这是**唯一**能让
/// 「说明」只存在一处的办法。这些处理器**没有状态**(闭包只读工单),
/// 造出来读一句说明就丢掉,成本可忽略。
pub fn station_descriptions() -> Vec<(String, String)> {
    builtin_handler_specs()
        .into_iter()
        .map(|(name, _kind, builder)| (name.to_string(), builder().description().to_string()))
        .collect()
}

/// ★ 按站名造一台处理器(**从规格表派生**)。
///
/// # Panics
/// 站名不认识时 panic------
/// 「写错站名」若静默跳过,链就少了一站,而报表上只会少几张单。
pub fn build_handler_by_name(
    station_name: &str,
) -> Box<dyn Handler<ServiceTicket, TicketResolution>> {
    for (name, _kind, builder) in builtin_handler_specs().into_iter() {
        if name == station_name {
            // 具体类型 → trait 对象(Rust 的 unsized coercion)。
            return builder();
        }
    }
    panic!(
        "不认识的处置站名:{}(加了新站要在 `builtin_handler_specs` 里登记)",
        station_name
    );
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : ticket_session.rs
//! 会话:把一批工单**投进一条链**,并留下逐单可查的结果。
//!
//! ## ★ 为什么「工单」与「结论」分开存
//!
//! 工单是**进来时的样子**,结论是**出去时的样子**。
//! 分开存,同一批工单才能跑两条不同的链并逐单比对------
//! 本工程幕九(换序对照)与幕十(广播式对照)都靠这一点成立。
//!
//! ## ★ 为什么会话自己不记账
//!
//! 计数全在共享台账 [`crate::chain::ChainLedger`] 里(只增不减)。
//! 会话只保存**逐单结果**。
//! 若会话再记一份,就会出现「两个数字都能回答『处理了几张』」的情况,
//! 而它们不一致时读者不知道该信哪个。

use crate::chain::{ChainResult, HandlerChain, RequestObservation, UNHANDLED_MARK};
use crate::domain::{Disposition, Money, ServiceTicket, TicketResolution, CNY};

/// 一张工单的投递结果。
///
/// # 字段说明
/// - `ticket`:工单本体(**进来时的样子**)。
/// - `result`:链返回的结果(结论 + 访问轨迹)。
#[derive(Clone, Debug)]
pub struct TicketOutcome {
    /// 工单本体。
    ticket: ServiceTicket,
    /// 链返回的结果。
    result: ChainResult<TicketResolution>,
}

impl TicketOutcome {
    /// 由工单与结果组装。
    pub fn new(ticket: ServiceTicket, result: ChainResult<TicketResolution>) -> Self {
        Self { ticket, result }
    }

    /// 工单本体。
    pub fn ticket(&self) -> &ServiceTicket {
        &self.ticket
    }

    /// 链返回的结果。
    pub fn result(&self) -> &ChainResult<TicketResolution> {
        &self.result
    }

    /// 工单号。
    pub fn ticket_number(&self) -> &str {
        self.ticket.ticket_number()
    }

    /// ★ 处置结论(**无人处置时给出一个显式的 `Unhandled` 结论**)。
    ///
    /// ⚠️ 为什么不返回 `Option`:报表要的是「每格都有字」。
    /// 若这里返回 `None`,表示层就得自己写一个「---」,
    /// 那时「无人处置」的写法有两处来源,改一处漏一处就分裂了。
    pub fn resolution(&self) -> TicketResolution {
        match self.result.result_ref() {
            Some(resolution) => resolution.clone(),
            None => TicketResolution::new(
                Disposition::Unhandled,
                UNHANDLED_MARK,
                Money::zero(CNY),
                "链上没有任何一站接下这张单(**这是装配缺陷,不是业务结论**)",
            ),
        }
    }

    /// 结论类型。
    pub fn disposition(&self) -> Disposition {
        self.resolution().disposition()
    }

    /// 处置者名。
    pub fn handler_name(&self) -> String {
        self.result.handler_name().to_string()
    }

    /// 赔付金额。
    pub fn compensation(&self) -> Money {
        self.resolution().compensation()
    }

    /// 走了几站。
    pub fn visited_total(&self) -> usize {
        self.result.visited_total()
    }

    /// ★ 因为短路而**一次都没被调用**的站数。
    pub fn skipped_total(&self) -> usize {
        self.result.skipped_total()
    }

    /// 访问轨迹文本。
    pub fn visited_text(&self) -> String {
        self.result.visited_text()
    }
}

/// 一次会话:一批工单 × 一条链。
pub struct TicketSession {
    /// 链。
    chain: HandlerChain<ServiceTicket, TicketResolution>,
    /// 逐单结果(**按投递顺序**)。
    outcomes: Vec<TicketOutcome>,
}

impl TicketSession {
    /// ★ 跑一遍。
    ///
    /// # 参数
    /// - `chain`:链(**跑完之后链就不能再改了**,因此按值传入)。
    /// - `tickets`:工单序列(按投递顺序)。
    pub fn run(
        chain: HandlerChain<ServiceTicket, TicketResolution>,
        tickets: &[ServiceTicket],
    ) -> Self {
        let mut outcomes: Vec<TicketOutcome> = Vec::new();
        for ticket in tickets.iter() {
            let result: ChainResult<TicketResolution> = chain.submit(ticket);
            outcomes.push(TicketOutcome::new(ticket.clone(), result));
        }
        Self { chain, outcomes }
    }

    /// ★ 跑一遍**广播式**对照(每个处理器都问一遍,仍是第一个接的算数)。
    ///
    /// # 参数
    /// - `chain`:链(同一批处理器)。
    /// - `tickets`:同一批工单。
    /// - `broadcast_ledger`:★ **独立的**台账。
    ///
    ///   ⚠️ 为什么必须另给一本:广播式的访问次数必然是 `单数 × 站数`,
    ///   若记进主台账,「省下多少次判定」这个数就被自己的对照实验污染了。
    ///   (既有工程的纪律:**每个用途各建一张图 / 一本账**。)
    pub fn run_broadcast(
        chain: HandlerChain<ServiceTicket, TicketResolution>,
        tickets: &[ServiceTicket],
        broadcast_ledger: std::rc::Rc<crate::chain::ChainLedger>,
    ) -> Self {
        let mut outcomes: Vec<TicketOutcome> = Vec::new();
        for ticket in tickets.iter() {
            let result: ChainResult<TicketResolution> =
                chain.submit_broadcast_first(ticket, &broadcast_ledger);
            outcomes.push(TicketOutcome::new(ticket.clone(), result));
        }
        Self { chain, outcomes }
    }

    /// 链。
    pub fn chain(&self) -> &HandlerChain<ServiceTicket, TicketResolution> {
        &self.chain
    }

    /// 逐单结果。
    pub fn outcomes(&self) -> &[TicketOutcome] {
        self.outcomes.as_slice()
    }

    /// 工单总数。
    pub fn ticket_total(&self) -> usize {
        self.outcomes.len()
    }

    /// ★ 被处置的单数。
    pub fn settled_total(&self) -> usize {
        self.outcomes.iter().filter(|o| o.result.is_handled()).count()
    }

    /// ★ 无人处置的单数(**正常链上必须是 0**)。
    pub fn unhandled_total(&self) -> usize {
        self.outcomes.iter().filter(|o| !o.result.is_handled()).count()
    }

    /// ★ 各结论类型的单数(**按 [`Disposition::all`] 的顺序**,含 0 的也列出)。
    ///
    /// ⚠️ 为什么把 0 也列出来:「拒单 0 张」与「根本没有拒单这一档」
    /// 在报表上是两件不同的事,只列非 0 的会让读者以为后者。
    pub fn disposition_counts(&self) -> Vec<(Disposition, usize)> {
        Disposition::all()
            .iter()
            .map(|disposition| {
                let count: usize = self
                    .outcomes
                    .iter()
                    .filter(|o| o.disposition() == *disposition)
                    .count();
                (*disposition, count)
            })
            .collect()
    }

    /// ★ 各站的接下次数(**按链上顺序**,含 0 的也列出)。
    pub fn handler_settle_counts(&self) -> Vec<(String, usize)> {
        self.chain
            .handler_names()
            .into_iter()
            .map(|name| {
                let count: usize = self
                    .outcomes
                    .iter()
                    .filter(|o| o.handler_name() == name)
                    .count();
                (name, count)
            })
            .collect()
    }

    /// ★ 赔付总额。
    pub fn compensation_total(&self) -> Money {
        let mut total: Money = Money::zero(CNY);
        for outcome in self.outcomes.iter() {
            total = total.plus(outcome.compensation());
        }
        total
    }

    /// ★ 累计走了几站(判定次数)。
    pub fn visited_total(&self) -> usize {
        self.outcomes.iter().map(|o| o.visited_total()).sum()
    }

    /// ★ 累计跳过几站(**这就是责任链省下的判定次数**)。
    pub fn skipped_total(&self) -> usize {
        self.outcomes.iter().map(|o| o.skipped_total()).sum()
    }

    /// ★ 若不用责任链(每个都问一遍)会是多少次判定。
    pub fn visited_total_if_broadcast(&self) -> usize {
        self.ticket_total() * self.chain.handler_total()
    }

    /// ★ 逐单观测(喂给契约审计)。
    pub fn observations(&self) -> Vec<RequestObservation> {
        self.outcomes
            .iter()
            .map(|outcome| RequestObservation::from_result(outcome.ticket_number(), outcome.result()))
            .collect()
    }

    /// ★ 逐单「工单号 → 结论指纹」(喂给恒等对照)。
    ///
    /// ⚠️ 只比结论指纹(结论类型 + 处置者 + 赔付),**不比说明文案**------
    /// 措辞改了不该让恒等检查失败。
    pub fn resolution_fingerprints(&self) -> Vec<(String, String)> {
        self.outcomes
            .iter()
            .map(|outcome| {
                (
                    outcome.ticket_number().to_string(),
                    outcome.resolution().fingerprint_text(),
                )
            })
            .collect()
    }
}
rust 复制代码
//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : audit_check_view.rs
//! 契约审计结果的**分析层视图**。
//!
//! ## ★ 为什么表示层不直接用 `crate::chain::RuleCheck`
//!
//! 机制层的 `RuleCheck` 带着 `ChainAuditRule`(一个机制层的枚举)。
//! 表示层一旦 import 它,`app -> chain` 这条边就出现了------
//! 依赖实测会当场抓到,而「这一列叫什么」也就押在了机制层的一个字段名上。
//!
//! 正确做法不是放宽红线,而是**补一层视图**:
//! 这里把规则转成**纯字符串**,表示层只认识字符串。
//! (既有工程的教训:★ 表示层 import 机制层时,正确修法是补视图而不是放宽红线。)

use crate::chain::RuleCheck;

/// 一条契约检查的视图(**全是字符串**,表示层不认识机制层类型)。
///
/// # 字段说明
/// - `code` / `name`:规则编码与中文名。
/// - `expectation`:期望文本。
/// - `actual`:实测文本。
/// - `passed`:是否通过。
/// - `violating`:违规的请求标识(用「、」连接后的文本)。
/// - `known_failing_input`:★ 已知会失败的输入(**没有它,规则就是一句注释**)。
#[derive(Clone, Debug)]
pub struct AuditCheckView {
    /// 规则编码。
    code: String,
    /// 规则中文名。
    name: String,
    /// 期望文本。
    expectation: String,
    /// 实测文本。
    actual: String,
    /// 是否通过。
    passed: bool,
    /// 违规请求标识(连接后的文本)。
    violating: String,
    /// 已知会失败的输入。
    known_failing_input: String,
}

impl AuditCheckView {
    /// 由机制层的一条检查结果转出。
    pub fn from_check(check: &RuleCheck) -> Self {
        Self {
            code: check.rule().code().to_string(),
            name: check.rule().chinese_text().to_string(),
            expectation: check.rule().expectation_text().to_string(),
            actual: check.actual_text().to_string(),
            passed: check.passed(),
            violating: if check.violating_labels().is_empty() {
                "---".to_string()
            } else {
                check.violating_labels().join("、")
            },
            known_failing_input: check.rule().known_failing_input_text().to_string(),
        }
    }

    /// 规则编码。
    pub fn code(&self) -> &str {
        self.code.as_str()
    }

    /// 规则中文名。
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// 期望文本。
    pub fn expectation(&self) -> &str {
        self.expectation.as_str()
    }

    /// 实测文本。
    pub fn actual(&self) -> &str {
        self.actual.as_str()
    }

    /// 是否通过。
    pub const fn passed(&self) -> bool {
        self.passed
    }

    /// 违规请求标识文本。
    pub fn violating(&self) -> &str {
        self.violating.as_str()
    }

    /// 已知会失败的输入。
    pub fn known_failing_input(&self) -> &str {
        self.known_failing_input.as_str()
    }
}

/// 把一批检查结果转成视图。
pub fn audit_check_views(checks: &[RuleCheck]) -> Vec<AuditCheckView> {
    checks.iter().map(AuditCheckView::from_check).collect()
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : broadcast_census.rs
//! 广播式对照:**责任链 vs 每个环节都问一遍**。
//!
//! ## ★ 「省下了什么」必须有一个对照才成立
//!
//! 「我们用了责任链所以很快」是一句**无法被反驳**的话------
//! 因为它没有对照。
//! 可数的对照是:
//!
//! - 责任链:第 k 站接下之后,**后面 `站数 − k − 1` 站一次都没被调用**;
//! - 广播式:每一站都问一遍(判定次数 = `单数 × 站数`)。
//!
//! 两者跑的是**同一批处理器**、给出**完全相同的结论**
//! ([`crate::chain::HandlerChain::submit_broadcast_first`] 刻意做成「第一个接的算数」),
//! 因此唯一的差别就是判定次数------
//! 于是「省下 N 次判定」是一个干净的、可复算的整数。
//!
//! ⚠️ 刻意**不用耗时**做对照:CPU 时间不可复现,
//! 两次运行不算同一个数就谈不上「逐字节一致」的验收。

use crate::analysis::CheckLine;
use crate::client::TicketSession;

/// 一张单在两种投递方式下的对照。
///
/// # 字段说明
/// - `ticket_number`:工单号。
/// - `chain_handler` / `broadcast_handler`:两种方式的处置者(**必须相同**)。
/// - `chain_disposition` / `broadcast_disposition`:两种方式的结论(**必须相同**)。
/// - `chain_visited` / `broadcast_visited`:★ 判定次数(**这个才应该不同**)。
#[derive(Clone, Debug)]
pub struct BroadcastPair {
    /// 工单号。
    pub ticket_number: String,
    /// 责任链的处置者。
    pub chain_handler: String,
    /// 广播式的处置者。
    pub broadcast_handler: String,
    /// 责任链的结论。
    pub chain_disposition: String,
    /// 广播式的结论。
    pub broadcast_disposition: String,
    /// 责任链走了几站。
    pub chain_visited: usize,
    /// 广播式走了几站。
    pub broadcast_visited: usize,
    /// 结论是否一致。
    pub identical: bool,
}

/// ★ 逐单比对两种投递方式。
pub fn compare_broadcast(chain: &TicketSession, broadcast: &TicketSession) -> Vec<BroadcastPair> {
    assert_eq!(
        chain.ticket_total(),
        broadcast.ticket_total(),
        "两种投递方式的工单数不同,无法逐单比对"
    );
    chain
        .outcomes()
        .iter()
        .zip(broadcast.outcomes().iter())
        .map(|(left, right)| {
            let identical: bool =
                left.resolution().fingerprint_text() == right.resolution().fingerprint_text();
            BroadcastPair {
                ticket_number: left.ticket_number().to_string(),
                chain_handler: left.handler_name(),
                broadcast_handler: right.handler_name(),
                chain_disposition: left.resolution().disposition_text().to_string(),
                broadcast_disposition: right.resolution().disposition_text().to_string(),
                chain_visited: left.visited_total(),
                broadcast_visited: right.visited_total(),
                identical,
            }
        })
        .collect()
}

/// ★ 广播式对照的检查。
///
/// # 参数
/// - `pairs`:逐单对照结果。
/// - `chain_visit_total`:责任链的实际判定次数。
/// - `broadcast_visit_total`:广播式的判定次数。
/// - `station_total`:站数。
pub fn broadcast_checks(
    pairs: &[BroadcastPair],
    chain_visit_total: usize,
    broadcast_visit_total: usize,
    station_total: usize,
) -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();

    // ① ★ 逐单结论完全一致(语义相同,只是判定次数不同)。
    //    已知会失败的输入:广播式改成「最后一个接的算数」------那时结论会全变。
    let identical_total: usize = pairs.iter().filter(|pair| pair.identical).count();
    lines.push(CheckLine::new(
        "★ 两种投递方式逐单结论一致(语义相同)",
        format!(
            "{} 张全部一致(已知会失败:广播式改成「最后一个接的算数」)",
            pairs.len()
        )
        .as_str(),
        format!("{}/{}", identical_total, pairs.len()).as_str(),
        !pairs.is_empty() && identical_total == pairs.len(),
    ));

    // ② 广播式的判定次数 == 单数 × 站数(它是「每个都问一遍」的定义)。
    let expected_broadcast: usize = pairs.len() * station_total;
    lines.push(CheckLine::new(
        "广播式判定次数 == 单数 × 站数",
        format!("{} 次", expected_broadcast).as_str(),
        format!("{} 次", broadcast_visit_total).as_str(),
        broadcast_visit_total == expected_broadcast,
    ));

    // ③ ★ 责任链的判定次数**严格少于**广播式(真的省了)。
    //    已知会失败的输入:把所有站的判据都写成「恒不接」,
    //    于是每张单都要走完链尾,判定次数与广播式相同。
    lines.push(CheckLine::new(
        "★ 责任链判定次数 < 广播式(真的短路了)",
        "严格小于(已知会失败:所有站判据都写成恒不接,每张单都走完链尾)",
        format!("{} < {}", chain_visit_total, broadcast_visit_total).as_str(),
        chain_visit_total < broadcast_visit_total,
    ));

    // ④ ★ 两张表的**结论文本**逐单相同(不只是指纹相同)。
    //    已知会失败的输入:广播式改成「最后一个接的算数」------
    //    那时结论文本会全变,而指纹比对也会变红;两条互为印证。
    let disposition_identical_total: usize = pairs
        .iter()
        .filter(|pair| pair.chain_disposition == pair.broadcast_disposition)
        .count();
    lines.push(CheckLine::new(
        "★ 结论文本逐单相同(不只指纹相同)",
        format!("{} 张全部相同", pairs.len()).as_str(),
        format!("{}/{}", disposition_identical_total, pairs.len()).as_str(),
        !pairs.is_empty() && disposition_identical_total == pairs.len(),
    ));

    // ⑤ 每一张单:责任链走的站数 <= 广播式走的站数。
    let monotone_total: usize = pairs
        .iter()
        .filter(|pair| pair.chain_visited <= pair.broadcast_visited)
        .count();
    lines.push(CheckLine::new(
        "每张单:责任链走的站数 <= 广播式",
        format!("{} 张全部成立", pairs.len()).as_str(),
        format!("{}/{}", monotone_total, pairs.len()).as_str(),
        monotone_total == pairs.len(),
    ));

    lines
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : check_line.rs
//! 检查项与检查报告:全工程「应当 == 实测」的**统一形状**。
//!
//! ## 为什么单独一个文件
//!
//! 工程里有六处独立的检查(求值纪律、读取时刻、契约审计、恒等、拓扑、价值量化),
//! 若各写一套「期望/实测/结论」,最后一张体检表就得认识六种类型。
//! 统一成 [`CheckLine`] 之后,体检表只有一个来源,
//! 而**新增一处检查 = 多几行 `push`**,不需要改体检表。
//!
//! ## ★ 为什么 `verdict_text()` 只在这里产生一次
//!
//! 「通过 / 未通过」的写法若散在各处,迟早会出现一处写成「符合/不符合」,
//! 而读者会以为那是另一种判断。判据只有一个:`passed`。

/// 一条检查项。
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CheckLine {
    /// 检查项名称。
    item: String,
    /// 期望文本(**应当**是什么)。
    expectation_text: String,
    /// 实测文本(**实际**是什么)。
    actual_text: String,
    /// 是否通过。
    passed: bool,
}

impl CheckLine {
    /// 由四个字段构造。
    pub fn new(item: &str, expectation_text: &str, actual_text: &str, passed: bool) -> Self {
        Self {
            item: item.to_string(),
            expectation_text: expectation_text.to_string(),
            actual_text: actual_text.to_string(),
            passed,
        }
    }

    /// 检查项名称。
    pub fn item(&self) -> &str {
        &self.item
    }

    /// 期望文本。
    pub fn expectation_text(&self) -> &str {
        &self.expectation_text
    }

    /// 实测文本。
    pub fn actual_text(&self) -> &str {
        &self.actual_text
    }

    /// 是否通过。
    pub const fn passed(&self) -> bool {
        self.passed
    }

    /// ★ 结论文本:全工程唯一一处产生「通过 / 未通过」的地方。
    pub const fn verdict_text(&self) -> &'static str {
        if self.passed {
            "通过"
        } else {
            "未通过"
        }
    }
}

/// 一组检查项。
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CheckReport {
    /// 检查项清单。
    lines: Vec<CheckLine>,
}

impl CheckReport {
    /// 空报告。
    pub fn new() -> Self {
        Self { lines: Vec::new() }
    }

    /// 追加一条。
    pub fn push(&mut self, line: CheckLine) {
        self.lines.push(line);
    }

    /// 全部检查项。
    pub fn lines(&self) -> &[CheckLine] {
        &self.lines
    }

    /// 条数。
    pub fn line_total(&self) -> usize {
        self.lines.len()
    }

    /// 通过条数。
    pub fn passed_total(&self) -> usize {
        self.lines.iter().filter(|line| line.passed()).count()
    }

    /// 未通过条数。
    pub fn failed_total(&self) -> usize {
        self.line_total() - self.passed_total()
    }

    /// 是否全部通过。
    pub fn all_passed(&self) -> bool {
        self.passed_total() == self.line_total()
    }

    /// 未通过的检查项名称(拼成一串,供注解使用)。
    ///
    /// ⚠️ 全部通过时返回「无」而不是空串:空串在报表上看起来像「忘了填」。
    pub fn failed_items_text(&self) -> String {
        let failed: Vec<&str> = self
            .lines
            .iter()
            .filter(|line| !line.passed())
            .map(|line| line.item())
            .collect();
        if failed.is_empty() {
            "无".to_string()
        } else {
            failed.join("、")
        }
    }
}

impl Default for CheckReport {
    fn default() -> Self {
        Self::new()
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : ledger_snapshot_view.rs
//! 链台账快照的**分析层视图**。
//!
//! ★ 理由同 [`super::audit_check_view`]:表示层不该认识 `crate::chain::LedgerSnapshot`。
//! 这里把它摊成一组**纯数字**,表示层只做排版。
//!
//! ## ★ 为什么「省下的判定次数」要在这一层算而不是在机制层
//!
//! 机制层知道访问次数与跳过次数;但「若不用责任链会是多少次」这个**对照分母**
//! (`单数 × 站数`)牵涉到「什么算一次判定」的口径。
//! 口径属于分析,不属于机制------机制层只要保证计数不漏记就够了。

use crate::chain::LedgerSnapshot;

/// 一个处理器累计尝试情况的视图。
#[derive(Clone, Debug)]
pub struct StationAttemptView {
    /// 站名。
    name: String,
    /// 被问了几次。
    asked_total: usize,
    /// 接下了几次。
    handled_total: usize,
    /// 放行了几次。
    passed_total: usize,
}

impl StationAttemptView {
    /// 站名。
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// 被问了几次。
    pub const fn asked_total(&self) -> usize {
        self.asked_total
    }

    /// 接下了几次。
    pub const fn handled_total(&self) -> usize {
        self.handled_total
    }

    /// 放行了几次。
    pub const fn passed_total(&self) -> usize {
        self.passed_total
    }

    /// ★ 是否**死站**(一次都没被问过)。
    pub const fn is_never_asked(&self) -> bool {
        self.asked_total == 0
    }

    /// ★ 是否**纯过路**(被问过,但一次都没接下)。
    pub const fn is_pure_passthrough(&self) -> bool {
        self.asked_total > 0 && self.handled_total == 0
    }
}

/// 台账快照的视图。
#[derive(Clone, Debug)]
pub struct LedgerSnapshotView {
    /// 投递数。
    pub submitted_total: usize,
    /// 被接下数。
    pub handled_total: usize,
    /// 无人接数。
    pub unhandled_total: usize,
    /// 访问次数(实际判定次数)。
    pub visit_total: usize,
    /// ★ 跳过次数(因短路而没被调用的站数之和)。
    pub skipped_total: usize,
    /// 站数。
    pub station_total: usize,
    /// 各站尝试情况(**按链上首次被问的顺序**)。
    pub attempts: Vec<StationAttemptView>,
    /// ★ 处置顺序(谁接了第几单的**处理器名**序列)。
    pub settle_order: Vec<String>,
    /// 恒等式一的差。
    pub submission_gap: i64,
    /// 恒等式二的差。
    pub settlement_gap: i64,
    /// 恒等式三的差。
    pub visit_gap: i64,
}

impl LedgerSnapshotView {
    /// 由机制层快照转出。
    pub fn from_snapshot(snapshot: &LedgerSnapshot) -> Self {
        Self {
            submitted_total: snapshot.submitted_total,
            handled_total: snapshot.handled_total,
            unhandled_total: snapshot.unhandled_total,
            visit_total: snapshot.visit_total,
            skipped_total: snapshot.skipped_total,
            station_total: snapshot.attempts.len(),
            settle_order: snapshot.settle_order.clone(),
            attempts: snapshot
                .attempts
                .iter()
                .map(|attempt| StationAttemptView {
                    name: attempt.name().to_string(),
                    asked_total: attempt.asked_total(),
                    handled_total: attempt.handled_total(),
                    passed_total: attempt.passed_total(),
                })
                .collect(),
            submission_gap: snapshot.submission_identity_gap(),
            settlement_gap: snapshot.settlement_identity_gap(),
            visit_gap: snapshot.visit_identity_gap(),
        }
    }

    /// ★ 若不用责任链(每个都问一遍)会是多少次判定。
    pub fn visit_total_if_broadcast(&self) -> usize {
        self.submitted_total * self.station_total
    }

    /// ★ 省下的判定次数。
    pub fn saved_judgement_total(&self) -> i64 {
        self.visit_total_if_broadcast() as i64 - self.visit_total as i64
    }

    /// ★ 死站清单。
    pub fn never_asked_stations(&self) -> Vec<&StationAttemptView> {
        self.attempts.iter().filter(|a| a.is_never_asked()).collect()
    }

    /// ★ 纯过路清单。
    pub fn pure_passthrough_stations(&self) -> Vec<&StationAttemptView> {
        self.attempts.iter().filter(|a| a.is_pure_passthrough()).collect()
    }

    /// ★ 最近一次处置者(**从台账的处置顺序里取**,不是从某张表抄)。
    pub fn last_settled_handler(&self) -> String {
        match self.settle_order.last() {
            Some(name) => name.clone(),
            None => "(还没有任何一站接下单)".to_string(),
        }
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : order_census.rs
//! 换序对照:**同一批处理器、两条不同的链**。
//!
//! ## ★ 这一幕是责任链最容易被忽略的一条真理
//!
//! > 链的顺序不是排版,是业务规则。
//!
//! 换序**照样能编译、照样能跑**,只是结论悄悄变了。
//! 这类错误不会报错、不会 pan​ic,只会在月底对账时变成
//! 「为什么这批在保维修让客户自付了」。
//!
//! 因此本模块做一件事:**把「换序改变了几张单的结论」变成一个数字**。
//! 这个数字为 0 不代表没问题(可能是数据没覆盖到),
//! 但它**不为 0** 就证明顺序真的在起作用------
//! 而这一点若只靠看代码,是永远看不出来的。

use crate::analysis::CheckLine;
use crate::client::TicketSession;

/// 一张单在两条链上的对照。
///
/// # 字段说明
/// - `ticket_number`:工单号。
/// - `ticket_type_name`:工单类型(**归因用**)。
/// - `normal_handler` / `swapped_handler`:两条链的处置者。
/// - `normal_disposition` / `swapped_disposition`:两条链的结论。
/// - `changed`:★ 结论是否变了。
#[derive(Clone, Debug)]
pub struct OrderPair {
    /// 工单号。
    pub ticket_number: String,
    /// 工单类型名。
    pub ticket_type_name: String,
    /// 正常序的处置者。
    pub normal_handler: String,
    /// 换序后的处置者。
    pub swapped_handler: String,
    /// 正常序的结论。
    pub normal_disposition: String,
    /// 换序后的结论。
    pub swapped_disposition: String,
    /// 结论是否变了。
    pub changed: bool,
}

/// ★ 逐单比对两条链。
///
/// # 参数
/// - `normal`:正常序会话。
/// - `swapped`:换序会话(**同一批工单、同一批处理器**)。
///
/// # Panics
/// 两条链的工单数不同时 panic------
/// 单子不同就没法逐单比对,而静默截断比对会让「变了 0 张」看起来像「顺序无影响」。
pub fn compare_orders(normal: &TicketSession, swapped: &TicketSession) -> Vec<OrderPair> {
    assert_eq!(
        normal.ticket_total(),
        swapped.ticket_total(),
        "两条链跑的工单数不同({} vs {}),无法逐单比对",
        normal.ticket_total(),
        swapped.ticket_total()
    );
    normal
        .outcomes()
        .iter()
        .zip(swapped.outcomes().iter())
        .map(|(left, right)| {
            assert_eq!(
                left.ticket_number(),
                right.ticket_number(),
                "两条链的工单顺序不同,无法逐单比对"
            );
            let normal_fingerprint: String = left.resolution().fingerprint_text();
            let swapped_fingerprint: String = right.resolution().fingerprint_text();
            OrderPair {
                ticket_number: left.ticket_number().to_string(),
                ticket_type_name: left.ticket().ticket_type().chinese_name().to_string(),
                normal_handler: left.handler_name(),
                swapped_handler: right.handler_name(),
                normal_disposition: left.resolution().disposition_text().to_string(),
                swapped_disposition: right.resolution().disposition_text().to_string(),
                changed: normal_fingerprint != swapped_fingerprint,
            }
        })
        .collect()
}

/// ★ 换序对照的检查。
///
/// # 参数
/// - `pairs`:逐单对照结果。
pub fn order_checks(pairs: &[OrderPair]) -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();
    let changed_total: usize = pairs.iter().filter(|pair| pair.changed).count();

    // ① ★ 换序**真的改变了**结论(> 0)。
    //    已知会失败的输入:用**同一个顺序**跑两遍(那时必然 0 张变化,
    //    这条检查会红------它盯的正是「换个顺序其实没差别」这种假对照)。
    lines.push(CheckLine::new(
        "★ 换序真的改变了结论(> 0 张)",
        "> 0(已知会失败:两条链用同一个顺序跑,必然 0 张变化)",
        format!("{} 张 / 共 {} 张", changed_total, pairs.len()).as_str(),
        changed_total > 0,
    ));

    // ② ★ 变化的单**全都是**受换序影响的类型(归因要准)。
    //    本工程的换序是把「保修判定」挪到金额权限之后,
    //    因此受影响的只可能是**走保修**的工单(维修 / 成色复检)。
    //    若一张「退货」也变了,说明换序的影响超出了预期------那就不是归因,是意外。
    let unexpected_total: usize = pairs
        .iter()
        .filter(|pair| pair.changed)
        .filter(|pair| !is_warranty_route_type(&pair.ticket_type_name))
        .count();
    lines.push(CheckLine::new(
        "★ 变化的单全部是走保修的类型(归因正确)",
        "0 张意外(已知会失败:把「单据校验」也挪位,退货单的结论也会变)",
        format!("{} 张意外", unexpected_total).as_str(),
        unexpected_total == 0,
    ));

    // ③ 两条链的单数相同。
    lines.push(CheckLine::new(
        "两条链跑的是同一批工单",
        format!("{} 张", pairs.len()).as_str(),
        format!("{} 张", pairs.len()).as_str(),
        !pairs.is_empty(),
    ));

    lines
}

/// 是否属于「走保修」的工单类型(**归因判据**)。
///
/// ⚠️ 这里按**工单类型名**判而不是按编码判:
/// 类型名是给人看的,报表上的归因必须与读者看到的一致。
/// 若按编码判,读者看到「维修」却不知道它算不算,归因就白做了。
fn is_warranty_route_type(ticket_type_name: &str) -> bool {
    ticket_type_name == crate::domain::TICKET_REPAIR.chinese_name()
        || ticket_type_name == crate::domain::TICKET_RECHECK.chinese_name()
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : outcome_view.rs
//! 工单处置结果的**分析层视图**。
//!
//! ## ★ 为什么表示层不直接拿 `crate::client::TicketOutcome`
//!
//! 依赖实测里 `app` 只能看见 `analysis / domain / support`,**没有 `client`**。
//! 这不是洁癖:一旦报表直接去读 `TicketOutcome`,
//! 「这一列显示什么」就会被会话的结构牵着走,改会话就要改报表。
//!
//! 因此这里把一张工单的结论摊成一组**已格式化好的字符串**,
//! 表示层只做排版,不做任何计算。

use crate::client::TicketOutcome;
use crate::domain::Money;

/// 一张工单的处置结果视图。
///
/// # 字段说明(**全是可直接排版字符串**)
///
/// ⚠️ 为什么**没有**门店 / 品类 / 购买月数这些字段:
/// 幕七这张表要回答的是「谁接的、走了几站、跳过了几站」,
/// 而那些维度已经在幕五「工单清单」里印过一遍。
/// 多带几个字段不影响正确性,但会让人以为「这张表也该核那些维度」------
/// 而它核不了(列宽不够),于是核对就被悄悄跳过了。
/// 宁可字段**少而全部被用**,不要多而有人没被读。
#[derive(Clone, Debug)]
pub struct OutcomeView {
    /// 工单号。
    pub ticket_number: String,
    /// 工单类型名。
    pub ticket_type_name: String,
    /// 涉及金额文本。
    pub amount_text: String,
    /// ★ 处置者名。
    pub handler_name: String,
    /// 结论类型文本。
    pub disposition_text: String,
    /// 赔付金额文本。
    pub compensation_text: String,
    /// 访问轨迹文本。
    pub visited_text: String,
    /// ★ 因短路未调用的站数。
    pub skipped_total: usize,
    /// 走了几站。
    pub visited_total: usize,
}

impl OutcomeView {
    /// 由一次投递结果转出。
    pub fn from_outcome(outcome: &TicketOutcome) -> Self {
        let ticket = outcome.ticket();
        let resolution = outcome.resolution();
        Self {
            ticket_number: ticket.ticket_number().to_string(),
            ticket_type_name: ticket.ticket_type().chinese_name().to_string(),
            amount_text: ticket.involved_amount().chinese_text(),
            handler_name: outcome.handler_name(),
            disposition_text: resolution.disposition_text().to_string(),
            compensation_text: resolution.compensation().chinese_text(),
            visited_text: outcome.visited_text(),
            skipped_total: outcome.skipped_total(),
            visited_total: outcome.visited_total(),
        }
    }

    /// 一批结果转出一批视图(**保持投递顺序**)。
    pub fn list_from_outcomes(outcomes: &[TicketOutcome]) -> Vec<OutcomeView> {
        outcomes.iter().map(OutcomeView::from_outcome).collect()
    }
}

/// ★ 赔付合计(**只统计真的要赔的**,见 [`crate::domain::Disposition::bears_compensation`])。
///
/// ⚠️ 为什么单列一个函数而不是在会话里加一个方法:
/// 「哪些结论算赔钱」是**口径**,口径属于分析层;
/// 会话只负责「跑完一遍」。
pub fn compensation_total(outcomes: &[TicketOutcome]) -> Money {
    let mut total: Money = Money::zero(crate::domain::CNY);
    for outcome in outcomes.iter() {
        if outcome.disposition().bears_compensation() {
            total = total.plus(outcome.compensation());
        }
    }
    total
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : station_census.rs
//! 链上各站的对照:**谁被问得最多、谁一次都没被问**。
//!
//! ## ★ 这张表是责任链最有用的一张表
//!
//! 「我们用了责任链」这句话本身没有任何信息。
//! 有信息的是:
//! - 第 1 站被问了 N 次(= 投递数),接下 0 次 → 它是过路站;
//! - 门店站接下了 3 张 → 七成的单在门店就解决了;
//! - 总部站只被问了 1 次 → 因为前面的站把其余的单都拦下了;
//! - **某站一次都没被问** → 要么它多余,要么前面某站的判据写宽了。
//!
//! 最后一条尤其重要:它不会自己说话。
//! 一个「写宽了」的判据在业务上表现为「某级审批形同虚设」,
//! 而报表上只是「那一站的被问次数是 0」。

use crate::analysis::CheckLine;
use crate::chain::{HandlerKind, LedgerSnapshot};
use crate::client::TicketSession;

/// 一站的对照探针。
///
/// # 字段说明
/// - `name` / `kind` / `kind_text` / `description`:这一站是什么。
///   (`kind` 存枚举、`kind_text` 存短文本:比较用前者、显示用后者。)
/// - `asked_total`:被问了几次(**判定次数**)。
/// - `handled_total`:接下了几次。
/// - `passed_total`:放行了几次。
/// - `never_reached_total`:★ 因前面已处置而**一次都没轮到**的次数。
#[derive(Clone, Debug)]
pub struct StationProbe {
    /// 站名。
    name: String,
    /// 处理器种类(**存枚举本身**,不存文本------比较要用,文本会变)。
    kind: HandlerKind,
    /// 种类短文本。
    kind_text: String,
    /// 说明。
    description: String,
    /// 被问次数。
    asked_total: usize,
    /// 接下次数。
    handled_total: usize,
    /// 放行次数。
    passed_total: usize,
    /// 因短路未轮到的次数。
    never_reached_total: usize,
}

impl StationProbe {
    /// 站名。
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// 种类短文本(`应答者` / `过路者`)。
    pub fn kind_text(&self) -> &str {
        self.kind_text.as_str()
    }

    /// ★ 处理器种类(**枚举本身**,用于判「这一站是过路者吗」)。
    pub const fn kind(&self) -> HandlerKind {
        self.kind
    }

    /// 说明。
    pub fn description(&self) -> &str {
        self.description.as_str()
    }

    /// 被问次数。
    pub const fn asked_total(&self) -> usize {
        self.asked_total
    }

    /// 接下次数。
    pub const fn handled_total(&self) -> usize {
        self.handled_total
    }

    /// 放行次数。
    pub const fn passed_total(&self) -> usize {
        self.passed_total
    }

    /// 因短路未轮到的次数。
    pub const fn never_reached_total(&self) -> usize {
        self.never_reached_total
    }

    /// ★ 是否是**死站**(一次都没被问过)。
    pub const fn is_never_asked(&self) -> bool {
        self.asked_total == 0
    }

    /// ★ 风险提示:把这一站的处境说成一句话。
    ///
    /// ⚠️ 判据只用**观测本身的两个数**(被问次数与接下次数),
    /// 不引用任何外部口径------否则这张表就会说出与数字不符的话。
    pub fn risk_text(&self) -> String {
        if self.is_never_asked() {
            return format!(
                "★★ 死站:{} 张单一张都没走到这一站(要么多余,要么前面判据写宽了)",
                self.never_reached_total
            );
        }
        if self.handled_total == 0 {
            return format!(
                "★ 纯过路:被问 {} 次、接下 0 次(只做事不做决定)",
                self.asked_total
            );
        }
        format!(
            "接下 {} 张(占投递 {} 张的 {}%)",
            self.handled_total,
            self.asked_total + self.never_reached_total,
            ratio_percent_text(crate::domain::ratio_basis_points(
                self.handled_total as i128,
                (self.asked_total + self.never_reached_total) as i128
            ))
        )
    }
}

/// ★ 由会话与快照汇总各站探针(**按链上顺序**)。
///
/// # 参数
/// - `session`:会话(提供链上站点、顺序与**逐单结果**)。
/// - `snapshot`:台账快照(提供计数)。
/// - `descriptions`:站名 → 说明(由调用方给,因为说明带金额上限这类算出来的文本)。
pub fn station_probes(
    session: &TicketSession,
    snapshot: &LedgerSnapshot,
    descriptions: &[(String, String)],
) -> Vec<StationProbe> {
    let mut probes: Vec<StationProbe> = Vec::new();
    for (index, (name, kind)) in session.chain().handler_kinds().into_iter().enumerate() {
        let mut asked_total: usize = 0;
        let mut handled_total: usize = 0;
        let mut passed_total: usize = 0;
        for attempt in snapshot.attempts.iter() {
            if attempt.name() == name {
                asked_total = attempt.asked_total();
                handled_total = attempt.handled_total();
                passed_total = attempt.passed_total();
            }
        }
        let description: String = descriptions
            .iter()
            .find(|(station_name, _)| *station_name == name)
            .map(|(_, text)| text.clone())
            .unwrap_or_else(|| "---".to_string());
        probes.push(StationProbe {
            name,
            kind,
            kind_text: kind.short_text().to_string(),
            description,
            asked_total,
            handled_total,
            passed_total,
            never_reached_total: never_reached_total_for(session, index),
        });
    }
    probes
}

/// ★ 第 `index` 站「一次都没轮到」的单数。
///
/// 口径:**被处置的位置 < index** 的单数(精确,不用估算)。
///
/// ⚠️ 为什么不能写成 `投递数 − 被问次数`:
/// 一张**无人处置**的单会走完链尾(每一站都被问过),
/// 那时它会被 `投递数 − 被问次数` 错算进「未被轮到」------
/// 于是检查①(`被问 + 未轮到 == 投递数`)会平白差那几张。
fn never_reached_total_for(session: &TicketSession, index: usize) -> usize {
    session
        .outcomes()
        .iter()
        .filter(|outcome| match outcome.result().visited_position().last() {
            Some(last_position) => *last_position < index,
            None => false,
        })
        .count()
}

/// 把万分点渲染成百分数(`3000` → `30.00%`)。
///
/// ⚠️ 为什么不复用 `app` 的同名函数:分析层**不能**依赖表示层
/// (依赖实测里 `analysis -> client domain chain`,没有 `app`)。
/// 两处各有一份不是重复------它们是不同层的同一口径,
/// 由体检里的「版面口径」项保证一致。
fn ratio_percent_text(basis_points: i64) -> String {
    format!("{}.{:02}%", basis_points / 100, basis_points % 100)
}

/// ★ 链上各站的检查。
///
/// # 参数
/// - `probes`:各站探针。
/// - `ticket_total`:投递单数。
pub fn station_checks(probes: &[StationProbe], ticket_total: usize) -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();

    // ① 每站:被问 + 未轮到 == 投递数。
    //    已知会失败的输入:某一站的计数被漏记(比如只记了接下没记放行)。
    let mut consistent: usize = 0;
    for probe in probes.iter() {
        if probe.asked_total() + probe.never_reached_total() == ticket_total {
            consistent += 1;
        }
    }
    lines.push(CheckLine::new(
        "每站:被问 + 未轮到 == 投递数",
        format!("{} 站全部成立(已知会失败:某站只记接下、漏记放行)", probes.len()).as_str(),
        format!("{}/{}", consistent, probes.len()).as_str(),
        consistent == probes.len() && !probes.is_empty(),
    ));

    // ② ★ 过路站接下次数必须是 0。
    //    已知会失败的输入:把某个过路站的 `handle` 改成返回一个结论。
    let passthrough_total: usize = probes
        .iter()
        .filter(|probe| probe.kind() == HandlerKind::PassThrough)
        .count();
    let passthrough_clean: usize = probes
        .iter()
        .filter(|probe| probe.kind() == HandlerKind::PassThrough)
        .filter(|probe| probe.handled_total() == 0)
        .count();
    lines.push(CheckLine::new(
        "★ 过路站(不做决定的站)接下次数 == 0",
        format!(
            "{} 个过路站全部为 0(已知会失败:让过路站偷偷返回一个结论)",
            passthrough_total
        )
        .as_str(),
        format!("{}/{}", passthrough_clean, passthrough_total).as_str(),
        passthrough_total > 0 && passthrough_clean == passthrough_total,
    ));

    // ③ ★ 死站数 == 0。
    //    已知会失败的输入:把某一站的判据放宽到「恒接」,后面几站就永远轮不到。
    let dead_total: usize = probes.iter().filter(|probe| probe.is_never_asked()).count();
    lines.push(CheckLine::new(
        "★ 死站数 == 0(每一站都真的被问过)",
        "0(已知会失败:把第 2 站的判据改成恒接,后面几站一次都轮不到)",
        format!("{} 站", dead_total).as_str(),
        dead_total == 0,
    ));

    // ④ ★ 至少有一站接下过(链不是死的)。
    let handling_stations: usize = probes.iter().filter(|probe| probe.handled_total() > 0).count();
    lines.push(CheckLine::new(
        "至少有一站真的接下过单",
        ">= 1(已知会失败:把所有站的判据都写成恒不接)",
        format!("{} 站", handling_stations).as_str(),
        handling_stations >= 1,
    ));

    lines
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : value_quantification.rs
//! 价值量化:**责任链省下了多少判定,值多少钱**。
//!
//! ## ★ 「省下了什么」用**判定次数**而不是耗时
//!
//! 耗时不可复现(两次运行不算同一个数,就谈不上「逐字节一致」的验收)。
//! 可数的是:一张单在第 k 站被接下,后面 `站数 − k − 1` 站**一次都没被调用**。
//!
//! ## ★ 为什么还要一个「次数之外的口径」
//!
//! 只说「省了 N 次判定」隐含一个**不成立**的假设:每次判定的代价一样。
//! 事实上「查一下保修期」与「走一遍权限表」的代价差得远。
//! 因此本工程另给一个口径:**被短路掉的站里,有几站是「要查外部状态」的**
//! (本工程里 = 金额权限站,它们要读权限档位)。
//! 两个口径并列,读者才不会把「省了 N 次」当成「省了 N 倍」。

use crate::analysis::CheckLine;
use crate::domain::{ratio_basis_points, Money, CNY};

/// 责任链的价值报告。
///
/// # 字段说明
/// - `ticket_total`:投递单数。
/// - `station_total`:链上站数。
/// - `visited_total`:★ 实际判定次数。
/// - `visited_total_if_broadcast`:★ 若不用责任链(广播式)的判定次数。
/// - `saved_judgement_total`:★ 省下的判定次数(**两个可数数字之差**)。
/// - `saved_ratio_basis_points`:省下的比例(万分点)。
/// - `skipped_total`:因短路未被调用的站数之和(**与上一项互为印证**)。
/// - `compensation_minor_units`:赔付总额(分)。
/// - `settled_total` / `unhandled_total`:被处置 / 无人处置单数。
#[derive(Clone, Copy, Debug)]
pub struct ChainValueReport {
    /// 投递单数。
    ticket_total: usize,
    /// 链上站数。
    station_total: usize,
    /// 实际判定次数。
    visited_total: usize,
    /// 广播式的判定次数。
    visited_total_if_broadcast: usize,
    /// ★ 省下的判定次数。
    saved_judgement_total: i64,
    /// 省下的比例(万分点)。
    saved_ratio_basis_points: i64,
    /// 因短路未被调用的站数之和。
    skipped_total: usize,
    /// 赔付总额(分)。
    compensation_minor_units: i64,
    /// 被处置单数。
    settled_total: usize,
    /// 无人处置单数。
    unhandled_total: usize,
}

impl ChainValueReport {
    /// 由一组口径数字组装。
    ///
    /// ⚠️ 十个参数不是设计缺陷,是**这份报告的口径清单**:
    /// 每个数都来自不同层的不同计数,缺一个就算不出「省下多少」。
    /// 若改成传一个 `&TicketSession`,本层就要认识 `client` 的具体结构,
    /// 而「报告只认数字」正是它能被**独立复算**的前提。
    pub fn new(
        ticket_total: usize,
        station_total: usize,
        visited_total: usize,
        skipped_total: usize,
        compensation_minor_units: i64,
        settled_total: usize,
        unhandled_total: usize,
    ) -> Self {
        let visited_total_if_broadcast: usize = ticket_total * station_total;
        let saved_judgement_total: i64 = visited_total_if_broadcast as i64 - visited_total as i64;
        Self {
            ticket_total,
            station_total,
            visited_total,
            visited_total_if_broadcast,
            saved_judgement_total,
            saved_ratio_basis_points: ratio_basis_points(
                saved_judgement_total as i128,
                visited_total_if_broadcast as i128,
            ),
            skipped_total,
            compensation_minor_units,
            settled_total,
            unhandled_total,
        }
    }

    /// 投递单数。
    pub const fn ticket_total(&self) -> usize {
        self.ticket_total
    }

    /// 链上站数。
    pub const fn station_total(&self) -> usize {
        self.station_total
    }

    /// 实际判定次数。
    pub const fn visited_total(&self) -> usize {
        self.visited_total
    }

    /// 广播式判定次数。
    pub const fn visited_total_if_broadcast(&self) -> usize {
        self.visited_total_if_broadcast
    }

    /// ★ 省下的判定次数。
    pub const fn saved_judgement_total(&self) -> i64 {
        self.saved_judgement_total
    }

    /// 省下的比例(万分点)。
    pub const fn saved_ratio_basis_points(&self) -> i64 {
        self.saved_ratio_basis_points
    }

    /// 因短路未被调用的站数之和。
    pub const fn skipped_total(&self) -> usize {
        self.skipped_total
    }

    /// 赔付总额。
    pub fn compensation(&self) -> Money {
        Money::new(self.compensation_minor_units, CNY)
    }

    /// 被处置单数。
    pub const fn settled_total(&self) -> usize {
        self.settled_total
    }

    /// 无人处置单数。
    pub const fn unhandled_total(&self) -> usize {
        self.unhandled_total
    }

    /// ★ 一句话总结。
    pub fn summary_text(&self) -> String {
        format!(
            "{} 张单 × {} 站:若每站都问一遍要 {} 次判定,责任链只用了 {} 次,\
             省下 {} 次({});赔付合计 {}。",
            self.ticket_total,
            self.station_total,
            self.visited_total_if_broadcast,
            self.visited_total,
            self.saved_judgement_total(),
            percent_text_local(self.saved_ratio_basis_points),
            self.compensation().chinese_text()
        )
    }

    /// ★ 五条自检(**每条都有已知会失败的输入**)。
    pub fn self_check(&self) -> Vec<CheckLine> {
        let mut lines: Vec<CheckLine> = Vec::new();

        // ① 投递 == 被处置 + 无人处置。
        lines.push(CheckLine::new(
            "投递数 == 被处置 + 无人处置",
            "相等(已知会失败:某张单既没记处置也没记无人处置)",
            format!(
                "{} == {} + {}",
                self.ticket_total, self.settled_total, self.unhandled_total
            )
            .as_str(),
            self.ticket_total == self.settled_total + self.unhandled_total,
        ));

        // ② ★ 省下的次数 == 广播式 − 实际(口径自检)。
        lines.push(CheckLine::new(
            "★ 省下次数 == 广播式 − 实际",
            "相等(已知会失败:把省下次数改成用「跳过站数」算,两者口径不同)",
            format!(
                "{} == {} − {}",
                self.saved_judgement_total(),
                self.visited_total_if_broadcast,
                self.visited_total
            )
            .as_str(),
            self.saved_judgement_total()
                == self.visited_total_if_broadcast as i64 - self.visited_total as i64,
        ));

        // ③ ★ 实际判定次数 < 广播式(真的省了)。
        lines.push(CheckLine::new(
            "★ 实际判定次数 < 广播式",
            "严格小于(已知会失败:所有站判据都写成恒不接,每张单都走完链尾)",
            format!("{} < {}", self.visited_total, self.visited_total_if_broadcast).as_str(),
            self.visited_total < self.visited_total_if_broadcast,
        ));

        // ④ 无人处置 == 0(链尾有兜底)。
        lines.push(CheckLine::new(
            "无人处置单数 == 0(链尾有兜底)",
            "0(已知会失败:摘掉总裁办兜底站)",
            format!("{} 张", self.unhandled_total).as_str(),
            self.unhandled_total == 0,
        ));

        // ⑤ ★ 跳过站数 == 省下的判定次数(两个口径互为印证)。
        //    ⚠️ 这两个数**恰好相等**是因为「每一站至多被问一次」:
        //    一张单在第 k 站被接下,后面 `站数 − k − 1` 站既没被问、也正因如此才省下。
        //    若将来某一站会被问两次,这两个数就会分叉------那时这条检查会红,
        //    而它红的原因正值得看一眼。
        lines.push(CheckLine::new(
            "★ 跳过站数 == 省下的判定次数(两口径互为印证)",
            "相等(已知会失败:某一站被重复问一次,两口径就会分叉)",
            format!("{} == {}", self.skipped_total, self.saved_judgement_total()).as_str(),
            self.skipped_total as i64 == self.saved_judgement_total(),
        ));

        lines
    }
}

/// 把万分点渲染成百分数(`3000` → `30.00%`)。
///
/// ⚠️ 与 [`crate::analysis::station_census`] 里的同名私有函数各有一份:
/// 分析层的两个模块互不依赖(职责单一),同一口径分头实现,
/// 由体检里的「版面口径」项保证一致。
fn percent_text_local(basis_points: i64) -> String {
    format!("{}.{:02}%", basis_points / 100, basis_points % 100)
}
相关推荐
m0_587383001 小时前
碰碰好评卡功能落地全流程实战指南 附核心逻辑与常见问题解决方案
java·前端·数据库·后端·struts·需求分析
dd聊技术1 小时前
给检索改动装一盏红绿灯:评测门禁怎么建、怎么用
人工智能·后端
阿里嘎多学长1 小时前
2026-10-14 GitHub 热点项目精选
开发语言·程序员·github·代码托管
迪康coolmu1 小时前
迪康Defender网络准入控制系统落地实战:部署、配置与问题排查记录
运维·开发语言·网络·经验分享·php
兰亭妙微UI设计公司1 小时前
兰亭妙微分享:高手设计登录页的12种切入点
开发语言·前端·javascript
木由里予1 小时前
3D NAND 闪存的可靠性
开发语言·嵌入式硬件
acd120091 小时前
Go 里 err 被不经意覆盖的几个现场
开发语言·后端·golang
前端Baymax1 小时前
静默的 86 小时:全链 402 停摆与零告警的故障复盘
开发语言·前端·javascript
合尘猫1 小时前
给客服系统加一道闸:把「该转人工」从模糊判断变成可测拦截
人工智能·后端·程序员