rust: Facade Pattern

项目结构:

image

 

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : cargo_category.rs
//! 货物类别(开放型标签)。
//!
//! ## 为什么是开放型
//!
//! 珠宝公司的货物类别会随业务扩张不断新增(海关样品、维修件、展品、
//! 赠品……)。做成枚举的话,每加一类都要改领域层。
//!
//! ## 携带的属性如何被使用
//!
//! - `requires_formal_declaration`:决定单据子系统是否生成报关单;
//! - `allows_letter_of_credit`:该品类能否用信用证结算(展品通常不行)。
//!
//! 两个都是布尔属性,由子系统读取后自行决定行为,门面不做分派。
//! 这正是「开放型标签」的典型用法:**扩展维度靠加常量,不靠改代码分支**。

/// 一种货物类别。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CargoCategory {
    /// 类别编码,如 `"JEWELRY"`。
    code: &'static str,
    /// 中文名称,如 `"珠宝首饰"`。
    label: &'static str,
    /// 是否必须做正式报关申报。
    requires_formal_declaration: bool,
    /// 是否允许使用信用证结算。
    allows_letter_of_credit: bool,
}

impl CargoCategory {
    /// 构造一个货物类别标签。
    pub const fn new(
        code: &'static str,
        label: &'static str,
        requires_formal_declaration: bool,
        allows_letter_of_credit: bool,
    ) -> Self {
        CargoCategory {
            code,
            label,
            requires_formal_declaration,
            allows_letter_of_credit,
        }
    }

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

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

    /// 是否必须正式报关。
    pub const fn requires_formal_declaration(&self) -> bool {
        self.requires_formal_declaration
    }

    /// 是否允许信用证结算。
    pub const fn allows_letter_of_credit(&self) -> bool {
        self.allows_letter_of_credit
    }
}

/// 珠宝首饰(高值、必须报关、可用信用证)。
pub const CARGO_JEWELRY: CargoCategory = CargoCategory::new("JEWELRY", "珠宝首饰", true, true);

/// 包装物料(低值、无需单独报关、不可用信用证)。
pub const CARGO_PACKAGING: CargoCategory = CargoCategory::new("PACKAGING", "包装物料", false, false);

/// 随货证书(无商业价值、无需报关)。
pub const CARGO_CERTIFICATE: CargoCategory =
    CargoCategory::new("CERTIFICATE", "随货证书", false, false);


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : country_code.rs
//! 国家/地区代码(开放型标签)。
//!
//! ## 为什么是结构体而不是枚举
//!
//! 门面演示里会「新增一个国家」来验证扩展性。若这里是
//! `enum CountryCode { CN, DE }`,工程外想加「新加坡」就得改本文件——
//! 一个纯粹的数据扩充却要动领域层,扩展性验证会当场失败。
//!
//! 做成 `const fn new(...)` 的开放型结构体后,新增国家只是在新位置写一行
//! 常量定义,领域层文件一字不改。这是本工程所有「标签类维度」的统一做法。
//!
//! ## 哪些属性值得携带
//!
//! `code`(ISO 3166-1 alpha-2)与 `label`(中文名)是展示与键控必需的。
//! 另外携带 `requires_customs_declaration`——它决定清关环节要不要生成报关单。
//! 这个标志**是数据不是行为分派**(没有 `match`),所以结构体字段足够,
//! 无需枚举。

/// 一个国家或地区的代码与其业务属性。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CountryCode {
    /// ISO 3166-1 alpha-2 代码,如 `"CN"`。
    code: &'static str,
    /// 中文名称,如 `"中国"`。
    label: &'static str,
    /// 该目的地是否要求做正式报关申报。
    requires_customs_declaration: bool,
}

impl CountryCode {
    /// 构造一个国家/地区标签。
    ///
    /// 参数 `code` / `label` / `requires_customs_declaration`。
    ///
    /// 为 `const fn`:这样预置常量与工程外新增常量都能在编译期构造,
    /// 运行时零开销。
    pub const fn new(
        code: &'static str,
        label: &'static str,
        requires_customs_declaration: bool,
    ) -> Self {
        CountryCode {
            code,
            label,
            requires_customs_declaration,
        }
    }

    /// 国家/地区代码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

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

    /// 是否要求正式报关。
    ///
    /// 注意这是**属性读取**而非行为分派:门面只是把它塞进单据子系统,
    /// 由单据子系统决定怎么用。因此不需要枚举。
    pub const fn requires_customs_declaration(&self) -> bool {
        self.requires_customs_declaration
    }
}

/// 中国内地。
pub const COUNTRY_CHINA: CountryCode = CountryCode::new("CN", "中国", true);

/// 德国(本工程的主要出口目的地)。
pub const COUNTRY_GERMANY: CountryCode = CountryCode::new("DE", "德国", true);

/// 中国香港(本工程用于演示「同一国家维度下的另一个地区标签」)。
pub const COUNTRY_HONG_KONG: CountryCode = CountryCode::new("HK", "中国香港", true);


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : currency_amount.rs
//! 货币与金额。
//!
//! ## 为什么金额必须用整数「分」
//!
//! 若用 `f64` 存金额,`0.1 + 0.2 != 0.3`,而在结算场景里这意味着
//! 「三笔费用相加对不上账单」。门面会汇总多个子系统的金额,
//! 每多一次浮点累加就多一分误差,因此全工程统一用 `i64` 存**最小单位**
//! (人民币为分),只在展示时才转成「元」。
//!
//! ## 为什么把 `Currency` 做成开放型结构体
//!
//! 门面以后可能要接美元、港币账单。若 `Currency` 做成枚举,
//! 工程外想加一种币种就得改本文件,扩展性验证当场失败。
//! 因此它是 [`Currency::new`] 构造的开放型标签——
//! 但注意其 `allows_cent_precision` 之类的属性是**数据**而非行为分派,
//! 所以用结构体携带即可,无需枚举的穷尽性检查。

use std::fmt;

/// 金额的缩放比例分母:万分之。
///
/// 所有「按比例缩放」的操作都以此为基础分母,保证全工程只有一种比例口径。
/// 之所以不用百分之一:运费折扣、税率、佣金率常需要万分之一级别的精度
/// (如 0.03% 的保价费率 = 3 万分之 3),用百分比会在中间步骤就被舍入掉。
pub const BASIS_POINTS_DENOMINATOR: i64 = 10_000;

/// 一种货币。
///
/// 开放型标签:`code` 为 ISO 4217 代码,`label` 为中文名,
/// `minor_units_per_major_unit` 为一个主单位等于多少最小单位
/// (人民币为 100 分)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Currency {
    /// 货币代码,如 `"CNY"`、`"HKD"`。
    code: &'static str,
    /// 货币中文名,如 `"人民币"`。
    label: &'static str,
    /// 一个主单位等于多少个最小单位(人民币 = 100)。
    minor_units_per_major_unit: i64,
    /// 该货币的展示符号,如 `"¥"`。
    symbol: &'static str,
}

impl Currency {
    /// 构造一种货币。
    ///
    /// 参数 `code` / `label` / `minor_units_per_major_unit` / `symbol`。
    /// 返回:货币值对象。
    ///
    /// 为 `const fn`,便于在常量上下文中预置币种。
    pub const fn new(
        code: &'static str,
        label: &'static str,
        minor_units_per_major_unit: i64,
        symbol: &'static str,
    ) -> Self {
        Currency {
            code,
            label,
            minor_units_per_major_unit,
            symbol,
        }
    }

    /// 货币代码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

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

    /// 一个主单位包含的最小单位数。
    pub const fn minor_units_per_major_unit(&self) -> i64 {
        self.minor_units_per_major_unit
    }

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

/// 人民币(本工程的主要记账币种)。
pub const CURRENCY_CHINESE_YUAN: Currency = Currency::new("CNY", "人民币", 100, "¥");

/// 港币(用于演示多币种扩展点;最小单位是分,与人民币一致)。
pub const CURRENCY_HONG_KONG_DOLLAR: Currency = Currency::new("HKD", "港币", 100, "HK$");

/// 一个金额,以最小单位(分)存储。
///
/// 内部只保存 `minor_units`,币种作为独立字段一起携带——
/// 这样「¥100 加 HK$100」这种错误在类型上仍然合法(都是 `CurrencyAmount`),
/// **需要靠调用方保证同币种**。这里不引入编译期币种类型标记,
/// 因为那会让所有算术签名膨胀,收益(防住一种低频错误)不划算。
/// 代价是:跨币种运算前必须显式换算,本工程在门面里做了这一步。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CurrencyAmount {
    /// 金额的最小单位数值(人民币即为「分」)。
    minor_units: i64,
    /// 该金额所属币种。
    currency: Currency,
}

impl CurrencyAmount {
    /// 以最小单位构造金额。
    ///
    /// 参数 `minor_units`:最小单位数值;`currency`:币种。
    pub const fn from_minor_units(minor_units: i64, currency: Currency) -> Self {
        CurrencyAmount {
            minor_units,
            currency,
        }
    }

    /// 以主单位构造金额(如「12.34 元」→ 1234 分)。
    ///
    /// 参数 `major_units`:主单位整数部分;`minor_part`:最小单位部分;
    /// `currency`:币种。
    /// 返回:金额。
    ///
    /// 拆成两个参数是为了**避免浮点**:调用方写
    /// `from_major_and_minor(12, 34, CURRENCY_CHINESE_YUAN)` 表示 12.34 元,
    /// 比写 `12.34` 再转换精确得多。
    pub const fn from_major_and_minor(
        major_units: i64,
        minor_part: i64,
        currency: Currency,
    ) -> Self {
        CurrencyAmount {
            minor_units: major_units * currency.minor_units_per_major_unit() + minor_part,
            currency,
        }
    }

    /// 零金额(给定币种)。
    ///
    /// 参数 `currency`:币种。
    pub const fn zero(currency: Currency) -> Self {
        CurrencyAmount {
            minor_units: 0,
            currency,
        }
    }

    /// 读取最小单位数值。
    pub const fn minor_units(&self) -> i64 {
        self.minor_units
    }

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

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

    /// 是否为负(表示退款、减免额时会出现)。
    pub const fn is_negative(&self) -> bool {
        self.minor_units < 0
    }

    /// 取绝对值(金额方向无关时使用,如「减免了多少」的展示)。
    pub const fn absolute(&self) -> Self {
        CurrencyAmount {
            minor_units: if self.minor_units < 0 {
                -self.minor_units
            } else {
                self.minor_units
            },
            currency: self.currency,
        }
    }

    /// 相加。
    ///
    /// 参数 `other`:另一个同币种金额。
    /// 返回:和。
    ///
    /// 用 `saturating_add` 而不是 `+`:金额溢出在业务上绝不该发生,
    /// 但一旦发生,`+` 会在 debug 下 panic、release 下静默回绕,
    /// 两种行为都不可接受。饱和到 `i64::MAX` 至少是**可观测**的错误值。
    pub const fn add(&self, other: &CurrencyAmount) -> Self {
        CurrencyAmount {
            minor_units: self.minor_units.saturating_add(other.minor_units),
            currency: self.currency,
        }
    }

    /// 相减。
    ///
    /// 参数 `other`:另一个同币种金额。
    /// 返回:差(可能为负)。
    pub const fn subtract(&self, other: &CurrencyAmount) -> Self {
        CurrencyAmount {
            minor_units: self.minor_units.saturating_sub(other.minor_units),
            currency: self.currency,
        }
    }

    /// 乘以一个整数数量(如「单价 × 件数」)。
    ///
    /// 参数 `quantity`:整数倍数。
    /// 返回:乘积。
    ///
    /// 用 i128 做中间量再判溢出:i64 相乘极易溢出,
    /// 直接判 i128 结果是否越界比预判 `a * b` 是否溢出更可靠。
    pub const fn multiply_by_quantity(&self, quantity: i64) -> Self {
        let intermediate: i128 = self.minor_units as i128 * quantity as i128;
        CurrencyAmount {
            minor_units: clamp_i128_to_i64(intermediate),
            currency: self.currency,
        }
    }

    /// 按「万分比」缩放(如运费 × 3 万分之 3 的保价费率)。
    ///
    /// 参数 `basis_points`:万分比数值(3 表示 0.03%)。
    /// 返回:缩放后的金额。
    ///
    /// **四舍五入方向:远离零**。理由:这是结算金额,
    /// 银行家舍入(四舍六入五成双)会让「五」这一档在业务上难以解释;
    /// 而「远离零」对正负金额对称,退款场景也能自洽。
    /// 实现上刻意不借道 `f64`——浮点在边界值会给出反直觉结果。
    pub const fn scale_by_basis_points(&self, basis_points: i64) -> Self {
        // 中间量用 i128:金额(i64)× 万分比(i64)可达 2^126,仍是 i64 的两倍余量。
        let numerator: i128 = self.minor_units as i128 * basis_points as i128;
        let rounded: i128 = divide_rounded_away_from_zero(numerator, BASIS_POINTS_DENOMINATOR as i128);
        CurrencyAmount {
            minor_units: clamp_i128_to_i64(rounded),
            currency: self.currency,
        }
    }

    /// 生成展示文本,如 `"¥1,234.56"`。
    ///
    /// 千分位分隔是本函数自己实现的(见 `insert_thousands_separator`),
    /// 不走 `format!` 的 `{:,.2}`——标准库的千分位选项在旧版本不可用,
    /// 且无法处理「最小单位不是 100」的币种。
    pub fn formatted(&self) -> String {
        let per_major: i64 = self.currency.minor_units_per_major_unit();
        // 取绝对值来分别处理「整数部分」与「小数部分」,最后再补符号。
        let absolute_units: i64 = self.minor_units.abs();
        let major_part: i64 = absolute_units / per_major;
        let minor_part: i64 = absolute_units % per_major;

        // 小数位数由「一个主单位有多少最小单位」决定:
        // 100 → 2 位;1000 → 3 位。用 10 的幂反推位数。
        let minor_digits: usize = decimal_digits_of(per_major);

        // 构造小数部分(补足前导零,如 5 分要显示为 05)。
        let minor_text: String = if minor_digits == 0 {
            String::new()
        } else {
            format!("{:0width$}", minor_part, width = minor_digits)
        };

        // 千分位分隔只加在整数部分。
        let major_text: String = insert_thousands_separator(major_part);
        // 符号在有小数时放在金额前,无论正负都显式标出(金额为负必须可见)。
        let sign_text: &str = if self.minor_units < 0 { "-" } else { "" };

        if minor_digits == 0 {
            format!("{}{}{}", sign_text, self.currency.symbol(), major_text)
        } else {
            format!(
                "{}{}{}.{}",
                sign_text,
                self.currency.symbol(),
                major_text,
                minor_text
            )
        }
    }

    /// 生成带币种代码的展示文本,如 `"¥1,234.56 CNY"`。
    ///
    /// 跨币种汇总时必须用这个,否则只看符号无法区分 CNY 与 HKD
    /// (两者符号都以 `$`/`¥` 起头,容易误读)。
    pub fn formatted_with_currency_code(&self) -> String {
        format!("{} {}", self.formatted(), self.currency.code())
    }
}

/// 把 i128 钳制到 i64 范围内。
///
/// 参数 `value`:待钳制的 i128。
/// 返回:钳制后的 i64。
///
/// 独立成自由函数(而非方法):它不依赖任何状态,
/// 且金额、重量、比率三处都要用,放在模块级便于共用。
pub const fn clamp_i128_to_i64(value: i128) -> i64 {
    if value > i64::MAX as i128 {
        i64::MAX
    } else if value < i64::MIN as i128 {
        i64::MIN
    } else {
        value as i64
    }
}

/// 整数除法并向**远离零**的方向四舍五入。
///
/// 参数 `numerator`:被除数(i128,避免中间溢出);`denominator`:除数。
/// 返回:四舍五入后的商。
///
/// 实现要点:Rust 的 `/` 是「向零截断」。要改成「四舍五入」,
/// 需要先算余数,再比较 `2 * |余数|` 与 `|除数|`。
/// **不能**用 `(numerator + denominator / 2) / denominator` 这种写法——
/// 它在负数上会偏(向零方向而非远离零),导致退款金额与正向金额不对称。
/// 本函数是全工程唯一的舍入入口,所有金额/重量/比率都走它。
pub const fn divide_rounded_away_from_zero(numerator: i128, denominator: i128) -> i128 {
    // 除数为 0 属于调用方错误;返回 0 而不是 panic,
    // 因为 const fn 里 panic 的可用形式受限,且本工程调用点均传常量分母。
    if denominator == 0 {
        return 0;
    }
    let quotient: i128 = numerator / denominator;
    let remainder: i128 = numerator % denominator;
    // 余数为 0 时无需调整。
    if remainder == 0 {
        return quotient;
    }
    // 比较 2*|余数| 与 |除数|,判断是否该进位。
    let doubled_remainder: i128 = if remainder < 0 {
        -remainder * 2
    } else {
        remainder * 2
    };
    let absolute_denominator: i128 = if denominator < 0 {
        -denominator
    } else {
        denominator
    };

    if doubled_remainder >= absolute_denominator {
        // 该进位。方向取决于商的符号:
        // 商为正 → 加 1;商为负(或零而分子为负)→ 减 1,从而远离零。
        if numerator < 0 {
            quotient - 1
        } else {
            quotient + 1
        }
    } else {
        quotient
    }
}

/// 计算一个 10 的幂有多少位小数(100 → 2,1000 → 3,1 → 0)。
///
/// 参数 `value`:非零正整数。
/// 返回:它是 10 的几次幂;不是 10 的幂时返回 0(调用方按「无小数」处理)。
fn decimal_digits_of(value: i64) -> usize {
    if value <= 1 {
        return 0;
    }
    let mut remaining: i64 = value;
    let mut digits: usize = 0;
    // 反复除以 10,直到剩下 1;若中途除不尽,说明不是 10 的幂。
    while remaining > 1 {
        if remaining % 10 != 0 {
            return 0;
        }
        remaining /= 10;
        digits += 1;
    }
    digits
}

/// 给整数部分插入千分位分隔符。
///
/// 参数 `integer_value`:非负整数。
/// 返回:插入分隔符后的字符串(如 `1234567` → `"1,234,567"`)。
///
/// 实现方式是从右往左每 3 位插一个逗号,且**必须在字符串上操作**——
/// 用整数取模拼串也能做,但对「1234 取中间三位」这类边界更容易写错。
fn insert_thousands_separator(integer_value: i64) -> String {
    // 先转成十进制文本,再自右向左分组。
    let digits: String = integer_value.to_string();
    let digit_count: usize = digits.len();
    // 每 3 位一组,向上取整得到组数。
    let group_count: usize = digit_count.div_ceil(3);

    let mut result: String = String::with_capacity(digit_count + group_count);
    // 逐组追加,组间用逗号连接。
    for group_index in 0..group_count {
        // 计算该组在原始文本中的起止下标。
        // 第 0 组(最左)可能不足 3 位。
        let group_start: usize = digit_count.saturating_sub((group_index + 1) * 3);
        let group_end: usize = digit_count.saturating_sub(group_index * 3);
        if group_index > 0 {
            result.insert(0, ',');
        }
        // 用 insert_str(0, ...) 从左侧拼接:等价于倒序构造,最后得到正序结果。
        result.insert_str(0, &digits[group_start..group_end]);
    }
    result
}

impl fmt::Display for CurrencyAmount {
    /// 默认展示用带符号形式,便于直接放进 `{}` 占位符。
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.formatted())
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : document_kind.rs
//! 单据类型(开放型标签)。
//!
//! ## 为什么是开放型而不是枚举
//!
//! 单据类型会随合规要求变化(产地证、成分证、濒危物种证明、AEO 认证……)。
//! 门面的单据子系统按类型生成不同单据,扩展时只应加常量。
//!
//! ## 一个重要的设计区分
//!
//! 注意这里的 `code`/`label`/`is_legally_required` 都是**数据**:
//! 单据子系统遍历「本次需要哪些单据」时只做集合运算,不 `match` 类型。
//! 这一点很关键——如果子系统里出现
//! `match document_kind { Invoice => ..., PackingList => ... }`,
//! 那么新增一种单据就必须改子系统代码,扩展性就没了。
//! 本工程的所有扩展维度(国家、品类、材料、单据、等级)都遵守这条纪律。

/// 一种单据类型。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DocumentKind {
    /// 类型编码,如 `"COMMERCIAL_INVOICE"`。
    code: &'static str,
    /// 中文名称,如 `"商业发票"`。
    label: &'static str,
    /// 该单据是否为法定必备(缺少会导致清关受阻)。
    is_legally_required: bool,
    /// 生成该单据所需的模板标识(由单据子系统解释)。
    template_identifier: &'static str,
}

impl DocumentKind {
    /// 构造一种单据类型。
    pub const fn new(
        code: &'static str,
        label: &'static str,
        is_legally_required: bool,
        template_identifier: &'static str,
    ) -> Self {
        DocumentKind {
            code,
            label,
            is_legally_required,
            template_identifier,
        }
    }

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

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

    /// 是否为法定必备单据。
    pub const fn is_legally_required(&self) -> bool {
        self.is_legally_required
    }

    /// 模板标识。
    pub const fn template_identifier(&self) -> &'static str {
        self.template_identifier
    }
}

/// 商业发票(法定必备)。
pub const DOCUMENT_COMMERCIAL_INVOICE: DocumentKind =
    DocumentKind::new("COMMERCIAL_INVOICE", "商业发票", true, "TPL_INVOICE_V3");

/// 装箱单(法定必备)。
pub const DOCUMENT_PACKING_LIST: DocumentKind =
    DocumentKind::new("PACKING_LIST", "装箱单", true, "TPL_PACKING_V2");

/// 原产地证书(法定必备,但可申请后补)。
pub const DOCUMENT_CERTIFICATE_OF_ORIGIN: DocumentKind =
    DocumentKind::new("CERTIFICATE_OF_ORIGIN", "原产地证书", true, "TPL_ORIGIN_V1");

/// 温控声明(非强制,但温控货强烈建议)。
pub const DOCUMENT_TEMPERATURE_DECLARATION: DocumentKind =
    DocumentKind::new("TEMPERATURE_DECLARATION", "温控声明", false, "TPL_TEMP_V1");


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : event_severity.rs
//! 事件严重等级(**封闭枚举**)。
//!
//! ## 为什么这里用枚举而不是开放型结构体
//!
//! 与 [`crate::domain::country_code`] 等标签相反,本类型的特点是
//! **每个变体都会被 `match` 分派不同行为**:
//!
//! - `Info` → 不阻断,只记录;
//! - `Warning` → 不阻断,但需人工确认;
//! - `Critical` → 阻断出单。
//!
//! 这种「必须穷尽处理」的维度正是枚举的用武之地:将来若新增一个等级,
//! 所有 `match` 点都会编译失败,逼开发者逐一决定新等级的行为。
//! 若做成开放型结构体,新等级会静默落入某个 `_ =>` 兜底分支——
//! 那是 bug 的温床。
//!
//! ## 判据总结
//!
//! **只被存/传/显示 → 开放型结构体;决定行为分派 → 封闭枚举。**

use std::fmt;

/// 一次装配或校验过程中产生的事件的严重等级。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum EventSeverity {
    /// 提示级:仅记录,不影响出单。
    Info,
    /// 警告级:需人工确认,但不阻断。
    Warning,
    /// 严重级:阻断出单。
    Critical,
}

impl EventSeverity {
    /// 返回该等级的中文短标签。
    ///
    /// 用 `match` 而非查表:新增变体时本处会编译报错,
    /// 这正是我们想要的「强制更新展示名称」。
    pub const fn label(&self) -> &'static str {
        match self {
            EventSeverity::Info => "提示",
            EventSeverity::Warning => "警告",
            EventSeverity::Critical => "严重",
        }
    }

    /// 该等级是否会阻断出单。
    ///
    /// 这是本枚举的核心行为分派点:门面据此决定汇聚后的结果是
    /// 「可出单」还是「被拒绝」。
    pub const fn blocks_dispatch(&self) -> bool {
        match self {
            // 提示与警告都不阻断——警告留给人工判断,避免系统过度拦截。
            EventSeverity::Info | EventSeverity::Warning => false,
            // 只有严重级阻断。
            EventSeverity::Critical => true,
        }
    }

    /// 该等级对应的处理优先级(数值越大越紧急)。
    ///
    /// 门面在汇总多个子系统的事件时按此排序,
    /// 保证最严重的问题排在报表最前面,运营不必翻到最后才发现。
    pub const fn priority(&self) -> u32 {
        match self {
            EventSeverity::Info => 0,
            EventSeverity::Warning => 1,
            EventSeverity::Critical => 2,
        }
    }

    /// 在报表中使用的标记符号。
    pub const fn marker(&self) -> &'static str {
        match self {
            EventSeverity::Info => "·",
            EventSeverity::Warning => "!",
            EventSeverity::Critical => "×",
        }
    }
}

impl fmt::Display for EventSeverity {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.label())
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : packaging_material.rs
//! 包装材料(开放型标签)。
//!
//! ## 为什么单独建模一种「材料」
//!
//! 包装子系统要按材料计费与估算体积。材料的属性(单位成本、是否可回收、
//! 是否属于温控耗材)会被门面汇总到成本里,也会被承运子系统用于体积重计算。
//!
//! ## 携带的属性
//!
//! - `unit_cost_minor_units`:每件材料成本(最小单位);
//! - `is_recyclable`:是否可回收(影响环保合规单据的勾选);
//! - `is_temperature_insulating`:是否为保温材料(决定能否用于温控货)。
//!
//! 三个属性都是数据。注意 `is_temperature_insulating` 会被包装子系统
//! **读取后判断**,但判断发生在子系统内部而非门面,所以仍是数据不是分派。

/// 一种包装材料。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PackagingMaterial {
    /// 材料编码,如 `"WOODEN_CRATE"`。
    code: &'static str,
    /// 中文名称,如 `"木质礼盒"`。
    label: &'static str,
    /// 每件材料成本(最小单位:分)。
    unit_cost_minor_units: i64,
    /// 是否可回收。
    is_recyclable: bool,
    /// 是否为保温材料。
    is_temperature_insulating: bool,
}

impl PackagingMaterial {
    /// 构造一种包装材料。
    pub const fn new(
        code: &'static str,
        label: &'static str,
        unit_cost_minor_units: i64,
        is_recyclable: bool,
        is_temperature_insulating: bool,
    ) -> Self {
        PackagingMaterial {
            code,
            label,
            unit_cost_minor_units,
            is_recyclable,
            is_temperature_insulating,
        }
    }

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

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

    /// 每件材料成本(最小单位)。
    pub const fn unit_cost_minor_units(&self) -> i64 {
        self.unit_cost_minor_units
    }

    /// 是否可回收。
    pub const fn is_recyclable(&self) -> bool {
        self.is_recyclable
    }

    /// 是否为保温材料。
    pub const fn is_temperature_insulating(&self) -> bool {
        self.is_temperature_insulating
    }
}

/// 木质礼盒:¥18.00/件,可回收,不保温。
pub const PACKAGING_WOODEN_CRATE: PackagingMaterial =
    PackagingMaterial::new("WOODEN_CRATE", "木质礼盒", 1_800, true, false);

/// 防震泡沫箱:¥6.50/件,不可回收,保温。
pub const PACKAGING_FOAM_BOX: PackagingMaterial =
    PackagingMaterial::new("FOAM_BOX", "防震泡沫箱", 650, false, true);

/// 真空铝箔袋:¥2.20/件,可回收,保温。
pub const PACKAGING_VACUUM_FOIL_BAG: PackagingMaterial =
    PackagingMaterial::new("VACUUM_FOIL_BAG", "真空铝箔袋", 220, true, true);


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : ratio.rs
//! 比率(万分比)。
//!
//! ## 为什么单独做一个类型而不是直接用 f64
//!
//! 费率、税率、折扣率在报表里既要参与计算,又要**原样展示**
//! (如「7.50%」「3 万分之 30」)。若用 `f64`:
//!
//! - `0.075` 在内部无法精确表示,累乘几次后展示成 `7.499999999%`;
//! - 无法回答「这个比率是多少个万分点」——而那正是报表要印的数字。
//!
//! 用整数万分比(`basis_points`)后,展示与计算共用同一个整数,
//! 展示时按整数除余拼串,**全程不出现浮点**。

use super::currency_amount::CurrencyAmount;

/// 万分比的分母(与金额缩放共用同一口径)。
pub const BASIS_POINTS_DENOMINATOR: i64 = 10_000;

/// 一个比率,以万分比(basis points)存储。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ratio {
    /// 比率数值:10000 表示 100%,1 表示 0.01%。
    basis_points: i64,
}

impl Ratio {
    /// 以万分比构造。
    ///
    /// 参数 `basis_points`:万分比数值(300 表示 3%)。
    pub const fn from_basis_points(basis_points: i64) -> Self {
        Ratio { basis_points }
    }

    /// 零比率。
    ///
    /// 为什么提供一个显式的 `ZERO` 构造而不是让调用方写
    /// `Ratio::from_basis_points(0)`:免税、免手续费这些**政策性的零**
    /// 值得有个名字;将来若零值需要附带含义(如「免税」标记),
    /// 只需改这一个构造点。
    pub const fn zero() -> Self {
        Ratio { basis_points: 0 }
    }

    /// 读取万分比数值。
    pub const fn basis_points(&self) -> i64 {
        self.basis_points
    }

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

    /// 判断该比率是否严格大于另一个比率。
    ///
    /// 用于「实际费率是否被上浮」这类核查(承运商燃油附加费的封顶比较)。
    pub const fn is_greater_than(&self, other: &Ratio) -> bool {
        self.basis_points > other.basis_points
    }

    /// 把一个金额按本比率缩放出新金额。
    ///
    /// 参数 `amount`:待缩放的金额。
    /// 返回:缩放后的金额。
    ///
    /// 本方法是**全工程唯一的比例缩放入口**:任何「乘以一个百分比」的运算
    /// 都必须走这里,包括零比率。这样将来变更舍入口径(例如改成
    /// 银行家舍入)只需改一处,不用去追散落各处的 `* 0.03`。
    pub fn apply_to(&self, amount: &CurrencyAmount) -> CurrencyAmount {
        amount.scale_by_basis_points(self.basis_points)
    }

    /// 返回百分比展示文本,如 `"7.50%"`。
    ///
    /// 保留 2 位小数:业务上费率很少需要比万分之一更细的粒度,
    /// 而 2 位小数(精确到万分之一)恰好与内部分母一致,不存在信息丢失。
    pub fn as_percent_text(&self) -> String {
        let absolute: i64 = self.basis_points.abs();
        // 万分比转百分比:除以 100 得整数部分,余数即小数部分。
        let whole_percent: i64 = absolute / 100;
        let fraction: i64 = absolute % 100;
        let sign: &str = if self.basis_points < 0 { "-" } else { "" };
        format!("{}{}.{:02}%", sign, whole_percent, fraction)
    }

    /// 返回万分比的原始数值展示,如 `"750 万分点"`。
    ///
    /// 报表里同时印百分比与万分点,是为了让对账同事能直接核对
    /// 「系统里存的整数」——只印百分比会让人怀疑中间是否被浮点污染。
    pub fn as_basis_points_text(&self) -> String {
        format!("{} 万分点", self.basis_points)
    }

    /// 把该比率转换为「每单位量的金额」,即乘法因子本身。
    ///
    /// 参数 `base_amount`:基数金额。
    /// 返回:比率对应金额(与 [`Ratio::apply_to`] 同义,命名更贴近业务读法)。
    ///
    /// 保留两个名字(`apply_to` / `of`)是有意的:
    /// 报价单里更自然的读法是「保费的 0.3%」,写成 `ratio.of(&premium)`
    /// 比 `ratio.apply_to(&premium)` 更贴近业务语言,能减少阅读时的翻译成本。
    pub fn of(&self, base_amount: &CurrencyAmount) -> CurrencyAmount {
        self.apply_to(base_amount)
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : service_tier.rs
//! 服务等级(开放型标签)。
//!
//! ## 为什么是开放型
//!
//! 承运商的服务等级会不断新增(标准、加急、当日达、经济小包……),
//! 且每个等级携带的是一组**数值参数**(时效倍数、附加费率、是否可承诺时刻)。
//! 承运子系统只读取这些参数参与计算,不做 `match` 分派,
//! 因此按「只被存/传/计算 → 开放型」的判据,它应该是结构体。
//!
//! ## 为什么把「时效倍数」也做成数据
//!
//! 若把「加急 = 0.5 倍时效」写死在承运子系统的 `match` 里,
//! 那么新增一个「次日达 = 0.3 倍」就必须改子系统代码。
//! 把倍数放进标签后,承运子系统只做 `标准时效 × 倍数` 这一件事,
//! 新增等级零代码改动。

/// 一种承运服务等级。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ServiceTier {
    /// 等级编码,如 `"EXPRESS"`。
    code: &'static str,
    /// 中文名称,如 `"加急"`。
    label: &'static str,
    /// 标准时效的倍数,以万分比表示(5000 = 0.5 倍时效)。
    transit_days_multiplier_basis_points: i64,
    /// 相对基础运费的附加费率(万分比,1200 = +12%)。
    surcharge_basis_points: i64,
    /// 是否承诺具体送达时刻(而非仅日期)。
    guarantees_exact_time: bool,
}

impl ServiceTier {
    /// 构造一个服务等级标签。
    pub const fn new(
        code: &'static str,
        label: &'static str,
        transit_days_multiplier_basis_points: i64,
        surcharge_basis_points: i64,
        guarantees_exact_time: bool,
    ) -> Self {
        ServiceTier {
            code,
            label,
            transit_days_multiplier_basis_points,
            surcharge_basis_points,
            guarantees_exact_time,
        }
    }

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

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

    /// 时效倍数(万分比)。10000 表示不变,5000 表示减半。
    pub const fn transit_days_multiplier_basis_points(&self) -> i64 {
        self.transit_days_multiplier_basis_points
    }

    /// 附加费率(万分比)。
    pub const fn surcharge_basis_points(&self) -> i64 {
        self.surcharge_basis_points
    }

    /// 是否承诺具体时刻。
    pub const fn guarantees_exact_time(&self) -> bool {
        self.guarantees_exact_time
    }
}

/// 标准服务:时效不变、无附加费、不承诺时刻。
pub const SERVICE_TIER_STANDARD: ServiceTier =
    ServiceTier::new("STANDARD", "标准", 10_000, 0, false);

/// 加急服务:时效 × 0.5、附加费 +12%、承诺时刻。
pub const SERVICE_TIER_EXPRESS: ServiceTier =
    ServiceTier::new("EXPRESS", "加急", 5_000, 1_200, true);

/// 经济服务:时效 × 1.5、附加费 −8%(负值表示折扣)、不承诺时刻。
pub const SERVICE_TIER_ECONOMY: ServiceTier =
    ServiceTier::new("ECONOMY", "经济", 15_000, -800, false);


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : shipping_weight.rs
//! 货物重量。
//!
//! ## 为什么用毫克
//!
//! 珠宝单件常以克表述(320 g),而物流计费以**公斤**为最小计费单位。
//! 若用克存,遇到「0.5 g」的金饰就要上浮点;若用公斤存,
//! 内部会充满 `0.00032` 这类值。
//!
//! 统一用**毫克**(i64)就可以覆盖从「毫克级宝石」到「吨级托盘」的全区间,
//! 且所有换算都是整数乘除。计费时再按「向上取整到公斤」处理,
//! 舍入点集中在一处,便于核算。

use super::currency_amount::divide_rounded_away_from_zero;

/// 一克包含的毫克数。
const MILLIGRAMS_PER_GRAM: i64 = 1_000;

/// 一公斤包含的毫克数。
const MILLIGRAMS_PER_KILOGRAM: i64 = 1_000_000;

/// 一件货物或一整票货物的重量,以毫克存储。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ShippingWeight {
    /// 重量数值,单位毫克。用 i64 是因为它同时要承载「单件 0.12 g」与「整车 32 t」。
    milligrams: i64,
}

impl ShippingWeight {
    /// 以毫克构造。
    pub const fn from_milligrams(milligrams: i64) -> Self {
        ShippingWeight { milligrams }
    }

    /// 以克构造(支持小数克,通过「克 + 毫克尾数」两个参数表达)。
    ///
    /// 参数 `grams`:克整数部分;`extra_milligrams`:不足一克的毫克尾数。
    ///
    /// 为什么要拆两个参数:`from_grams(320.5)` 会迫使调用方写浮点字面量,
    /// 而浮点在域层是禁忌。拆开后调用方写 `from_grams_and_milligrams(320, 500)`,
    /// 语义精确且无浮点。
    pub const fn from_grams_and_milligrams(grams: i64, extra_milligrams: i64) -> Self {
        ShippingWeight {
            milligrams: grams * MILLIGRAMS_PER_GRAM + extra_milligrams,
        }
    }

    /// 以整数克构造。
    pub const fn from_grams(grams: i64) -> Self {
        ShippingWeight {
            milligrams: grams * MILLIGRAMS_PER_GRAM,
        }
    }

    /// 零重量。
    pub const fn zero() -> Self {
        ShippingWeight { milligrams: 0 }
    }

    /// 读取毫克数。
    pub const fn milligrams(&self) -> i64 {
        self.milligrams
    }

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

    /// 是否严格大于另一个重量。
    ///
    /// 为什么不用 `PartialOrd` 的 `>`:派生出来的 `Ord` 已经能用,
    /// 但显式命名方法能让「承重上限校验」这类调用点读起来就是业务语言。
    pub const fn is_greater_than(&self, other: &ShippingWeight) -> bool {
        self.milligrams > other.milligrams
    }

    /// 相加。
    pub const fn add(&self, other: &ShippingWeight) -> Self {
        ShippingWeight {
            milligrams: self.milligrams.saturating_add(other.milligrams),
        }
    }

    /// 求和(对一个重量序列)。
    ///
    /// 参数 `weights`:重量切片。
    /// 返回:总重量。
    ///
    /// 放在本类型上而不是让调用方写 `iter().fold(...)`:
    /// 「整票总重」是各个子系统都要算的高频量(计费、承重校验、报关),
    /// 集中一处可避免各处累加口径不一致(如有的漏算了包装重量)。
    pub fn sum(weights: &[ShippingWeight]) -> Self {
        let mut total: i64 = 0;
        for weight in weights {
            total = total.saturating_add(weight.milligrams());
        }
        ShippingWeight {
            milligrams: total,
        }
    }

    /// 按「每公斤单价」计费。
    ///
    /// 参数 `rate_per_kilogram`:每公斤的费率(以最小单位表示,如分)。
    /// 返回:计费金额的最小单位数值(未套币种)。
    ///
    /// **关键取舍:计费重量是否向上取整到公斤**。
    /// 本函数使用「按实际重量等比计费」(不足一公斤按比例算),
    /// 因为珠宝样品单件的实际重量差异很大,向上取整会让 320 g 与 999 g
    /// 收一样的钱,演示上不直观。真实快递公司的「首重 + 续重」规则
    /// 属于**计价策略**,应由调用方传入费率来表达,不写死在本值对象里。
    ///
    /// 返回值刻意是裸 i64 而不是 `CurrencyAmount`:本层不引入币种假设,
    /// 由上层(承运子系统)决定这个数配上哪种币种。
    pub fn charge_at_rate_per_kilogram(&self, rate_per_kilogram: i64) -> i64 {
        // 中间量 i128:毫克 × 分 可达 1e14 量级,仍在 i64 内,
        // 但为避免将来换用更大单位时溢出,统一走 i128。
        let numerator: i128 = self.milligrams as i128 * rate_per_kilogram as i128;
        let rounded: i128 =
            divide_rounded_away_from_zero(numerator, MILLIGRAMS_PER_KILOGRAM as i128);
        super::currency_amount::clamp_i128_to_i64(rounded)
    }

    /// 返回以克为单位的展示文本(保留 3 位小数,即克 + 毫克)。
    ///
    /// 举例:`320500` 毫克 → `"320.500 g"`。
    /// 固定 3 位小数是为了让报表里的重量列宽度稳定,便于对齐。
    pub fn formatted_grams(&self) -> String {
        let absolute: i64 = self.milligrams.abs();
        let grams: i64 = absolute / MILLIGRAMS_PER_GRAM;
        let milligram_remainder: i64 = absolute % MILLIGRAMS_PER_GRAM;
        let sign: &str = if self.milligrams < 0 { "-" } else { "" };
        format!(
            "{}{}.{:03} g",
            sign, grams, milligram_remainder
        )
    }

    /// 返回以公斤为单位的展示文本(保留 3 位小数)。
    ///
    /// 举例:`1045000` 毫克 → `"1.045 kg"`。
    /// 保留 3 位小数意味着精确到克,这与「珠宝按克报价」的业务口径一致。
    pub fn formatted_kilograms(&self) -> String {
        let absolute: i64 = self.milligrams.abs();
        let kilograms: i64 = absolute / MILLIGRAMS_PER_KILOGRAM;
        // 剩余毫克换算成「公斤的小数部分」:克 * 1000 + 毫克,再补足 6 位。
        let remaining_milligrams: i64 = absolute % MILLIGRAMS_PER_KILOGRAM;
        // 把剩余毫克转成「千分之一公斤」单位:1 千分之一公斤 = 1000 毫克。
        let thousandths: i64 = remaining_milligrams / 1_000;
        let sign: &str = if self.milligrams < 0 { "-" } else { "" };
        format!("{}{}.{:03} kg", sign, kilograms, thousandths)
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : builtin_dispatch.rs
//! 内置调度端口:把 `dispatch` 子系统接到门面的 [`super::DispatchPort`] 契约上。
//!
//! # 适配器为什么要独立成文件
//!
//! 这个文件是**唯一**同时 import `dispatch` 与 `facade` 的地方。
//! 把适配职责集中在这里有两个好处:
//!
//! 1. 子系统内部文件完全不需要知道门面的存在,可独立测试与替换;
//! 2. 「门面依赖子系统」这条边的**具体位置**被压缩到少数几个适配器文件里,
//!    依赖脚本的输出因此非常干净:`dispatch` 不引用 `facade`,
//!    只有 `facade::builtin_*` 引用 `dispatch`。
//!
//! 这正是「严格分层」与「可替换」能同时成立的原因。

use std::cell::RefCell;

use crate::dispatch::capacity_ledger::CapacityLedger;
use crate::dispatch::carrier_option::CarrierOption;
use crate::dispatch::route_planner::{plan_route_legs, RoutePlanningRequest};

use crate::facade::shipping_facade::translate_carrier_option;
use crate::facade::subsystem_ports::{
    CapacityPort, CarrierOptionView, DispatchInput, DispatchPort,
};

/// 内置调度端口。
///
/// ## 为什么用 `RefCell<CapacityLedger>`
///
/// [`DispatchPort`] 的 `plan_options` 接收 `&self`(门面只需读能力,
/// 不应给实现方强加可变性要求)。但运力台账在规划过程中需要**占用运力**
/// (有状态变更)。用 `RefCell` 在内部实现「内部可变性」,
/// 就能在 `&self` 的方法里更新台账。
///
/// 这是**单线程**场景下的正当用法(本工程不涉及并发)。
/// 若将来需要多线程,应换成 `Mutex`,且 trait 方法签名不变——
/// 这正是把可变性藏在实现内部的好处。
pub struct BuiltinDispatchPort {
    /// 运力台账(内部可变)。
    ledger: RefCell<CapacityLedger>,
}

impl BuiltinDispatchPort {
    /// 用给定的运力台账构造端口。
    pub fn new(ledger: CapacityLedger) -> Self {
        BuiltinDispatchPort {
            ledger: RefCell::new(ledger),
        }
    }
}

impl DispatchPort for BuiltinDispatchPort {
    fn plan_options(&self, input: &DispatchInput) -> Vec<CarrierOptionView> {
        // 构造调度子系统的请求结构体。
        // 注意这里是「门面类型 → 子系统类型」的方向,
        // 属于适配器应有的翻译职责。
        let request = RoutePlanningRequest {
            origin_city: input.origin_city,
            origin_country: input.origin_country,
            destination_city: input.destination_city,
            destination_country: input.destination_country,
            total_weight: input.chargeable_weight,
            requires_temperature_control: input.requires_temperature_control,
            service_tier: input.service_tier,
        };

        // 借用内部台账(可变)执行规划。
        // 若在同一线程中发生重入借用,这里会 panic——本工程不会出现,
        // 因为门面不会在规划过程中再次调用本端口。
        let mut ledger_borrow = self.ledger.borrow_mut();
        let planning_result = plan_route_legs(&request, &mut ledger_borrow);

        // 把子系统的 CarrierOption 翻译成中立视图。
        planning_result
            .options
            .iter()
            .map(translate_carrier_option)
            .collect()
    }
}

/// 内置运力查询端口。
///
/// ## 为什么与 [`BuiltinDispatchPort`] 分开
///
/// 因为它们的**生命周期需求不同**:查询运力是只读的,
/// 而 [`BuiltinDispatchPort`] 持有 `RefCell` 内部可变状态。
/// 拆开后,只读场景不必触碰可变状态。
///
/// 代价是两者必须共享同一份台账。本实现的做法是:
/// 查询端口**不持有台账**,而是由门面在构造时传入同一个台账的共享引用。
/// 但 `CapacityLedger` 不是 `Rc`,为保持简单,
/// 本工程让查询端口也持有一份台账快照式的独立实例——
/// 见下方 `utilization` 字段的说明。
pub struct BuiltinCapacityPort {
    /// 台账快照:记录构造时各承运商的占用率(万分比)。
    /// 用「快照」而不是共享台账,避免为只读查询引入 `Rc<RefCell<..>>`
    /// 这类共享可变状态(那是环状引用的高发地,见技能里的行为型陷阱)。
    utilization_snapshot: Vec<(&'static str, i64)>,
}

impl BuiltinCapacityPort {
    /// 用给定的占用率快照构造端口。
    ///
    /// 参数 `utilization_snapshot`:(承运商代码, 占用率万分比)列表。
    pub fn new(utilization_snapshot: Vec<(&'static str, i64)>) -> Self {
        BuiltinCapacityPort {
            utilization_snapshot,
        }
    }

    /// 从台账构造端口(读取当前占用率作为快照)。
    ///
    /// 参数 `ledger`:运力台账。
    /// 返回:查询端口。
    pub fn from_ledger(ledger: &CapacityLedger) -> Self {
        let snapshot: Vec<(&'static str, i64)> = ledger
            .snapshot()
            .iter()
            .map(|(carrier_code, _occupied, _limit)| {
                let utilization: i64 = ledger
                    .utilization_basis_points(carrier_code)
                    .unwrap_or(0);
                (*carrier_code, utilization)
            })
            .collect();
        BuiltinCapacityPort::new(snapshot)
    }
}

impl CapacityPort for BuiltinCapacityPort {
    fn utilization_basis_points(&self, carrier_code: &str) -> i64 {
        // 查不到即返回 0:未知承运商视为「无占用信息」,
        // 而不是返回一个会触发误报的高值。
        self.utilization_snapshot
            .iter()
            .find(|(code, _)| *code == carrier_code)
            .map(|(_, utilization)| *utilization)
            .unwrap_or(0)
    }
}

/// 一个辅助函数:把 `CarrierOption` 的列表一次性翻译成视图。
///
/// 参数 `options`:承运方案列表。
/// 返回:中立视图列表。
///
/// 提取出来是为了让工程外实现也能复用同一套翻译,
/// 而不必自己重写(那会导致口径漂移)。
pub fn translate_options(options: &[CarrierOption]) -> Vec<CarrierOptionView> {
    options.iter().map(translate_carrier_option).collect()
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : builtin_ports.rs
//! 内置包装 / 单据 / 承运 / 结算端口。
//!
//! ## 为什么四个适配器放在同一个文件
//!
//! 它们都极薄:把门面的中立输入翻译成子系统请求、调用、再把结果翻译回中立视图。
//! 每个大约十行。拆成四个文件会让导航成本超过收益。
//!
//! 而 [`super::builtin_dispatch`] 单独成文件,是因为它引入了
//! `RefCell` 内部可变状态——那值得一份独立的文档说明。
//! 文件划分依据是「职责与复杂度」,不是「角色的数量」。

use crate::carrier::timeline_builder::{build_timeline, TimelineRequest};
use crate::documents::document_compiler::{
    compile_document_checklist, DocumentRequest,
};
use crate::packaging::packaging_planner::{plan_packaging, PackagingRequest};
use crate::settlement::settlement_ledger::settle_charges;

use crate::facade::shipping_facade::{
    translate_document_requirement, translate_packaging_plan, translate_settlement,
    translate_timeline,
};
use crate::facade::subsystem_ports::{
    DocumentCompilationPort, DocumentInput, DocumentRequirementView, PackagingInput,
    PackagingPlanView, PackagingPort, SettlementPort, SettlementView, TimelineInput, TimelinePort,
    TimelineView,
};

/// 内置包装端口。
///
/// 无状态(包装规划是纯函数),因此是单元结构体。
/// 单元结构体(`struct X;`)而非「带空字段的结构体」:
/// 前者在类型层面就说明「没有状态」,读者不必去找有没有隐藏字段。
pub struct BuiltinPackagingPort;

impl PackagingPort for BuiltinPackagingPort {
    fn plan_packaging(&self, input: &PackagingInput) -> PackagingPlanView {
        let request = PackagingRequest {
            jewelry_piece_count: input.jewelry_piece_count,
            other_piece_count: input.other_piece_count,
            requires_temperature_control: input.requires_temperature_control,
            net_cargo_weight: input.net_cargo_weight,
        };
        // 调用子系统并翻译结果。
        let plan = plan_packaging(&request);
        translate_packaging_plan(&plan)
    }
}

/// 内置单据编成端口。
pub struct BuiltinDocumentPort;

impl DocumentCompilationPort for BuiltinDocumentPort {
    fn compile_checklist(&self, input: &DocumentInput) -> Vec<DocumentRequirementView> {
        let request = DocumentRequest {
            is_cross_border: input.is_cross_border,
            requires_temperature_control: input.requires_temperature_control,
            has_declarable_cargo: input.has_declarable_cargo,
            has_high_value_cargo: input.has_high_value_cargo,
            destination_country_code: input.destination_country_code,
            destination_requires_customs_declaration: input.destination_requires_customs_declaration,
        };
        // 内置端口不追加额外规则:传空切片。
        // 工程外实现可在这里传入自定义规则,这是「扩展不侵入」的口子。
        let checklist = compile_document_checklist(&request, &[]);
        checklist
            .requirements()
            .iter()
            .map(translate_document_requirement)
            .collect()
    }
}

/// 内置承运(时间线)端口。
pub struct BuiltinTimelinePort;

impl TimelinePort for BuiltinTimelinePort {
    fn build_timeline_view(&self, input: &TimelineInput) -> TimelineView {
        let request = TimelineRequest {
            dispatch_date: input.dispatch_date,
            route_leg_days: input.route_leg_days.clone(),
            route_leg_is_cross_border: input.route_leg_is_cross_border.clone(),
            destination_city: input.destination_city,
        };
        let timeline = build_timeline(&request);
        translate_timeline(&timeline)
    }
}

/// 内置结算端口。
pub struct BuiltinSettlementPort;

impl SettlementPort for BuiltinSettlementPort {
    fn settle(&self, items: Vec<super::subsystem_ports::SettlementInputItem>) -> SettlementView {
        // 中立输入 → 子系统费用项,需要把来源字符串映射回枚举。
        // 这个映射是适配器的职责;门面无需知道枚举存在。
        let charge_items: Vec<crate::settlement::charge_item::ChargeItem> = items
            .iter()
            .map(|item| {
                crate::settlement::charge_item::ChargeItem::new(
                    item.code,
                    item.label,
                    source_from_code(item.source_code),
                    item.amount,
                    item.basis_text,
                )
            })
            .collect();
        let result = settle_charges(charge_items);
        translate_settlement(&result)
    }
}

/// 把来源编码字符串映射回 [`crate::settlement::ChargeSource`]。
///
/// 参数 `source_code`:来源编码(如 `"FREIGHT"`)。
/// 返回:对应的来源枚举。
///
/// ## 未知编码如何处理(重要取舍)
///
/// **回退到增值服务费**而不是 panic 或 `Option`。理由:
/// - panic 会让门面因一个陌生的费用来源而整体崩溃,不可接受;
/// - `Option` 会迫使调用方写 `unwrap`,反而把处理责任推给了不需要关心的地方;
/// - 回退到「增值服务费」在报表上是**可见的**(会出现在服务费一栏),
///   运营能立刻看出「这条费用归类可能不对」,
///   而不会像静默丢弃那样无声无息。
///
/// 这是一次刻意的「宽松但可见」选择。
fn source_from_code(source_code: &str) -> crate::settlement::charge_item::ChargeSource {
    match source_code {
        "FREIGHT" => crate::settlement::charge_item::ChargeSource::Freight,
        "PACKAGING" => crate::settlement::charge_item::ChargeSource::Packaging,
        "DOCUMENTATION" => crate::settlement::charge_item::ChargeSource::Documentation,
        // 其余(含 VALUE_ADDED_SERVICE 与任何未知编码)归入增值服务费。
        _ => crate::settlement::charge_item::ChargeSource::ValueAddedService,
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : shipment_request.rs
//! 门面的输入:一票运输需求。
//!
//! ## 为什么门面要定义自己的输入类型,而不是直接收各子系统的参数
//!
//! 若门面签名是 `plan(origin, destination, weight, pieces, temp, tier, ...)`,
//! 那么「新增一个考虑因素」就要改门面签名,所有调用点跟着改。
//! 收一个请求结构体后,扩展只是给结构体加一个字段。
//!
//! 更重要的是:**这个请求类型属于门面,不属于任何子系统**。
//! 各子系统只看到自己需要的那部分(门面负责翻译),
//! 因此门面的输入结构演化不会传染给子系统。

use crate::domain::{CargoCategory, CountryCode, ServiceTier, ShippingWeight};

/// 货物清单中的一行。
///
/// 注意这里用 `CargoCategory`(开放型标签)而不是枚举:
/// 门面要能接受工程外新增的品类,若用枚举,
/// 「新增品类」就会变成「改门面的输入类型」,扩展性立刻丧失。
#[derive(Debug, Clone, Copy)]
pub struct CargoLine {
    /// 货物名称(如「翡翠手镯」)。
    pub name: &'static str,
    /// 货物类别(开放型标签)。
    pub category: CargoCategory,
    /// 件数。
    pub quantity: i64,
    /// 单件重量。
    pub unit_weight: ShippingWeight,
    /// 单件申报价值(最小单位,分)。
    pub unit_declared_value_minor_units: i64,
}

impl CargoLine {
    /// 构造一行货物。
    pub const fn new(
        name: &'static str,
        category: CargoCategory,
        quantity: i64,
        unit_weight: ShippingWeight,
        unit_declared_value_minor_units: i64,
    ) -> Self {
        CargoLine {
            name,
            category,
            quantity,
            unit_weight,
            unit_declared_value_minor_units,
        }
    }

    /// 该行总重量。
    pub const fn total_weight(&self) -> ShippingWeight {
        ShippingWeight::from_milligrams(self.unit_weight.milligrams())
    }

    /// 该行总重量(含件数)。
    ///
    /// 与 [`CargoLine::total_weight`] 的区别:本方法乘件数。
    /// 两个方法都存在是因为「单件重量」与「行总重」在报表里都要展示,
    /// 显式区分比让调用方自己乘更不容易出错。
    pub fn line_total_weight(&self) -> ShippingWeight {
        // 先算单件毫克 × 件数(i64 乘法),再构造。
        let total_milligrams: i64 = self
            .unit_weight
            .milligrams()
            .saturating_mul(self.quantity);
        ShippingWeight::from_milligrams(total_milligrams)
    }

    /// 该行总申报价值(最小单位)。
    pub fn line_total_declared_value_minor_units(&self) -> i64 {
        self.unit_declared_value_minor_units
            .saturating_mul(self.quantity)
    }
}

/// 一票运输需求(门面的输入)。
#[derive(Debug, Clone)]
pub struct ShipmentRequest {
    /// 运单业务号(用于派生各类单号;由调用方提供以保证可追溯)。
    pub shipment_reference: &'static str,
    /// 寄件方名称。
    pub sender_name: &'static str,
    /// 寄件方国家/地区。
    pub sender_country: CountryCode,
    /// 寄件方城市。
    pub sender_city: &'static str,
    /// 收件方名称。
    pub receiver_name: &'static str,
    /// 收件方国家/地区。
    pub receiver_country: CountryCode,
    /// 收件方城市。
    pub receiver_city: &'static str,
    /// 计划发货日。
    pub planned_dispatch_date: crate::support::calendar_date::CalendarDate,
    /// 服务等级。
    pub service_tier: ServiceTier,
    /// 是否需要温控。
    pub requires_temperature_control: bool,
    /// 是否要求投保价险。
    pub requires_insurance: bool,
    /// 是否要求拆箱查验。
    pub requires_inspection: bool,
    /// 货物清单。
    pub cargo_lines: Vec<CargoLine>,
}

impl ShipmentRequest {
    /// 货物总件数。
    pub fn total_piece_count(&self) -> i64 {
        let mut total: i64 = 0;
        for line in &self.cargo_lines {
            total = total.saturating_add(line.quantity);
        }
        total
    }

    /// 货物净重(不含包装)。
    pub fn net_cargo_weight(&self) -> ShippingWeight {
        let mut total_milligrams: i64 = 0;
        for line in &self.cargo_lines {
            total_milligrams =
                total_milligrams.saturating_add(line.line_total_weight().milligrams());
        }
        ShippingWeight::from_milligrams(total_milligrams)
    }

    /// 申报总价值(最小单位)。
    pub fn total_declared_value_minor_units(&self) -> i64 {
        let mut total: i64 = 0;
        for line in &self.cargo_lines {
            total = total.saturating_add(line.line_total_declared_value_minor_units());
        }
        total
    }

    /// 珠宝类件数(用于包装规划:决定用多少个硬质礼盒)。
    pub fn jewelry_piece_count(&self) -> i64 {
        let mut total: i64 = 0;
        for line in &self.cargo_lines {
            // 只统计「需要正式报关」的高值珠宝类。
            // 这里用品类编码判断而非 `==` 比较对象,
            // 使工程外新增的同类品类也能被正确归类。
            if crate::packaging::packaging_planner::requires_rigid_packaging(line.category.code()) {
                total = total.saturating_add(line.quantity);
            }
        }
        total
    }

    /// 其他(非珠宝)件数。
    pub fn other_piece_count(&self) -> i64 {
        // 总件数减珠宝件数,保证两者之和恒等于总件数(不重不漏)。
        self.total_piece_count() - self.jewelry_piece_count()
    }

    /// 是否包含需要正式报关的品类。
    pub fn has_declarable_cargo(&self) -> bool {
        self.cargo_lines
            .iter()
            .any(|line| line.category.requires_formal_declaration())
    }

    /// 是否包含高值货(申报总价值达到阈值)。
    ///
    /// 阈值取自 [`HIGH_VALUE_THRESHOLD_MINOR_UNITS`],
    /// 之所以做成常量而不是散落的字面量,是为了让报表文案与判断逻辑
    /// 引用同一个数,不会出现「判断用 5 万、文案写 8 万」的不一致。
    pub fn has_high_value_cargo(&self) -> bool {
        self.total_declared_value_minor_units() >= HIGH_VALUE_THRESHOLD_MINOR_UNITS
    }

    /// 是否为跨境运输。
    pub fn is_cross_border(&self) -> bool {
        self.sender_country.code() != self.receiver_country.code()
    }
}

/// 高值货判定阈值(最小单位:分),即 ¥50,000.00。
///
/// 放在模块级而不是塞进方法体:单据子系统与报表都要引用它,
/// 集中一处才能保证「判断」与「文案」永远一致。
pub const HIGH_VALUE_THRESHOLD_MINOR_UNITS: i64 = 5_000_000;


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : shipping_facade.rs
//! # ShippingFacade —— 门面本体
//!
//! ## 它做的唯一一件事
//!
//! 把「一票运输需求」变成「一份可执行的装配结果」,
//! 中间按正确顺序协调五个子系统,并把它们的输出**翻译**成门面自己的视图类型。
//!
//! ## 为什么门面的核心方法很长(一个方法几百行)
//!
//! 门面的「编排顺序」本身就是它的核心知识,拆成多个私有方法反而会
//! 把「先包装、再按包装后实测重量去调度、最后结算」这个**顺序约束**
//! 打散到多个地方,读者要跳来跳去才能拼出全貌。
//!
//! 因此这里刻意保留一条**线性叙事的主流程**,只在真正的独立计算上
//! 拆出私有辅助方法(如事件汇聚、单据翻译)。可读性优先于方法长度。
//!
//! ## 门面持有什么
//!
//! 五个**端口特征对象**(`Box<dyn ...Port>`)+ 一个运力台账端口。
//! 它**不持有**任何具体子系统类型——这是「子系统可整体替换」的结构保证。
//! 具体绑定发生在构造处([`ShippingFacade::with_default_subsystems`]),
//! 而不在这个文件里硬编码。

use crate::carrier::delivery_rules::adjust_dispatch_date_for_weekend;
use crate::dispatch::CarrierOption;
use crate::documents::document_checklist::DocumentRequirement;
use crate::domain::{
    CurrencyAmount, EventSeverity, Ratio, CURRENCY_CHINESE_YUAN,
};
use crate::packaging::packaging_plan::PackagingPlan;
use crate::settlement::charge_item::{ChargeItem, ChargeSource};
use crate::support::deterministic_code::{build_seed, derive_code, short_fingerprint};

use super::shipment_request::{ShipmentRequest, HIGH_VALUE_THRESHOLD_MINOR_UNITS};
use super::shipping_result::{
    DocumentView, FacadeEvent, PackagingView, RouteLegView, ShipmentOutcome, ShipmentTimelineView,
    ShippingResult,
};
use super::subsystem_ports::{
    CapacityPort, CarrierOptionView, DispatchInput, DispatchPort, DocumentCompilationPort,
    DocumentInput, DocumentRequirementView, PackagingInput, PackagingPort, SettlementInputItem,
    SettlementPort, SettlementView, TimelineInput, TimelinePort,
};

/// 运费中「单据工本费」的每张单价(最小单位:分),¥12.00。
///
/// 这是一个**门面层面的商务参数**——它不属于任何一个子系统:
/// 单据子系统知道「要哪些单」,但不该知道「每张收多少钱」(那是定价)。
/// 放在门面里,意味着调价只需改门面这一处。
const DOCUMENT_ISSUANCE_FEE_MINOR_UNITS: i64 = 1_200;

/// 保价费率:万分之 30(即 0.30%)。
///
/// 同样属于门面的商务参数。用 [`Ratio`] 表达而非字面量,
/// 是为了走全工程统一的缩放路径(含统一的舍入口径)。
const INSURANCE_RATE_BASIS_POINTS: i64 = 30;

/// 拆箱查验费:每件 ¥35.00。
const INSPECTION_FEE_PER_PIECE_MINOR_UNITS: i64 = 3_500;

/// 运力占用率超过此值时,门面追加一条「运力紧张」提醒(万分比,8500 = 85%)。
///
/// 注意这条规则**是门面的规则**而不是子系统的:
/// 子系统只负责报出占用率,至于「多少算紧张」是门面对整体风险的判断。
/// 把判断留在门面,各家承运商就可以用同一份子系统实现,
/// 而门面按自己的风控口径决定提醒与否。
const CAPACITY_TIGHTNESS_THRESHOLD_BASIS_POINTS: i64 = 8_500;

/// 门面:协调五个子系统完成一票运单的装配。
pub struct ShippingFacade {
    /// 调度端口。
    dispatch_port: Box<dyn DispatchPort>,
    /// 包装端口。
    packaging_port: Box<dyn PackagingPort>,
    /// 单据端口。
    document_port: Box<dyn DocumentCompilationPort>,
    /// 承运(时间线)端口。
    timeline_port: Box<dyn TimelinePort>,
    /// 结算端口。
    settlement_port: Box<dyn SettlementPort>,
    /// 运力台账端口。
    capacity_port: Box<dyn CapacityPort>,
}

impl ShippingFacade {
    /// 用一组端口构造门面。
    ///
    /// 参数为六个端口实现。
    ///
    /// ## 为什么构造参数这么多却不收一个「端口包」结构体
    ///
    /// 收一个结构体看起来更整洁,但那会引入一个「必须与门面同步演化」的类型。
    /// 六个参数虽多,却让「门面依赖哪些能力」在签名上一览无余——
    /// 这对一个示范工程来说比少写几个参数更有价值。
    /// 调用方若嫌麻烦,可自行封装一个工厂(本工程在
    /// [`ShippingFacade::with_default_subsystems`] 里做了示范)。
    pub fn new(
        dispatch_port: Box<dyn DispatchPort>,
        packaging_port: Box<dyn PackagingPort>,
        document_port: Box<dyn DocumentCompilationPort>,
        timeline_port: Box<dyn TimelinePort>,
        settlement_port: Box<dyn SettlementPort>,
        capacity_port: Box<dyn CapacityPort>,
    ) -> Self {
        ShippingFacade {
            dispatch_port,
            packaging_port,
            document_port,
            timeline_port,
            settlement_port,
            capacity_port,
        }
    }

    /// 用本工程内置的五个子系统构造门面(便捷构造)。
    ///
    /// 返回:接好内置子系统的门面。
    ///
    /// 这个函数是**唯一**知道「内置子系统具体是什么类型」的地方。
    /// 若把它删掉,本文件的其余部分对具体子系统一无所知——
    /// 这正是我们想要的结构。
    pub fn with_default_subsystems(ledger: crate::dispatch::CapacityLedger) -> Self {
        // 先用台账构造调度端口(它会占用运力),再据此生成运力快照端口。
        let dispatch_port = crate::facade::builtin_dispatch::BuiltinDispatchPort::new(ledger);
        ShippingFacade::new(
            Box::new(dispatch_port),
            Box::new(crate::facade::builtin_ports::BuiltinPackagingPort),
            Box::new(crate::facade::builtin_ports::BuiltinDocumentPort),
            Box::new(crate::facade::builtin_ports::BuiltinTimelinePort),
            Box::new(crate::facade::builtin_ports::BuiltinSettlementPort),
            // 运力快照端口:本工程演示中由调用方另行注入以体现「运力紧张」,
            // 这里给一个空快照作为默认(查询任何承运商都得到 0 占用率)。
            Box::new(crate::facade::builtin_dispatch::BuiltinCapacityPort::new(
                Vec::new(),
            )),
        )
    }

    /// 用内置子系统构造门面,并显式注入运力快照。
    ///
    /// 参数 `ledger`:运力台账;`capacity_port`:运力查询端口。
    /// 返回:门面。
    ///
    /// 与 [`ShippingFacade::with_default_subsystems`] 的区别:
    /// 本函数允许调用方传入**自定义的运力快照**,
    /// 用于演示「同一套子系统在不同运力状况下给出不同提醒」。
    /// 这正是端口化带来的灵活性——换一个端口实现,门面代码零改动。
    pub fn with_capacity_snapshot(
        ledger: crate::dispatch::CapacityLedger,
        capacity_port: Box<dyn CapacityPort>,
    ) -> Self {
        ShippingFacade::new(
            Box::new(crate::facade::builtin_dispatch::BuiltinDispatchPort::new(ledger)),
            Box::new(crate::facade::builtin_ports::BuiltinPackagingPort),
            Box::new(crate::facade::builtin_ports::BuiltinDocumentPort),
            Box::new(crate::facade::builtin_ports::BuiltinTimelinePort),
            Box::new(crate::facade::builtin_ports::BuiltinSettlementPort),
            capacity_port,
        )
    }

    /// 装配一票运单(门面的核心用例方法)。
    ///
    /// 参数 `request`:运输需求。
    /// 返回:装配结论(成功或拒绝,均携带完整结果)。
    ///
    /// ## 编排顺序(这是门面的核心知识,顺序不可随意调换)
    ///
    /// 1. **包装先行**:因为包装会增重,而调度必须按「含包装的重量」计费,
    ///    否则运费会少算。若先调度再包装,就得重新调度一次——
    ///    那就说明顺序设计错了。
    /// 2. **调度**:拿到候选方案,选首个可行者。
    /// 3. **时间线**:需要调度给出的路由段天数才能推算。
    /// 4. **单据**:需要「是否跨境」「是否有可申报品类」等信息,
    ///    而这些在调度完成后才是确定的(路由决定了跨境与否)。
    /// 5. **结算**:需要前面所有环节的费用项。
    /// 6. **事件汇聚**:把各环节发现的问题统一成事件列表。
    ///
    /// 每一步之间没有反向依赖,因此这条链可以线性走完。
    pub fn plan_shipment(&self, request: &ShipmentRequest) -> ShipmentOutcome {
        // ---------- 步骤 0:基础量(净重、申报价值、件数) ----------
        let net_cargo_weight = request.net_cargo_weight();
        let total_declared_value_minor_units = request.total_declared_value_minor_units();
        let total_declared_value: CurrencyAmount = CurrencyAmount::from_minor_units(
            total_declared_value_minor_units,
            CURRENCY_CHINESE_YUAN,
        );

        // ---------- 步骤 1:包装(先于调度,见方法文档) ----------
        let packaging_view = self.packaging_port.plan_packaging(&PackagingInput {
            jewelry_piece_count: request.jewelry_piece_count(),
            other_piece_count: request.other_piece_count(),
            requires_temperature_control: request.requires_temperature_control,
            net_cargo_weight,
        });

        // ---------- 步骤 2:调度(按含包装重量计费) ----------
        let dispatch_input = DispatchInput {
            origin_city: request.sender_city,
            origin_country: request.sender_country,
            destination_city: request.receiver_city,
            destination_country: request.receiver_country,
            chargeable_weight: packaging_view.total_weight_with_packaging,
            requires_temperature_control: request.requires_temperature_control,
            service_tier: request.service_tier,
        };
        let candidate_options: Vec<CarrierOptionView> =
            self.dispatch_port.plan_options(&dispatch_input);

        // 选首个可行方案。端口实现约定「可行优先、报价升序」,
        // 因此这里取首个可行者即最优可行方案。
        let chosen_option: Option<CarrierOptionView> = candidate_options
            .iter()
            .find(|option| option.is_feasible)
            .cloned();

        // 若没有任何可行方案,仍要产出一份完整结果(含事件),
        // 而不是中途 return——门面的重要契约是「永远给出可解释的结论」。
        // 这里用一个「占位方案」承载后续计算所需的形状。
        let option: CarrierOptionView = match chosen_option {
            Some(found) => found,
            None => self.build_placeholder_option(&candidate_options, &dispatch_input),
        };

        // ---------- 步骤 3:时间线 ----------
        let timeline_view = self.timeline_port.build_timeline_view(&TimelineInput {
            dispatch_date: request.planned_dispatch_date,
            route_leg_days: option.route_leg_days.clone(),
            route_leg_is_cross_border: option.route_leg_is_cross_border.clone(),
            destination_city: request.receiver_city,
        });

        // ---------- 步骤 4:单据 ----------
        let is_cross_border: bool = option.route_leg_is_cross_border.iter().any(|flag| *flag);
        let document_requirement_views: Vec<DocumentRequirementView> =
            self.document_port.compile_checklist(&DocumentInput {
                is_cross_border,
                requires_temperature_control: request.requires_temperature_control,
                has_declarable_cargo: request.has_declarable_cargo(),
                has_high_value_cargo: request.has_high_value_cargo(),
                destination_country_code: request.receiver_country.code(),
                destination_requires_customs_declaration: request
                    .receiver_country
                    .requires_customs_declaration(),
            });

        // ---------- 步骤 5:结算(归集各来源费用) ----------
        let settlement_input_items: Vec<SettlementInputItem> = self.build_charge_items(
            request,
            &packaging_view,
            &option,
            &document_requirement_views,
            &total_declared_value,
        );
        let settlement_view: SettlementView = self.settlement_port.settle(settlement_input_items);

        // ---------- 步骤 6:事件汇聚 ----------
        let events: Vec<FacadeEvent> = self.collect_events(
            request,
            &option,
            &candidate_options,
            &packaging_view,
            &document_requirement_views,
            &settlement_view,
            total_declared_value_minor_units,
        );

        // ---------- 步骤 7:派生标识 ----------
        // 运单号由「业务号 + 收件方 + 承运商」派生:三者任一变化即换号,
        // 且同一输入永远得到同一个号(演示可复现)。
        let waybill_seed: String = build_seed(&[
            request.shipment_reference,
            request.receiver_name,
            request.receiver_city,
            option.carrier_code,
        ]);
        let waybill_number: String = derive_code("WB", &waybill_seed, 12);
        // 指纹用于报表比对同一票货的不同阶段快照。
        let fingerprint: String = short_fingerprint(&build_seed(&[
            request.shipment_reference,
            &option.carrier_code,
            &total_declared_value_minor_units.to_string(),
        ]));

        // ---------- 步骤 8:组装结果 ----------
        let result = ShippingResult {
            shipment_reference: request.shipment_reference,
            waybill_number,
            fingerprint,
            sender_text: format!(
                "{}({} {} · {})",
                request.sender_name,
                request.sender_country.code(),
                request.sender_country.label(),
                request.sender_city
            ),
            receiver_text: format!(
                "{}({} {} · {})",
                request.receiver_name,
                request.receiver_country.code(),
                request.receiver_country.label(),
                request.receiver_city
            ),
            carrier_code: option.carrier_code,
            carrier_label: option.carrier_label,
            service_tier_label: request.service_tier.label(),
            route_summary: option.route_summary.clone(),
            route_legs: self.translate_route_legs(&option),
            packaging: PackagingView {
                material_summary: self.summarize_material_lines(&packaging_view),
                material_kind_count: packaging_view.material_lines.len(),
                total_material_cost: packaging_view.total_material_cost,
                weight_gain: packaging_view.weight_gain,
                total_weight_with_packaging: packaging_view.total_weight_with_packaging,
                uses_insulating_material: packaging_view.uses_insulating_material,
            },
            documents: document_requirement_views
                .iter()
                .map(|requirement| DocumentView {
                    kind: requirement.kind,
                    status_label: requirement.status_label,
                    is_mandatory: requirement.is_mandatory,
                    blocks_dispatch: requirement.blocks_dispatch,
                    trigger_reason: requirement.trigger_reason,
                })
                .collect(),
            timeline: ShipmentTimelineView {
                milestones: timeline_view
                    .milestones
                    .iter()
                    .map(|milestone| {
                        (milestone.code, milestone.label, milestone.date, milestone.note)
                    })
                    .collect(),
                estimated_delivery_date: timeline_view.estimated_delivery_date,
                delayed_by_weekend: timeline_view.delayed_by_weekend,
            },
            charge_items: settlement_view
                .items
                .iter()
                .map(|item| {
                    (
                        item.code,
                        item.label,
                        item.source_label,
                        item.amount,
                        item.basis_text,
                    )
                })
                .collect(),
            grand_total: settlement_view.grand_total,
            largest_item_code: settlement_view.largest_item_code,
            cargo_lines: request
                .cargo_lines
                .iter()
                .map(|line| {
                    (
                        line.name,
                        line.category.label(),
                        line.quantity,
                        line.unit_weight,
                        CurrencyAmount::from_minor_units(
                            line.unit_declared_value_minor_units,
                            CURRENCY_CHINESE_YUAN,
                        ),
                    )
                })
                .collect(),
            events,
            net_cargo_weight,
            total_declared_value,
        };

        // 按事件是否阻断决定返回形态。
        if result.is_blocked() {
            ShipmentOutcome::Rejected {
                reason: format!(
                    "存在 {} 条严重级问题,装配已拒绝",
                    result.blocker_count()
                ),
                result: Box::new(result),
            }
        } else {
            ShipmentOutcome::Planned(Box::new(result))
        }
    }

    /// 在没有可行方案时构造一个「占位方案」。
    ///
    /// 参数 `candidates`:全部候选(可能为空);`input`:调度输入。
    /// 返回:可承载后续计算(时间线、结算)的占位方案。
    ///
    /// 为什么要占位而不是直接返回错误:
    /// 门面的契约是「无论成败都给出一份完整、可解释的结果」。
    /// 若中途返回,调用方就拿不到「为什么不行」「其他费用是多少」,
    /// 运营只能看到一个空白页。占位方案让所有后续步骤仍能执行,
    /// 报表上会把「无可行承运方案」作为一条严重级事件明确列出。
    fn build_placeholder_option(
        &self,
        candidates: &[CarrierOptionView],
        input: &DispatchInput,
    ) -> CarrierOptionView {
        // 优先用第一个候选(即使不可行)的信息,因为它的路由与报价
        // 对用户仍有参考价值(「最接近可行的那一档差多少」)。
        if let Some(first) = candidates.first() {
            return first.clone();
        }
        // 连候选都没有(端口实现完全给不出方案):构造一个空壳。
        // 此时路由段为空,时间线会退化为「发货日即达(含周末顺延)」。
        let _ = self.capacity_port.utilization_basis_points("");
        CarrierOptionView {
            carrier_code: "NONE",
            carrier_label: "无可用承运商",
            freight_charge: CurrencyAmount::zero(CURRENCY_CHINESE_YUAN),
            route_leg_days: Vec::new(),
            route_leg_is_cross_border: Vec::new(),
            route_summary: "(无路由)".to_string(),
            visited_country_codes: vec![input.origin_country.code()],
            is_feasible: false,
            infeasibility_reason: "端口未返回任何候选承运方案",
        }
    }

    /// 把调度返回的路由段信息翻译成门面的 [`RouteLegView]` 列表。
    ///
    /// 参数 `option`:承运方案中立视图。
    /// 返回:路由段视图列表。
    ///
    /// 注意这里只能重建「天数」与「是否跨境」,因为端口契约刻意只传了这些。
    /// **城市名等信息不在契约里**——若报表需要,应向端口契约追加字段,
    /// 而不是让门面去猜。这个限制是有意的:它逼着契约承担起表达力责任,
    /// 而不是让门面偷偷依赖实现细节。
    fn translate_route_legs(&self, option: &CarrierOptionView) -> Vec<RouteLegView> {
        let mut views: Vec<RouteLegView> = Vec::with_capacity(option.route_leg_days.len());
        // 途经国家列表来自契约,用于给每个段标注起终点国家。
        let visited: &[&'static str] = &option.visited_country_codes;
        for (index, days) in option.route_leg_days.iter().enumerate() {
            let is_cross_border: bool = option
                .route_leg_is_cross_border
                .get(index)
                .copied()
                .unwrap_or(false);
            // 起点国家取途经列表中的第 index 个(若不足则回退到第一个)。
            let origin_country_code: &'static str = visited
                .get(index)
                .copied()
                .or_else(|| visited.first().copied())
                .unwrap_or("--");
            // 终点国家取下一个(若已到末尾则取最后一个)。
            let destination_country_code: &'static str = visited
                .get(index + 1)
                .copied()
                .or_else(|| visited.last().copied())
                .unwrap_or("--");
            views.push(RouteLegView {
                sequence_number: (index as u32) + 1,
                // 城市名不在端口契约内,用序号化描述代替,避免编造数据。
                origin_city: if index == 0 { "起运地" } else { "中转地" },
                origin_country_code,
                destination_city: if index + 1 == option.route_leg_days.len() {
                    "目的地"
                } else {
                    "中转地"
                },
                destination_country_code,
                transport_mode_label: if is_cross_border { "跨境干线" } else { "境内接驳" },
                transit_days: *days,
                is_cross_border,
            });
        }
        views
    }

    /// 汇总材料行的展示文本。
    fn summarize_material_lines(
        &self,
        packaging_view: &super::subsystem_ports::PackagingPlanView,
    ) -> String {
        if packaging_view.material_lines.is_empty() {
            return "(无需包装材料)".to_string();
        }
        let mut summary: String = String::new();
        for (position, line) in packaging_view.material_lines.iter().enumerate() {
            if position > 0 {
                summary.push('、');
            }
            summary.push_str(&format!("{} × {}", line.material_label, line.quantity));
        }
        summary
    }

    /// 构造各来源的费用项(门面的翻译职责)。
    ///
    /// 参数 `request` / `packaging_view` / `option` /
    /// `document_requests` / `total_declared_value`。
    /// 返回:结算端口的中立输入项列表。
    ///
    /// ## 这个函数是门面模式的精华所在
    ///
    /// 五个子系统各自产出自己的量(运费、包装费、单据数、服务项),
    /// 只有门面同时看得见它们,因此**只有门面能完成这次翻译**。
    /// 若把这段逻辑下放到任何子系统,那个子系统就必须认识其他子系统,
    /// 横向耦合随之出现。这就是「门面不是简单转发器」的具体证据。
    fn build_charge_items(
        &self,
        request: &ShipmentRequest,
        packaging_view: &super::subsystem_ports::PackagingPlanView,
        option: &CarrierOptionView,
        document_requests: &[DocumentRequirementView],
        total_declared_value: &CurrencyAmount,
    ) -> Vec<SettlementInputItem> {
        let mut items: Vec<SettlementInputItem> = Vec::new();

        // ---------- 运费 ----------
        items.push(SettlementInputItem {
            code: "FREIGHT",
            label: "基础运费",
            source_code: "FREIGHT",
            source_label: ChargeSource::Freight.label(),
            amount: option.freight_charge,
            basis_text: "含包装后总重 × 承运商费率 × 服务等级调整",
        });

        // ---------- 包装材料费 ----------
        if !packaging_view.total_material_cost.is_zero() {
            items.push(SettlementInputItem {
                code: "PACKAGING_MATERIAL",
                label: "包装材料费",
                source_code: "PACKAGING",
                source_label: ChargeSource::Packaging.label(),
                amount: packaging_view.total_material_cost,
                basis_text: "各材料单价 × 用量之和",
            });
        }

        // ---------- 单据工本费 ----------
        // 按「实际会出具的单据张数」计费:豁免项不计费。
        let billable_document_count: i64 = document_requests
            .iter()
            // 只有非「豁免」状态的单据才收费。
            .filter(|requirement| requirement.status_label != "已豁免")
            .count() as i64;
        if billable_document_count > 0 {
            let document_fee: CurrencyAmount =
                CurrencyAmount::from_minor_units(DOCUMENT_ISSUANCE_FEE_MINOR_UNITS, CURRENCY_CHINESE_YUAN)
                    .multiply_by_quantity(billable_document_count);
            items.push(SettlementInputItem {
                code: "DOCUMENTATION",
                label: "单据工本费",
                source_code: "DOCUMENTATION",
                source_label: ChargeSource::Documentation.label(),
                amount: document_fee,
                basis_text: "按出具单据张数 × 每张 ¥12.00 计费",
            });
        }

        // ---------- 增值服务:保价 ----------
        if request.requires_insurance {
            let insurance_amount: CurrencyAmount =
                Ratio::from_basis_points(INSURANCE_RATE_BASIS_POINTS).of(total_declared_value);
            items.push(SettlementInputItem {
                code: "INSURANCE",
                label: "保价服务",
                source_code: "VALUE_ADDED_SERVICE",
                source_label: ChargeSource::ValueAddedService.label(),
                amount: insurance_amount,
                basis_text: "申报总价值 × 0.30%",
            });
        }

        // ---------- 增值服务:拆箱查验 ----------
        if request.requires_inspection {
            let inspection_fee: CurrencyAmount = CurrencyAmount::from_minor_units(
                INSPECTION_FEE_PER_PIECE_MINOR_UNITS,
                CURRENCY_CHINESE_YUAN,
            )
            .multiply_by_quantity(request.total_piece_count());
            items.push(SettlementInputItem {
                code: "INSPECTION",
                label: "拆箱查验",
                source_code: "VALUE_ADDED_SERVICE",
                source_label: ChargeSource::ValueAddedService.label(),
                amount: inspection_fee,
                basis_text: "按总件数 × 每件 ¥35.00 计费",
            });
        }

        items
    }

    /// 汇聚各子系统的问题,形成统一的事件列表。
    ///
    /// 参数为各环节的输出。
    /// 返回:事件列表。
    ///
    /// ## 「一次报全」在这里体现
    ///
    /// 本函数**线性走完所有检查**,从不提前 return。
    /// 因此一票货的所有问题会一次性出现在同一份报表里。
    /// 对运营而言,这意味着「改一次就能提交」而不是「提交八轮」。
    #[allow(clippy::too_many_arguments)]
    fn collect_events(
        &self,
        request: &ShipmentRequest,
        option: &CarrierOptionView,
        candidates: &[CarrierOptionView],
        packaging_view: &super::subsystem_ports::PackagingPlanView,
        document_requests: &[DocumentRequirementView],
        settlement_view: &SettlementView,
        total_declared_value_minor_units: i64,
    ) -> Vec<FacadeEvent> {
        let mut events: Vec<FacadeEvent> = Vec::new();

        // ---------- 调度环节 ----------
        if candidates.is_empty() {
            events.push(FacadeEvent::new(
                "调度",
                EventSeverity::Critical,
                "没有任何承运方案可供选择".to_string(),
                "联系调度部门确认承运商接入状态",
            ));
        } else if !option.is_feasible && !candidates.iter().any(|candidate| candidate.is_feasible) {
            // 全部不可行:把每个候选的原因都列出来(一次报全)。
            let reasons: String = candidates
                .iter()
                .map(|candidate| format!("{}:{}", candidate.carrier_label, candidate.infeasibility_reason))
                .collect::<Vec<String>>()
                .join(";");
            events.push(FacadeEvent::new(
                "调度",
                EventSeverity::Critical,
                format!("全部 {} 个候选承运方案均不可行({})", candidates.len(), reasons),
                "调整运输方式、拆分货物或改换温控方案",
            ));
        } else if option.is_feasible {
            // 运力紧张提示:占用率超过门面设定的阈值。
            let utilization: i64 = self
                .capacity_port
                .utilization_basis_points(option.carrier_code);
            if utilization >= CAPACITY_TIGHTNESS_THRESHOLD_BASIS_POINTS {
                events.push(FacadeEvent::new(
                    "调度",
                    EventSeverity::Warning,
                    format!(
                        "承运商「{}」运力占用率已达 {}.{:02}%,舱位可能紧张",
                        option.carrier_label,
                        utilization / 100,
                        utilization % 100
                    ),
                    "建议提前订舱或准备备选承运商",
                ));
            }
        }

        // ---------- 包装环节 ----------
        if request.requires_temperature_control && !packaging_view.uses_insulating_material {
            events.push(FacadeEvent::new(
                "包装",
                EventSeverity::Critical,
                "本票要求温控,但包装方案未使用保温材料".to_string(),
                "改用保温包装材料或取消温控要求",
            ));
        }
        // 包装增重提示(信息级):让客户知道计费重量的构成。
        if packaging_view.weight_gain.milligrams() > 0 {
            events.push(FacadeEvent::new(
                "包装",
                EventSeverity::Info,
                format!(
                    "包装增重 {},已计入计费重量",
                    packaging_view.weight_gain.formatted_grams()
                ),
                "如需降低运费可减少包装层数",
            ));
        }

        // ---------- 单据环节 ----------
        for requirement in document_requests {
            // 只有「会阻断」的项才产生严重级事件;
            // 其余(已具备)不必逐条报,否则报表会被无信息量的行填满。
            if requirement.blocks_dispatch {
                events.push(FacadeEvent::new(
                    "单据",
                    EventSeverity::Critical,
                    format!(
                        "必备单据「{}」缺失:{}",
                        requirement.kind.label(),
                        requirement.trigger_reason
                    ),
                    "补齐该单据或申请豁免",
                ));
            }
        }

        // ---------- 结算环节 ----------
        // 高值货未投保:警告级(不阻断,但风控上应确认)。
        let insurance_present: bool = settlement_view
            .items
            .iter()
            .any(|item| item.code == "INSURANCE");
        if total_declared_value_minor_units >= HIGH_VALUE_THRESHOLD_MINOR_UNITS && !insurance_present
        {
            events.push(FacadeEvent::new(
                "结算",
                EventSeverity::Warning,
                format!(
                    "申报总价值已达高价值线 ¥{},但未投保价服务",
                    format_minor_units_as_yuan(HIGH_VALUE_THRESHOLD_MINOR_UNITS)
                ),
                "如为贵重货品,建议追加保价服务",
            ));
        }
        // 应付总额为负(折扣超过费用):属数据异常,需人工核查。
        if settlement_view.grand_total.is_negative() {
            events.push(FacadeEvent::new(
                "结算",
                EventSeverity::Warning,
                "应付总额为负值,可能存在折扣配置错误".to_string(),
                "核查折扣与费率配置",
            ));
        }

        events
    }
}

/// 把最小单位金额格式化为「元」的整数字符串(用于文案中的阈值展示)。
///
/// 参数 `minor_units`:最小单位数值。
/// 返回:如 `"50,000.00"` 的文本(不含货币符号)。
///
/// 这是一个**自由函数**而非方法:它不需要门面的任何状态,
/// 且被事件文案与报表共用,放在模块级便于两边引用同一个实现
/// (避免文案里的数字与判断用的阈值不一致)。
pub fn format_minor_units_as_yuan(minor_units: i64) -> String {
    // 复用 domain 的格式化能力,确保与报表中的金额格式完全一致。
    CurrencyAmount::from_minor_units(minor_units, CURRENCY_CHINESE_YUAN).formatted()
}

/// 一个便捷的自由函数:生成标准运单号(供工程外扩展区复用)。
///
/// 参数 `shipment_reference` / `carrier_code`。
/// 返回:运单号。
///
/// 之所以把它暴露成自由函数而不是让工程外自己写派生逻辑:
/// 保证外部生成的号与门面内部的号**同一套算法**,
/// 否则两边会产出不同的号,报表比对就失真了。
pub fn derive_waybill_number(shipment_reference: &str, carrier_code: &str) -> String {
    derive_code(
        "WB",
        &build_seed(&[shipment_reference, carrier_code]),
        12,
    )
}

/// 一个便捷的自由函数:把内部包装方案转成门面的包装视图。
///
/// 参数 `plan`:包装子系统的方案。
/// 返回:门面的包装视图。
///
/// 存在的理由:工程外扩展区需要构造与内置实现**形状一致**的端口实现,
/// 而它们往往想复用内置包装方案的翻译逻辑。暴露这个函数后,
/// 外部实现可以「用内置方案 + 自定义端口」组合,而不必重写翻译。
pub fn translate_packaging_plan(plan: &PackagingPlan) -> super::subsystem_ports::PackagingPlanView {
    super::subsystem_ports::PackagingPlanView {
        material_lines: plan
            .lines()
            .iter()
            .map(|line| super::subsystem_ports::PackagingMaterialLineView {
                material_label: line.material().label(),
                material_code: line.material().code(),
                quantity: line.quantity(),
                line_cost: line.line_cost(),
            })
            .collect(),
        total_material_cost: plan.total_material_cost(),
        weight_gain: plan.packaging_weight_gain(),
        total_weight_with_packaging: plan.total_weight_with_packaging(),
        uses_insulating_material: plan.uses_insulating_material(),
    }
}

/// 一个便捷的自由函数:把内部承运方案转成中立视图。
///
/// 参数 `option`:调度子系统的候选方案。
/// 返回:中立视图。
///
/// 与 [`translate_packaging_plan`] 同理:内置端口实现与工程外实现
/// 都可以复用它,保证「翻译口径」只有一处。
pub fn translate_carrier_option(option: &CarrierOption) -> CarrierOptionView {
    CarrierOptionView {
        carrier_code: option.carrier_code(),
        carrier_label: option.carrier_label(),
        freight_charge: option.base_freight_charge(),
        route_leg_days: option
            .route_legs()
            .iter()
            .map(|leg| leg.transit_days())
            .collect(),
        route_leg_is_cross_border: option
            .route_legs()
            .iter()
            .map(|leg| leg.is_cross_border())
            .collect(),
        route_summary: option.route_summary_text(),
        visited_country_codes: option.visited_country_codes(),
        is_feasible: option.is_feasible(),
        infeasibility_reason: option.infeasibility_reason(),
    }
}

/// 一个便捷的自由函数:把内部单据要求转成中立视图。
///
/// 参数 `requirement`:单据子系统的清单项。
/// 返回:中立视图。
pub fn translate_document_requirement(
    requirement: &DocumentRequirement,
) -> DocumentRequirementView {
    DocumentRequirementView {
        kind: requirement.kind(),
        status_label: requirement.status().label(),
        is_mandatory: requirement.is_mandatory(),
        blocks_dispatch: requirement.blocks_dispatch(),
        trigger_reason: requirement.trigger_reason(),
    }
}

/// 一个便捷的自由函数:把内部费用项转成结算端口的中立输入。
///
/// 参数 `item`:结算子系统的费用项。
/// 返回:中立输入项。
///
/// 注意 [`ChargeItem`] 与 [`ChargeSource`] 在此被「翻译」掉,
/// 使工程外实现无需依赖 `settlement` 模块即可接受门面的输入。
pub fn translate_charge_item(item: &ChargeItem) -> SettlementInputItem {
    SettlementInputItem {
        code: item.code(),
        label: item.label(),
        source_code: source_code_of(item.source()),
        source_label: item.source().label(),
        amount: item.amount(),
        basis_text: item.basis_text(),
    }
}

/// 把费用来源枚举转成稳定的字符串编码。
///
/// 参数 `source`:费用来源。
/// 返回:大写编码(如 `"FREIGHT"`)。
///
/// 用 `match` 而不是 `Debug` 格式化:`Debug` 输出(如 `Freight`)
/// 是内部表示,一旦枚举改名就会变化;显式的字符串映射才是稳定契约。
pub fn source_code_of(source: ChargeSource) -> &'static str {
    match source {
        ChargeSource::Freight => "FREIGHT",
        ChargeSource::Packaging => "PACKAGING",
        ChargeSource::Documentation => "DOCUMENTATION",
        ChargeSource::ValueAddedService => "VALUE_ADDED_SERVICE",
    }
}

/// 一个便捷的自由函数:把内部时间线转成中立视图。
///
/// 参数 `timeline`:承运子系统的时间线。
/// 返回:中立视图。
pub fn translate_timeline(
    timeline: &crate::carrier::timeline_builder::CarrierTimeline,
) -> super::subsystem_ports::TimelineView {
    super::subsystem_ports::TimelineView {
        milestones: timeline
            .milestones()
            .iter()
            .map(|milestone| super::subsystem_ports::TimelineMilestoneView {
                code: milestone.code(),
                label: milestone.label(),
                date: milestone.date(),
                note: milestone.note(),
            })
            .collect(),
        estimated_delivery_date: timeline.estimated_delivery_date(),
        delayed_by_weekend: timeline.delayed_by_weekend(),
    }
}

/// 一个便捷的自由函数:把内部结算结果转成中立视图。
///
/// 参数 `result`:结算子系统的结果。
/// 返回:中立视图。
pub fn translate_settlement(
    result: &crate::settlement::settlement_ledger::SettlementResult,
) -> SettlementView {
    SettlementView {
        items: result
            .items()
            .iter()
            .map(translate_charge_item)
            .collect(),
        grand_total: result.grand_total(),
        largest_item_code: result.largest_item().map(|item| item.code()),
        source_subtotals: result
            .by_source()
            .iter()
            .map(|subtotal| (source_code_of(subtotal.source()), subtotal.subtotal()))
            .collect(),
    }
}

/// 判断某工作日调整是否发生(供报表提示用)。
///
/// 参数 `planned` / `adjusted`。
/// 返回:发生变化返回 `true`。
///
/// 这类小工具放在门面模块而非 `support`:它只被门面与报表使用,
/// 提升到支持层反而会让支持层承载业务语义。
pub fn dispatch_date_was_adjusted(planned: &crate::support::calendar_date::CalendarDate) -> bool {
    let adjusted = adjust_dispatch_date_for_weekend(planned);
    adjusted != *planned
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : shipping_result.rs
//! 门面的输出:装配结果与其视图类型。
//!
//! # 为什么门面必须定义自己的输出类型
//!
//! 这是门面模式最容易被忽略、却最影响可维护性的一条:
//!
//! **若门面把子系统的类型直接返回给调用方,门面就白做了。**
//! 调用方仍会写出 `use crate::documents::DocumentChecklist;`,
//! 于是子系统一改,调用方跟着改——封装只是形式上的。
//!
//! 因此本文件的每个 `*View` 都是门面**自己的**类型:
//! 字段全部是 `domain` 层的值对象或用例无关的原始类型。
//! 调用方(`app` 报表 / `analysis` 分析)只 import 本模块,
//! **编译期就保证了它们看不到任何子系统类型**。
//!
//! 这个性质是可以被验证的:若有人不小心在 `app` 里写了
//! `use crate::dispatch::...`,依赖方向脚本会立刻把它暴露出来。

use crate::domain::{CurrencyAmount, DocumentKind, EventSeverity, ShippingWeight};
use crate::support::calendar_date::CalendarDate;

/// 路由段视图。
#[derive(Debug, Clone)]
pub struct RouteLegView {
    /// 段序号。
    pub sequence_number: u32,
    /// 起点城市。
    pub origin_city: &'static str,
    /// 起点国家/地区代码。
    pub origin_country_code: &'static str,
    /// 终点城市。
    pub destination_city: &'static str,
    /// 终点国家/地区代码。
    pub destination_country_code: &'static str,
    /// 运输方式描述。
    pub transport_mode_label: &'static str,
    /// 该段天数。
    pub transit_days: u32,
    /// 是否为跨境段。
    pub is_cross_border: bool,
}

impl RouteLegView {
    /// 返回一行展示文本。
    pub fn formatted(&self) -> String {
        format!(
            "第 {} 段|{} {} → {} {}|{}|{} 天",
            self.sequence_number,
            self.origin_city,
            self.origin_country_code,
            self.destination_city,
            self.destination_country_code,
            self.transport_mode_label,
            self.transit_days
        )
    }
}

/// 包装视图。
#[derive(Debug, Clone)]
pub struct PackagingView {
    /// 材料清单文本(如「木质礼盒 × 1、防震泡沫箱 × 2」)。
    pub material_summary: String,
    /// 材料种类数。
    pub material_kind_count: usize,
    /// 材料总成本。
    pub total_material_cost: CurrencyAmount,
    /// 包装增重。
    pub weight_gain: ShippingWeight,
    /// 包装后总重。
    pub total_weight_with_packaging: ShippingWeight,
    /// 是否使用保温材料。
    pub uses_insulating_material: bool,
}

/// 单据视图。
#[derive(Debug, Clone)]
pub struct DocumentView {
    /// 单据类型。
    pub kind: DocumentKind,
    /// 状态标签。
    pub status_label: &'static str,
    /// 是否强制。
    pub is_mandatory: bool,
    /// 是否阻断出单。
    pub blocks_dispatch: bool,
    /// 触发原因。
    pub trigger_reason: &'static str,
}

/// 时间线视图。
#[derive(Debug, Clone)]
pub struct ShipmentTimelineView {
    /// 里程碑列表(编码, 名称, 日期, 说明)。
    pub milestones: Vec<(&'static str, &'static str, CalendarDate, &'static str)>,
    /// 预计送达日。
    pub estimated_delivery_date: CalendarDate,
    /// 是否因周末顺延。
    pub delayed_by_weekend: bool,
}

/// 一条汇总事件(门面在汇聚各子系统后产出的统一问题表述)。
#[derive(Debug, Clone)]
pub struct FacadeEvent {
    /// 事件来源环节(如「包装」「单据」)。
    pub stage_label: &'static str,
    /// 严重等级。
    pub severity: EventSeverity,
    /// 问题描述。
    pub description: String,
    /// 处理建议。
    pub suggestion: &'static str,
}

impl FacadeEvent {
    /// 构造一条事件。
    pub fn new(
        stage_label: &'static str,
        severity: EventSeverity,
        description: String,
        suggestion: &'static str,
    ) -> Self {
        FacadeEvent {
            stage_label,
            severity,
            description,
            suggestion,
        }
    }
}

/// 一次装配的最终结果。
///
/// ## 为什么把「是否可出单」做成方法而不是字段
///
/// 它是由事件列表推导出来的:只要存在 `Critical` 事件即不可出单。
/// 做成派生方法可以杜绝「字段值与事件列表不一致」这类 bug
/// (那在报表场景下会非常难查)。
#[derive(Debug, Clone)]
pub struct ShippingResult {
    /// 运单业务号。
    pub shipment_reference: &'static str,
    /// 运单号(由门面派生)。
    pub waybill_number: String,
    /// 结果指纹(用于报表比对同一票货的不同阶段)。
    pub fingerprint: String,
    /// 寄件方描述。
    pub sender_text: String,
    /// 收件方描述。
    pub receiver_text: String,
    /// 承运商代码。
    pub carrier_code: &'static str,
    /// 承运商中文名。
    pub carrier_label: &'static str,
    /// 服务等级名称。
    pub service_tier_label: &'static str,
    /// 路由摘要文本。
    pub route_summary: String,
    /// 路由段视图列表。
    pub route_legs: Vec<RouteLegView>,
    /// 包装视图。
    pub packaging: PackagingView,
    /// 单据视图列表。
    pub documents: Vec<DocumentView>,
    /// 时间线视图。
    pub timeline: ShipmentTimelineView,
    /// 费用项(编码, 名称, 来源名, 金额, 计价依据)。
    pub charge_items: Vec<(&'static str, &'static str, &'static str, CurrencyAmount, &'static str)>,
    /// 应付总额。
    pub grand_total: CurrencyAmount,
    /// 最大费用项编码。
    pub largest_item_code: Option<&'static str>,
    /// 货物明细(名称, 品类名, 件数, 单件重量, 单件申报价值)。
    pub cargo_lines: Vec<(&'static str, &'static str, i64, ShippingWeight, CurrencyAmount)>,
    /// 汇聚后的事件列表。
    pub events: Vec<FacadeEvent>,
    /// 货物净重。
    pub net_cargo_weight: ShippingWeight,
    /// 申报总价值。
    pub total_declared_value: CurrencyAmount,
}

impl ShippingResult {
    /// 是否存在阻断级事件。
    pub fn is_blocked(&self) -> bool {
        self.events
            .iter()
            .any(|event| event.severity.blocks_dispatch())
    }

    /// 阻断级事件数量。
    pub fn blocker_count(&self) -> usize {
        self.events
            .iter()
            .filter(|event| event.severity.blocks_dispatch())
            .count()
    }

    /// 警告级事件数量。
    pub fn warning_count(&self) -> usize {
        self.events
            .iter()
            .filter(|event| event.severity == EventSeverity::Warning)
            .count()
    }

    /// 提示级事件数量。
    pub fn info_count(&self) -> usize {
        self.events
            .iter()
            .filter(|event| event.severity == EventSeverity::Info)
            .count()
    }

    /// 事件总数。
    pub fn event_count(&self) -> usize {
        self.events.len()
    }

    /// 路由段数。
    pub fn route_leg_count(&self) -> usize {
        self.route_legs.len()
    }

    /// 单据数量。
    pub fn document_count(&self) -> usize {
        self.documents.len()
    }

    /// 单据编码列表。
    pub fn document_codes(&self) -> Vec<&'static str> {
        self.documents
            .iter()
            .map(|document| document.kind.code())
            .collect()
    }

    /// 返回单一业务结论文本(供报表首行展示)。
    pub fn outcome_text(&self) -> &'static str {
        if self.is_blocked() {
            "装配被拒绝:存在严重级问题"
        } else if self.warning_count() > 0 {
            "装配通过,另有提醒供人工确认"
        } else {
            "装配通过,未发现需要人工确认的事项"
        }
    }
}

/// 门面的装配结论(比 [`ShippingResult`] 更轻量的返回形式)。
///
/// 存在的理由:门面偶尔需要返回「成功/失败 + 原因」而无需完整结果,
/// 例如工程外扩展区里只想知道「这票货能不能走」。
/// 用独立的枚举比复用 `ShippingResult` 更贴合这类调用点,
/// 且使得 `match` 的穷尽性检查能覆盖两种情形。
#[derive(Debug, Clone)]
pub enum ShipmentOutcome {
    /// 装配成功,携带完整结果。
    Planned(Box<ShippingResult>),
    /// 装配被拒绝,携带原因与完整结果(便于定位)。
    Rejected {
        /// 拒绝原因。
        reason: String,
        /// 仍然返回的完整结果(含全部事件)。
        result: Box<ShippingResult>,
    },
}

impl ShipmentOutcome {
    /// 是否成功。
    pub fn is_planned(&self) -> bool {
        match self {
            ShipmentOutcome::Planned(_) => true,
            ShipmentOutcome::Rejected { .. } => false,
        }
    }

    /// 取出结果引用(无论成功与否都有结果)。
    pub fn result(&self) -> &ShippingResult {
        match self {
            ShipmentOutcome::Planned(result) => result,
            ShipmentOutcome::Rejected { result, .. } => result,
        }
    }

    /// 拒绝原因(成功时为 None)。
    pub fn rejection_reason(&self) -> Option<&str> {
        match self {
            ShipmentOutcome::Planned(_) => None,
            ShipmentOutcome::Rejected { reason, .. } => Some(reason.as_str()),
        }
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : subsystem_ports.rs
//! 子系统「端口」特征 —— 门面与子系统之间的**方法形状契约**。
//!
//! # 这是本工程可扩展性的关键设计,务必先读这段
//!
//! ## 问题的由来
//!
//! 「门面封装子系统」听起来很美好,但有个陷阱:
//! 如果门面直接持有 `dispatch::RoutePlanner` 这类**具体类型**,
//! 那么「换一套子系统实现」就必须改门面——门面反而成了新的耦合中心。
//!
//! ## 本工程的对策:门面对子系统只依赖「方法形状」
//!
//! 下面每个 trait 都**只声明方法签名**,且方法签名里**不出现任何子系统类型**
//! (输入输出都是 `domain` 层的值对象或门面自己的类型)。
//! 于是:
//!
//! - 门面持有的是 `Box<dyn DispatchPort>` 之类的**特征对象**;
//! - 工程外只要写一个结构体,实现同名方法(乃至不同类型的同名方法集),
//!   就能替换掉整套子系统,**门面代码零改动**;
//! - 这比「再写一个门面实现」有力得多,因为它证明可替换的是**子系统**
//!   (门面模式的真正价值点),而不是门面本身。
//!
//! ## 为什么用特征而不是「结构体 + 泛型」
//!
//! 泛型(`ShippingFacade<D: DispatchPort, P: PackagingPort, ...>`)
//! 也能做到同样的解耦,且是零成本抽象的。这里选特征对象的原因:
//! **门面需要在本工程里被以「运行期组装」的方式演示**
//! (不同幕次使用不同子系统组合),而泛型会把这种灵活性推到类型层面,
//! 让调用方必须写出长长的类型参数。对示范工程而言,可读性优先。
//!
//! 若这条路径将来成为性能瓶颈,把特征对象换成泛型是一个**机械重构**
//! (签名一一对应),不会触及业务逻辑——这正是先定契约的价值。

use crate::domain::{
    CountryCode, CurrencyAmount, DocumentKind, ServiceTier, ShippingWeight,
};
use crate::support::calendar_date::CalendarDate;

/// 调度端口:给定运输需求,返回候选承运方案的**中立描述**。
///
/// 注意返回类型不是 `dispatch::CarrierOption`,而是本文件定义的
/// [`CarrierOptionView`]——这样 `dispatch` 内部改字段也不会波及门面,
/// 且工程外的替代实现无需依赖 `dispatch` 模块。
pub trait DispatchPort {
    /// 返回候选方案列表(含不可行项及其原因)。
    fn plan_options(&self, input: &DispatchInput) -> Vec<CarrierOptionView>;
}

/// 调度端口的输入(中立描述)。
#[derive(Debug, Clone)]
pub struct DispatchInput {
    /// 起始城市。
    pub origin_city: &'static str,
    /// 起始国家/地区。
    pub origin_country: CountryCode,
    /// 目的城市。
    pub destination_city: &'static str,
    /// 目的国家/地区。
    pub destination_country: CountryCode,
    /// 计费重量。
    pub chargeable_weight: ShippingWeight,
    /// 是否需要温控。
    pub requires_temperature_control: bool,
    /// 服务等级。
    pub service_tier: ServiceTier,
}

/// 承运方案的中立视图。
///
/// 字段全部是 `domain` 层的值对象或原始类型,
/// 因此任何子系统实现都能轻松构造它,门面也无从依赖具体实现。
#[derive(Debug, Clone)]
pub struct CarrierOptionView {
    /// 承运商代码。
    pub carrier_code: &'static str,
    /// 承运商中文名。
    pub carrier_label: &'static str,
    /// 基础运费(含服务等级调整)。
    pub freight_charge: CurrencyAmount,
    /// 路由各段的天数(按序)。
    pub route_leg_days: Vec<u32>,
    /// 路由各段是否为跨境段(与 `route_leg_days` 等长)。
    pub route_leg_is_cross_border: Vec<bool>,
    /// 路由的一行式摘要(如「深圳 → 广州 → 法兰克福 → 柏林」)。
    pub route_summary: String,
    /// 途经国家/地区代码(有序)。
    pub visited_country_codes: Vec<&'static str>,
    /// 是否可行。
    pub is_feasible: bool,
    /// 不可行原因(可行时为空串)。
    pub infeasibility_reason: &'static str,
}

/// 包装端口:给定包装需求,返回包装方案的中立描述。
pub trait PackagingPort {
    /// 返回包装方案。
    fn plan_packaging(&self, input: &PackagingInput) -> PackagingPlanView;
}

/// 包装端口的输入。
#[derive(Debug, Clone)]
pub struct PackagingInput {
    /// 珠宝类件数。
    pub jewelry_piece_count: i64,
    /// 其他件数。
    pub other_piece_count: i64,
    /// 是否需要温控。
    pub requires_temperature_control: bool,
    /// 未包装净重。
    pub net_cargo_weight: ShippingWeight,
}

/// 包装方案的中立视图。
#[derive(Debug, Clone)]
pub struct PackagingPlanView {
    /// 各材料行(材料名, 数量, 行成本)。
    ///
    /// 用元组而不是定义一个结构体,是因为门面只做转发展示,
    /// 不参与计算;加一层命名结构体属于过度设计。
    /// 若将来门面要按材料做运算,再引入结构体不迟。
    pub material_lines: Vec<PackagingMaterialLineView>,
    /// 包装材料总成本。
    pub total_material_cost: CurrencyAmount,
    /// 包装增重。
    pub weight_gain: ShippingWeight,
    /// 包装后总重。
    pub total_weight_with_packaging: ShippingWeight,
    /// 是否使用保温材料。
    pub uses_insulating_material: bool,
}

/// 包装方案中的一行材料(中立视图)。
#[derive(Debug, Clone)]
pub struct PackagingMaterialLineView {
    /// 材料名称。
    pub material_label: &'static str,
    /// 材料编码。
    pub material_code: &'static str,
    /// 用量。
    pub quantity: i64,
    /// 该行成本。
    pub line_cost: CurrencyAmount,
}

/// 单据端口:给定编成条件,返回单据清单的中立描述。
pub trait DocumentCompilationPort {
    /// 返回单据清单项列表。
    fn compile_checklist(&self, input: &DocumentInput) -> Vec<DocumentRequirementView>;
}

/// 单据端口的输入。
#[derive(Debug, Clone)]
pub struct DocumentInput {
    /// 是否跨境。
    pub is_cross_border: bool,
    /// 是否需要温控。
    pub requires_temperature_control: bool,
    /// 是否有可申报品类。
    pub has_declarable_cargo: bool,
    /// 是否有高值货。
    pub has_high_value_cargo: bool,
    /// 目的国家/地区代码。
    pub destination_country_code: &'static str,
    /// 目的地是否要求正式报关。
    pub destination_requires_customs_declaration: bool,
}

/// 单据需求的中立视图。
#[derive(Debug, Clone)]
pub struct DocumentRequirementView {
    /// 单据类型。
    pub kind: DocumentKind,
    /// 状态中文标签(如「已具备」)。
    ///
    /// 这里用 `&'static str` 而不是系统内部的状态枚举,
    /// 是为了让工程外实现无需依赖 `documents` 模块即可产出清单。
    /// 代价是失去了状态的可比较性——门面只做展示与「是否阻断」判断,
    /// 不需要比较状态,因此这个取舍是划算的。
    pub status_label: &'static str,
    /// 是否为强制项。
    pub is_mandatory: bool,
    /// 是否会阻断出单。
    pub blocks_dispatch: bool,
    /// 触发原因。
    pub trigger_reason: &'static str,
}

/// 承运端口:给定时间线需求,返回时间线中立描述。
pub trait TimelinePort {
    /// 返回时间线视图。
    fn build_timeline_view(&self, input: &TimelineInput) -> TimelineView;
}

/// 承运端口的输入。
#[derive(Debug, Clone)]
pub struct TimelineInput {
    /// 计划发货日。
    pub dispatch_date: CalendarDate,
    /// 各段天数。
    pub route_leg_days: Vec<u32>,
    /// 各段是否跨境。
    pub route_leg_is_cross_border: Vec<bool>,
    /// 目的城市名。
    pub destination_city: &'static str,
}

/// 时间线的中立视图。
#[derive(Debug, Clone)]
pub struct TimelineView {
    /// 各里程碑(编码, 名称, 日期, 说明)。
    pub milestones: Vec<TimelineMilestoneView>,
    /// 预计送达日。
    pub estimated_delivery_date: CalendarDate,
    /// 是否因周末顺延。
    pub delayed_by_weekend: bool,
}

/// 时间线里程碑的中立视图。
#[derive(Debug, Clone)]
pub struct TimelineMilestoneView {
    /// 里程碑编码。
    pub code: &'static str,
    /// 里程碑名称。
    pub label: &'static str,
    /// 日期。
    pub date: CalendarDate,
    /// 说明。
    pub note: &'static str,
}

/// 结算端口:给定费用项,返回结算结果的中立描述。
pub trait SettlementPort {
    /// 返回结算视图。
    fn settle(&self, items: Vec<SettlementInputItem>) -> SettlementView;
}

/// 结算端口的输入项(中立描述)。
///
/// 注意它**不依赖** `settlement::ChargeItem`,
/// 也不依赖 `settlement::ChargeSource`——来源用字符串表达,
/// 使替代实现无需 import 任何 `settlement` 中的类型。
/// 这正是「端口只依赖方法形状」的具体体现。
#[derive(Debug, Clone)]
pub struct SettlementInputItem {
    /// 费用项编码。
    pub code: &'static str,
    /// 费用项名称。
    pub label: &'static str,
    /// 来源编码(如 `"FREIGHT"`)。
    pub source_code: &'static str,
    /// 来源名称(如「基础运费」)。
    pub source_label: &'static str,
    /// 金额。
    pub amount: CurrencyAmount,
    /// 计价依据。
    pub basis_text: &'static str,
}

/// 结算结果的中立视图。
#[derive(Debug, Clone)]
pub struct SettlementView {
    /// 全部费用项(已按来源排序)。
    pub items: Vec<SettlementInputItem>,
    /// 应付总额。
    pub grand_total: CurrencyAmount,
    /// 最大费用项的编码(无项时为 None)。
    pub largest_item_code: Option<&'static str>,
    /// 某一来源的合计金额查询用表:(来源编码, 合计金额)。
    pub source_subtotals: Vec<(&'static str, CurrencyAmount)>,
}

impl SettlementView {
    /// 查询某一来源的合计金额(不存在返回零)。
    pub fn subtotal_of_source(&self, source_code: &str) -> Option<CurrencyAmount> {
        self.source_subtotals
            .iter()
            .find(|(code, _)| *code == source_code)
            .map(|(_, amount)| *amount)
    }
}

/// 运力台账端口:门面用它查询运力紧张度(用于追加警告)。
///
/// 单独成一个端口而不是并入 [`DispatchPort`]:
/// 「规划路线」与「查询运力」是两个不同的关注点,
/// 将来可能有实现只关心其中一个。接口细粒度化便于按需替换。
pub trait CapacityPort {
    /// 返回某承运商的占用率(万分比)。
    fn utilization_basis_points(&self, carrier_code: &str) -> i64;
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : packaging_plan.rs
//! 包装方案值对象(包装子系统的输出结构)。
//!
//! ## 为什么把「一行材料」单独建模
//!
//! 包装方案常需要**逐件材料列出**(报关与客户对账都要看明细)。
//! 若只在方案里存一个总成本,明细就丢失了。
//! 因此方案由若干 [`PackagingPlanLine`] 组成,汇总值由明细推导
//! (而不是另外存一遍,避免两处不一致)。

use crate::domain::{CurrencyAmount, PackagingMaterial, ShippingWeight, CURRENCY_CHINESE_YUAN};

/// 包装方案中的一行材料用量。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PackagingPlanLine {
    /// 使用的材料。
    material: PackagingMaterial,
    /// 用量(件数)。
    quantity: i64,
}

impl PackagingPlanLine {
    /// 构造一行材料用量。
    pub const fn new(material: PackagingMaterial, quantity: i64) -> Self {
        PackagingPlanLine { material, quantity }
    }

    /// 材料。
    pub const fn material(&self) -> PackagingMaterial {
        self.material
    }

    /// 用量。
    pub const fn quantity(&self) -> i64 {
        self.quantity
    }

    /// 该行成本(材料单价 × 用量)。
    pub fn line_cost(&self) -> CurrencyAmount {
        CurrencyAmount::from_minor_units(self.material.unit_cost_minor_units(), CURRENCY_CHINESE_YUAN)
            .multiply_by_quantity(self.quantity)
    }

    /// 返回一行展示文本,如 `"木质礼盒 × 1 = ¥18.00"`。
    pub fn formatted(&self) -> String {
        format!(
            "{} × {} = {}",
            self.material.label(),
            self.quantity,
            self.line_cost().formatted()
        )
    }
}

/// 一份完整的包装方案。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackagingPlan {
    /// 材料明细行。
    lines: Vec<PackagingPlanLine>,
    /// 包装后的总重量(原货重 + 包装增重)。
    ///
    /// 存总重而不是只存增重,是因为下游(承运、单据)几乎总是需要总重;
    /// 同时保留 [`PackagingPlan::packaging_weight_gain`] 让需要增重的地方也能拿到。
    total_weight_with_packaging: ShippingWeight,
    /// 包装产生的增重(用于门面在报表里单独展示「包装增重」一栏)。
    packaging_weight_gain: ShippingWeight,
    /// 是否使用了保温材料(温控货必须为 true,由门面核查)。
    uses_insulating_material: bool,
}

impl PackagingPlan {
    /// 构造一份包装方案。
    pub fn new(
        lines: Vec<PackagingPlanLine>,
        total_weight_with_packaging: ShippingWeight,
        packaging_weight_gain: ShippingWeight,
        uses_insulating_material: bool,
    ) -> Self {
        PackagingPlan {
            lines,
            total_weight_with_packaging,
            packaging_weight_gain,
            uses_insulating_material,
        }
    }

    /// 材料明细行。
    pub fn lines(&self) -> &[PackagingPlanLine] {
        &self.lines
    }

    /// 包装后总重。
    pub const fn total_weight_with_packaging(&self) -> ShippingWeight {
        self.total_weight_with_packaging
    }

    /// 包装增重。
    pub const fn packaging_weight_gain(&self) -> ShippingWeight {
        self.packaging_weight_gain
    }

    /// 是否使用了保温材料。
    pub const fn uses_insulating_material(&self) -> bool {
        self.uses_insulating_material
    }

    /// 包装材料总成本(对明细求和)。
    ///
    /// 从明细推导而非另存字段:明细是唯一事实来源,
    /// 派生值随明细变化,不存在「改了明细忘改汇总」的可能。
    pub fn total_material_cost(&self) -> CurrencyAmount {
        let mut total: CurrencyAmount =
            CurrencyAmount::zero(CURRENCY_CHINESE_YUAN);
        for line in &self.lines {
            total = total.add(&line.line_cost());
        }
        total
    }

    /// 材料种类数。
    pub fn material_kind_count(&self) -> usize {
        self.lines.len()
    }

    /// 材料总件数(各行用量之和)。
    pub fn total_material_quantity(&self) -> i64 {
        let mut total: i64 = 0;
        for line in &self.lines {
            total = total.saturating_add(line.quantity());
        }
        total
    }

    /// 返回材料清单的一行式文本,如 `"木质礼盒 × 1、防震泡沫箱 × 2"`。
    pub fn material_summary_text(&self) -> String {
        if self.lines.is_empty() {
            return "(无需包装材料)".to_string();
        }
        let mut summary: String = String::new();
        for (position, line) in self.lines.iter().enumerate() {
            if position > 0 {
                summary.push('、');
            }
            summary.push_str(&format!("{} × {}", line.material().label(), line.quantity()));
        }
        summary
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : packaging_planner.rs
//! 包装方案规划(包装子系统的纯计算部分)。
//!
//! ## 输入为什么是一个「请求结构体」
//!
//! 包装需要知道:货物品类构成、是否温控、件数。
//! 这些信息里有些来自门面(温控要求),有些来自货物清单。
//! 用一个请求结构体承载,将来若要加入「是否易碎」「是否需防潮」,
//! 只加字段不动签名。
//!
//! ## 包装增重的计算口径
//!
//! 每种材料有**单位增重**(在下方常量表中定义),
//! 总增重 = Σ(材料单位增重 × 用量)。这与 [`super::packaging_plan`] 的成本口径
//! 完全一致,两处都是「单位值 × 用量」,便于对账同事按同一套逻辑核对。

use crate::domain::{
    PackagingMaterial, ShippingWeight, CARGO_JEWELRY, PACKAGING_FOAM_BOX, PACKAGING_VACUUM_FOIL_BAG,
    PACKAGING_WOODEN_CRATE,
};

use super::packaging_plan::{PackagingPlan, PackagingPlanLine};

/// 单件木质礼盒的增重(毫克):450 g。
///
/// 为什么把增重放在这里而不是 `PackagingMaterial` 里:
/// 材料定义在 `domain` 层,是「跨子系统共享的标签」;
/// 而「一件木盒重多少克」是**包装子系统的专业参数**,
/// 把它塞进 domain 会让领域层承载子系统知识,破坏职责边界。
/// 代价是:若将来多个子系统都需要这个值,需要提升到 domain——
/// 那是重构点,而不是现在就过度设计。
const WOODEN_CRATE_WEIGHT_MILLIGRAMS: i64 = 450_000;

/// 单件防震泡沫箱的增重(毫克):180 g。
const FOAM_BOX_WEIGHT_MILLIGRAMS: i64 = 180_000;

/// 单件真空铝箔袋的增重(毫克):30 g。
const VACUUM_FOIL_BAG_WEIGHT_MILLIGRAMS: i64 = 30_000;

/// 包装规划的输入。
#[derive(Debug, Clone)]
pub struct PackagingRequest {
    /// 珠宝类货物的件数(决定需要多少个硬质礼盒)。
    pub jewelry_piece_count: i64,
    /// 其他货物件数(包装物料、证书等,用泡沫箱承载)。
    pub other_piece_count: i64,
    /// 是否需要温控(决定是否追加真空铝箔袋)。
    pub requires_temperature_control: bool,
    /// 未包装前的货物净重。
    pub net_cargo_weight: ShippingWeight,
}

/// 根据输入规划包装方案。
///
/// 参数 `request`:包装请求。
/// 返回:包装方案。
///
/// ## 规则(刻意简单,便于手算复核)
///
/// 1. 每件珠宝 → 1 个木质礼盒(硬质外观件);
/// 2. 每件其他货物 → 1 个防震泡沫箱;
/// 3. 若需温控 → 每种已有材料之外再套 1 个真空铝箔袋
///    (数量 = 1,因为整票货作为一个温控单元托运);
/// 4. 总重 = 净重 + 各材料单位增重 × 用量。
///
/// 注意规则 3 的数量固定为 1 而非「按件数」:
/// 温控是**整票层面**的属性(一个冷藏单元),不是逐件属性。
/// 这个区分容易写错,本函数用注释明确固定下来。
pub fn plan_packaging(request: &PackagingRequest) -> PackagingPlan {
    // 用 Vec 收集行,仅在数量大于 0 时推入,避免报表里出现「× 0」的噪音行。
    let mut lines: Vec<PackagingPlanLine> = Vec::new();
    // 累计增重,单位毫克。
    let mut total_gain_milligrams: i64 = 0;

    if request.jewelry_piece_count > 0 {
        lines.push(PackagingPlanLine::new(
            PACKAGING_WOODEN_CRATE,
            request.jewelry_piece_count,
        ));
        total_gain_milligrams = total_gain_milligrams
            .saturating_add(WOODEN_CRATE_WEIGHT_MILLIGRAMS * request.jewelry_piece_count);
    }

    if request.other_piece_count > 0 {
        lines.push(PackagingPlanLine::new(
            PACKAGING_FOAM_BOX,
            request.other_piece_count,
        ));
        total_gain_milligrams = total_gain_milligrams
            .saturating_add(FOAM_BOX_WEIGHT_MILLIGRAMS * request.other_piece_count);
    }

    let mut uses_insulating_material: bool = false;
    if request.requires_temperature_control {
        // 温控为整票属性:数量恒为 1(见函数文档规则 3)。
        lines.push(PackagingPlanLine::new(PACKAGING_VACUUM_FOIL_BAG, 1));
        total_gain_milligrams =
            total_gain_milligrams.saturating_add(VACUUM_FOIL_BAG_WEIGHT_MILLIGRAMS);
        uses_insulating_material = true;
    }

    let packaging_weight_gain: ShippingWeight =
        ShippingWeight::from_milligrams(total_gain_milligrams);
    let total_weight_with_packaging: ShippingWeight =
        request.net_cargo_weight.add(&packaging_weight_gain);

    PackagingPlan::new(
        lines,
        total_weight_with_packaging,
        packaging_weight_gain,
        uses_insulating_material,
    )
}

/// 返回一种材料对应的单位增重(毫克)。
///
/// 参数 `material`:包装材料。
/// 返回:单位增重(毫克)。
///
/// ## 这里为什么必须是 `if` 链而不是 `match`
///
/// `PackagingMaterial` 是**开放型结构体**,没有变体可以 `match`。
/// 因此这里用「材料编码字符串比较」的方式分派。
/// 代价是「新增材料时忘登记」不会编译报错——所以返回 0 单位增重,
/// 并在 [`unknown_material_warning`] 里给出可被门面汇总的提示。
///
/// 这正是开放型标签的另一面:**扩展自由,但需要配套一个运行期兜底**。
/// 本工程把这个兜底显式做出来,而不是假装问题不存在。
pub fn unit_gain_milligrams(material: &PackagingMaterial) -> i64 {
    if material.code() == PACKAGING_WOODEN_CRATE.code() {
        WOODEN_CRATE_WEIGHT_MILLIGRAMS
    } else if material.code() == PACKAGING_FOAM_BOX.code() {
        FOAM_BOX_WEIGHT_MILLIGRAMS
    } else if material.code() == PACKAGING_VACUUM_FOIL_BAG.code() {
        VACUUM_FOIL_BAG_WEIGHT_MILLIGRAMS
    } else {
        // 未登记材料:返回 0 而不是 panic,并由调用方决定如何提示。
        0
    }
}

/// 判断一种材料是否已被本子系统登记增重参数。
///
/// 参数 `material`:待检查材料。
/// 返回:已登记返回 `true`。
///
/// 用于工程外新增材料时的运行期兜底提示(见 [`unit_gain_milligrams`] 文档)。
pub fn is_material_registered(material: &PackagingMaterial) -> bool {
    material.code() == PACKAGING_WOODEN_CRATE.code()
        || material.code() == PACKAGING_FOAM_BOX.code()
        || material.code() == PACKAGING_VACUUM_FOIL_BAG.code()
}

/// 判断某个品类是否需要硬质外观包装(木质礼盒)。
///
/// 参数 `category_code`:品类编码。
/// 返回:需要返回 `true`。
///
/// 用品类编码而非品类对象:本函数只关心「是不是珠宝」这一个事实,
/// 不需要整个 CargoCategory 的其它属性,接口越窄越好替换。
pub fn requires_rigid_packaging(category_code: &str) -> bool {
    category_code == CARGO_JEWELRY.code()
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : charge_item.rs
//! 费用项(结算子系统的中立输入结构)。
//!
//! ## 为什么需要 `ChargeSource`
//!
//! 报表要按来源分组展示(运费 / 包装 / 单据 / 服务),
//! 也要能回答「增值服务费占总额多少」。
//! 若费用项不带来源,门面就得自己记「我刚刚传进去的那条是运费」——
//! 那是把汇总逻辑散在调用点,一旦顺序调整就会串味。
//!
//! `ChargeSource` 是**封闭枚举**:它决定报表的分组行为
//! (不同来源在报表里进入不同小节),属于「行为分派」,
//! 因此用枚举而非开放型标签。工程外新增费用来源时,
//! 应映射到既有来源(如自定义服务费归入 `ValueAddedService`),
//! 而不是扩充本枚举——这条纪律写在枚举文档里。

use crate::domain::CurrencyAmount;

/// 费用来源。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChargeSource {
    /// 基础运费。
    Freight,
    /// 包装材料费。
    Packaging,
    /// 单据工本费。
    Documentation,
    /// 增值服务费。
    ValueAddedService,
}

impl ChargeSource {
    /// 中文标签。
    pub const fn label(&self) -> &'static str {
        match self {
            ChargeSource::Freight => "基础运费",
            ChargeSource::Packaging => "包装材料费",
            ChargeSource::Documentation => "单据工本费",
            ChargeSource::ValueAddedService => "增值服务费",
        }
    }

    /// 排序权重:决定报表中的展示顺序。
    ///
    /// 顺序刻意是「运费 → 包装 → 单据 → 服务」,
    /// 与业务人员的阅读习惯一致(先看主要成本,再看附加项)。
    pub const fn display_order(&self) -> u32 {
        match self {
            ChargeSource::Freight => 0,
            ChargeSource::Packaging => 1,
            ChargeSource::Documentation => 2,
            ChargeSource::ValueAddedService => 3,
        }
    }

    /// 该来源是否计入「应付总额」。
    ///
    /// 全部来源都计入——本工程没有「仅展示不计入」的费用。
    /// 保留这个方法是为了给将来留出扩展点(如「预估费用」不计入),
    /// 且它使「哪些计入」这件事成为显式可审计的代码,而非隐含假设。
    pub const fn counts_toward_total(&self) -> bool {
        match self {
            ChargeSource::Freight
            | ChargeSource::Packaging
            | ChargeSource::Documentation
            | ChargeSource::ValueAddedService => true,
        }
    }
}

/// 一个费用项。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChargeItem {
    /// 费用项编码(如 `"FREIGHT"`、`"PACKAGING_WOODEN_CRATE"`)。
    code: &'static str,
    /// 费用项中文名。
    label: &'static str,
    /// 来源分类。
    source: ChargeSource,
    /// 金额。
    amount: CurrencyAmount,
    /// 计价依据说明(报表展示,让客户明白钱花在哪)。
    basis_text: &'static str,
}

impl ChargeItem {
    /// 构造一个费用项。
    pub const fn new(
        code: &'static str,
        label: &'static str,
        source: ChargeSource,
        amount: CurrencyAmount,
        basis_text: &'static str,
    ) -> Self {
        ChargeItem {
            code,
            label,
            source,
            amount,
            basis_text,
        }
    }

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

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

    /// 来源分类。
    pub const fn source(&self) -> ChargeSource {
        self.source
    }

    /// 金额。
    pub const fn amount(&self) -> CurrencyAmount {
        self.amount
    }

    /// 计价依据说明。
    pub const fn basis_text(&self) -> &'static str {
        self.basis_text
    }

    /// 返回一行展示文本。
    ///
    /// 这是「子系统自带的显示口径」,门面刻意不采用它——
    /// 报表的列宽、货币符号、缩进都属于**门面/表示层**的职责,子系统一旦
    /// 自行决定排版,换一个终端(PDF、Web)就要改子系统。
    /// 保留并开放它,是为了在第七幕与门面生成的表格**并列打印**,
    /// 让「子系统越界做排版」的后果肉眼可见。
    pub fn formatted(&self) -> String {
        format!("{}:{}({})", self.label, self.amount.formatted(), self.basis_text)
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : settlement_ledger.rs
//! 费用归集(结算子系统的计算部分)。
//!
//! ## 归集做三件事
//!
//! 1. 按来源分组求和(`by_source`);
//! 2. 计算应付总额(只累加 `counts_toward_total` 为真的项);
//! 3. 找出最大费用项(报表要突出「钱主要花在哪」)。
//!
//! ## 为什么不在这里做「折扣计算」
//!
//! 折扣是**商务政策**,不是结算机制。若把折扣规则塞进本函数,
//! 每换一次促销政策就要改结算代码。正确做法是:门面在传入费用项前
//! 把折扣算好(或作为一个负值费用项传入),结算只管归集。
//! 本工程的 `ChargeItem` 允许负金额,正是为此留的口子。

use crate::domain::{CurrencyAmount, CURRENCY_CHINESE_YUAN};

use super::charge_item::{ChargeItem, ChargeSource};

/// 单个来源的汇总。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceSubtotal {
    /// 来源。
    source: ChargeSource,
    /// 该来源金额合计。
    subtotal: CurrencyAmount,
    /// 该来源包含的费用项条数。
    item_count: usize,
}

impl SourceSubtotal {
    /// 构造一个来源汇总。
    pub const fn new(source: ChargeSource, subtotal: CurrencyAmount, item_count: usize) -> Self {
        SourceSubtotal {
            source,
            subtotal,
            item_count,
        }
    }

    /// 来源。
    pub const fn source(&self) -> ChargeSource {
        self.source
    }

    /// 金额合计。
    pub const fn subtotal(&self) -> CurrencyAmount {
        self.subtotal
    }

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

/// 结算结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SettlementResult {
    /// 全部费用项(按来源排序后)。
    items: Vec<ChargeItem>,
    /// 按来源的汇总(按展示顺序)。
    by_source: Vec<SourceSubtotal>,
    /// 应付总额。
    grand_total: CurrencyAmount,
    /// 最大费用项的下标(若列表非空)。
    largest_item_index: Option<usize>,
}

impl SettlementResult {
    /// 全部费用项。
    pub fn items(&self) -> &[ChargeItem] {
        &self.items
    }

    /// 按来源的汇总。
    pub fn by_source(&self) -> &[SourceSubtotal] {
        &self.by_source
    }

    /// 应付总额。
    pub const fn grand_total(&self) -> CurrencyAmount {
        self.grand_total
    }

    /// 费用项条数。
    pub fn item_count(&self) -> usize {
        self.items.len()
    }

    /// 最大费用项(若有)。
    pub fn largest_item(&self) -> Option<&ChargeItem> {
        self.largest_item_index
            .and_then(|index| self.items.get(index))
    }

    /// 返回某一来源的合计金额(不存在则返回零)。
    ///
    /// 参数 `source`:来源分类。
    /// 返回:该来源合计。
    ///
    /// 门面用它回答「增值服务费占总费用多少」这类比例问题。
    pub fn subtotal_of(&self, source: ChargeSource) -> CurrencyAmount {
        for subtotal in &self.by_source {
            if subtotal.source() == source {
                return subtotal.subtotal();
            }
        }
        // 未出现的来源返回零金额:比返回 Option 更好用,
        // 因为调用点绝大多数只需知道「没有就是 0」,不想为此写 match。
        CurrencyAmount::zero(CURRENCY_CHINESE_YUAN)
    }

    /// 返回某一来源占应付总额的万分比(总额为 0 时返回 0)。
    ///
    /// 参数 `source`:来源分类。
    /// 返回:万分比数值。
    pub fn share_basis_points(&self, source: ChargeSource) -> i64 {
        let total_minor_units: i64 = self.grand_total.minor_units();
        if total_minor_units == 0 {
            return 0;
        }
        let source_minor_units: i64 = self.subtotal_of(source).minor_units();
        // i128 中间量:金额 × 10000 后仍在 i64 内,但保持与全工程一致的口径。
        let numerator: i128 = source_minor_units as i128 * 10_000i128;
        (numerator / total_minor_units as i128) as i64
    }

    /// 费用项编码列表(报表用)。
    pub fn item_codes(&self) -> Vec<&'static str> {
        self.items.iter().map(|item| item.code()).collect()
    }
}

/// 对一组费用项做归集,产出结算结果。
///
/// 参数 `items`:费用项列表(所有权移交)。
/// 返回:结算结果。
///
/// ## 排序
///
/// 归集前先按 `source.display_order()` 稳定排序。
/// 用**稳定排序**(`sort_by` 在 Rust 中即为稳定)的意图是:
/// 同一来源内部的原始顺序被保留——门面传进来的顺序通常已按业务重要性排好,
/// 不应被结算子系统打乱。
pub fn settle_charges(mut items: Vec<ChargeItem>) -> SettlementResult {
    // 稳定排序:同来源项保持原有相对顺序。
    items.sort_by_key(|item| item.source().display_order());

    // ---------- 按来源汇总 ----------
    // 遍历排序后的列表,相邻同来源即归入同一组。
    // 不用 HashMap:来源只有 4 种,且线性扫描能自然保序。
    let mut by_source: Vec<SourceSubtotal> = Vec::new();
    for item in &items {
        let source: ChargeSource = item.source();
        // 查找已有分组(最后一个即可,因为已排序)。
        match by_source.last_mut() {
            Some(last) if last.source() == source => {
                // 同来源:累加金额与条数。
                // 注意这里需要修改 last,故先在局部算好再整体替换,
                // 避免同时借用 last 的两个字段(借用检查器会拒绝)。
                let updated_subtotal: CurrencyAmount = last.subtotal().add(&item.amount());
                let updated_count: usize = last.item_count() + 1;
                *last = SourceSubtotal::new(source, updated_subtotal, updated_count);
            }
            _ => {
                // 新来源:开一个新分组。
                by_source.push(SourceSubtotal::new(source, item.amount(), 1));
            }
        }
    }

    // ---------- 计算应付总额 ----------
    let mut grand_total: CurrencyAmount = CurrencyAmount::zero(CURRENCY_CHINESE_YUAN);
    for item in &items {
        // 只累加「计入总额」的来源(当前全部计入,但保留这个判断
        // 使将来出现「仅展示」的预算项时无需改动本函数结构)。
        if item.source().counts_toward_total() {
            grand_total = grand_total.add(&item.amount());
        }
    }

    // ---------- 找最大费用项 ----------
    // 用「先取 0 再比较」而不是 `max_by_key`:需要的是下标,
    // 且要保证在金额相同的情况下取**先出现的**(报表更稳定)。
    let mut largest_item_index: Option<usize> = None;
    for (index, item) in items.iter().enumerate() {
        match largest_item_index {
            None => largest_item_index = Some(index),
            Some(current_best) => {
                let current_amount: i64 = items[current_best].amount().minor_units();
                if item.amount().minor_units() > current_amount {
                    largest_item_index = Some(index);
                }
            }
        }
    }

    SettlementResult {
        items,
        by_source,
        grand_total,
        largest_item_index,
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : calendar_date.rs
//! 日历日(不含时刻、不含时区)与天数推算。
//!
//! ## 为什么不用 `chrono`
//!
//! 门面里要算的是「预计送达日」「最迟装运日」这类**日历日**概念:
//! 「2026-10-05 往后推 3 天是哪一天」。它**不是**时间戳,也不涉及时区,
//! 用 `DateTime<Utc>` 表达会引入两处错配:
//!
//! 1. 一个日历日对应 24 小时,但跨夏令时时对应 23 或 25 小时——
//!    用「加 24 小时」算出来的日期可能差一天;
//! 2. 演示数值会随系统时区漂移,无法人工复核。
//!
//! 因此这里用一个三元组 `(年, 月, 日)` 自实现闰年与逐日推进,
//! 全部为纯整数运算,结果与运行环境无关。
//!
//! ## 边界行为
//!
//! [`CalendarDate::from_ymd`] 是 `const fn`,可在常量上下文使用;
//! 它对非法日号采取**就近钳制**(如 2 月 30 日 → 2 月 28/29 日)而不是 panic,
//! 理由见方法文档。

/// 日期所属月份的天数表(非闰年),索引 0 刻意留空以便直接用月份取。
///
/// 为什么用长度为 13 的数组而不是 12:让 `MONTH_LENGTHS[month]` 直接成立,
/// 省掉一次 `month - 1` 的心算,减少差一错误。
const MONTH_LENGTHS_IN_COMMON_YEAR: [u32; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

/// 某个公历年份是否为闰年。
///
/// 判据(格里高利历):能被 4 整除 **且** 不能被 100 整除,或能被 400 整除。
/// 参数 `year`:公历年份(如 2026)。
/// 返回:是闰年返回 `true`。
pub const fn is_leap_year(year: i32) -> bool {
    // 用取余表达式直译判据,避免引入辅助函数掩盖逻辑。
    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}

/// 某个公历年份中指定月份的天数。
///
/// 参数 `year`:公历年份;`month`:月份(1..=12)。
/// 返回:该月天数;若 `month` 越界则返回 0(调用方应避免传入非法月份)。
pub const fn days_in_month(year: i32, month: u32) -> u32 {
    // 越界保护:数组长度 13,下标 0..=12。月份 0 是本表的哨兵位。
    if month == 0 || month > 12 {
        return 0;
    }
    // 二月在闰年为 29 天,其余月份与平年一致。
    if month == 2 && is_leap_year(year) {
        return 29;
    }
    MONTH_LENGTHS_IN_COMMON_YEAR[month as usize]
}

/// 一个不含时刻、不含时区的日历日。
///
/// 三个字段都是私有且不可变的:外部只能通过 [`CalendarDate::from_ymd`]
/// 构造、通过 [`CalendarDate::add_days`] 推进,保证任何时候拿到的实例
/// 都是「合法日期」——不会出现 `month = 13` 这种值在系统里流动。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CalendarDate {
    /// 公历年(如 2026)。允许负数以表达公元前,本工程不涉及。
    year: i32,
    /// 公历月(1..=12)。
    month: u32,
    /// 公历日(1..=该月天数)。
    day: u32,
}

impl CalendarDate {
    /// 由年、月、日构造一个日历日。
    ///
    /// 参数 `year` / `month` / `day`:公历年月日。
    /// 返回:构造出的日期。
    ///
    /// **越界处理策略:就近钳制而非 panic**。理由是这些值可能来自
    /// 上游业务数据(如承运日历配置),在报表场景下「2 月 30 日」
    /// 更应该表现为 2 月末,而不是让整个门面调用 panic 崩掉。
    /// 若将来需要严格校验,应在上游入口做,而不是让这个基础工具承担。
    pub const fn from_ymd(year: i32, month: u32, day: u32) -> Self {
        // 先把月份钳到 1..=12。
        let clamped_month: u32 = if month < 1 {
            1
        } else if month > 12 {
            12
        } else {
            month
        };
        // 再按钳制后的月份取天数上界。
        let maximum_day: u32 = days_in_month(year, clamped_month);
        // 日号钳到 1..=maximum_day。
        let clamped_day: u32 = if day < 1 {
            1
        } else if day > maximum_day {
            maximum_day
        } else {
            day
        };
        CalendarDate {
            year,
            month: clamped_month,
            day: clamped_day,
        }
    }

    /// 读取公历年。
    pub fn year(&self) -> i32 {
        self.year
    }

    /// 读取公历月。
    pub fn month(&self) -> u32 {
        self.month
    }

    /// 读取公历日。
    pub fn day(&self) -> u32 {
        self.day
    }

    /// 返回往后推 `offset_days` 天之后的日期。
    ///
    /// 参数 `offset_days`:天数偏移(可为负,表示往前推)。
    /// 返回:新日期(`self` 本身不变——`CalendarDate` 是值语义的不可变对象)。
    ///
    /// 实现方式为**逐日推进**而不是「用日期算法换算儒略日」:
    /// 逐日在调试时可读性高得多(循环里能直接看出闰年跳变),
    /// 而本工程的偏移量都是个位到两位数天数,性能无差异。
    /// 若将来出现上千天的偏移,应改走儒略日算法(届时也需保留本函数作为对照)。
    pub fn add_days(&self, offset_days: i32) -> Self {
        // 复制当前值到局部可变状态;用 i64 是为了避免极端偏移下的中间溢出。
        let mut cursor_year: i64 = self.year as i64;
        let mut cursor_month: i64 = self.month as i64;
        let mut cursor_day: i64 = self.day as i64;
        // remaining 为「还剩多少天要推进」;负数时按逆方向处理。
        let mut remaining: i64 = offset_days as i64;

        // 统一转成「向前推进」循环:负数偏移等价于反复退一天。
        while remaining > 0 {
            // 当前月的天数上界(注意这里 year 需要转回 i32 传给 days_in_month)。
            let maximum_day: i64 = days_in_month(cursor_year as i32, cursor_month as u32) as i64;
            if cursor_day < maximum_day {
                // 还没到月末,直接加一天。
                cursor_day += 1;
            } else {
                // 已到月末:进入下个月的第一天。
                cursor_day = 1;
                if cursor_month < 12 {
                    cursor_month += 1;
                } else {
                    // 年末:进入下一年的 1 月。
                    cursor_month = 1;
                    cursor_year += 1;
                }
            }
            remaining -= 1;
        }

        while remaining < 0 {
            // 反向推进:逐日退。
            if cursor_day > 1 {
                cursor_day -= 1;
            } else {
                // 已到月初:退到上个月的最后一天。
                if cursor_month > 1 {
                    cursor_month -= 1;
                } else {
                    // 年初:退到上一年的 12 月。
                    cursor_month = 12;
                    cursor_year -= 1;
                }
                cursor_day = days_in_month(cursor_year as i32, cursor_month as u32) as i64;
            }
            remaining += 1;
        }

        CalendarDate {
            year: cursor_year as i32,
            month: cursor_month as u32,
            day: cursor_day as u32,
        }
    }

    /// 返回与另一日期相差的天数(`self` 早于 `other` 时为正)。
    ///
    /// 参数 `other`:另一个日期。
    /// 返回:`other` 减去 `self` 的天数差(可能为负)。
    ///
    /// 实现方式同样走逐日推进,与 [`CalendarDate::add_days`] 共享同一套日历规则,
    /// 因此不会出现「加天数与求差值用了两套闰年判断」这种隐性不一致。
    pub fn days_until(&self, other: &CalendarDate) -> i32 {
        // 若两日期相同,直接返回 0,省去循环。
        if self == other {
            return 0;
        }

        // 判断方向,决定用哪一种推进,避免在两个循环里各写一遍逻辑。
        let forward: bool = other.year > self.year
            || (other.year == self.year && (other.month > self.month
                || (other.month == self.month && other.day > self.day)));

        if forward {
            // 从 self 出发逐日前进,直到与 other 相等,统计步数。
            let mut cursor: CalendarDate = *self;
            let mut distance: i32 = 0;
            while cursor != *other {
                cursor = cursor.add_days(1);
                distance += 1;
                // 防御性上界:跨世纪比较误用时不至于死循环。
                // 取 366 * 200 ≈ 200 年,业务上远超合理范围。
                if distance > 366 * 200 {
                    break;
                }
            }
            distance
        } else {
            // 反向:从 other 前进到 self,步数取负。
            let mut cursor: CalendarDate = *other;
            let mut distance: i32 = 0;
            while cursor != *self {
                cursor = cursor.add_days(1);
                distance -= 1;
                if distance < -(366 * 200) {
                    break;
                }
            }
            distance
        }
    }

    /// 返回 `YYYY-MM-DD` 形式的文本。
    ///
    /// 固定用 `{:02}` 补零,保证「10 月 5 日」写成 `2026-10-05` 而不是
    /// `2026-10-5`——报表里日期列宽度必须稳定,否则表格会随月份位数跳动。
    pub fn formatted(&self) -> String {
        // 注意:这里刻意走 getter 而不是直接读字段,
        // 使 getter 成为「报表路径上的必要访问」,避免它们变成死代码。
        format!(
            "{:04}-{:02}-{:02}",
            self.year(),
            self.month(),
            self.day()
        )
    }

    /// 返回不含年份的中文月日文本,如「10 月 5 日」。
    ///
    /// 用于「发货日 → 到达日」这种同年内的紧凑展示,省掉重复的年份。
    pub fn month_day_text(&self) -> String {
        format!("{} 月 {} 日", self.month(), self.day())
    }

    /// 返回该日期是星期几(0 = 周一 ... 6 = 周日)。
    ///
    /// 实现用的是 **Sakamoto 算法**(查表版):先用一张「月份偏移表」
    /// `t[m-1]` 把月份贡献折算成一个常数,再叠上年份、世纪修正。
    /// 选它而不选「累加天数再取模」的理由是——后者需要一个已知参照日,
    /// 而参照日的正确性本身又要被验证,链条更长。
    ///
    /// ⚠️ 踩坑记录(本工程实测踩到过):
    /// 曾误用**算术近似式** `(26 * (m + 1)) / 10` 来代替偏移表,
    /// 这个式子只是 `floor(2.6 * (m + 1))` 的整数写法,
    /// 与查表值在 3 月、9 月等多个月份上并不相等,会导致**整年星期错一位**。
    /// 例如 2026-10-05 是周一,错版算法算出「周二」。
    /// 教训:这类有公认表格的算法,宁可把表写出来,也不要用「看起来等价」的算式。
    pub fn weekday_index(&self) -> u32 {
        // 标准 Sakamoto 月份偏移表(下标 0 = 1 月):
        // 它把「每月 1 日的星期基线」按累计日数取模后的偏移量预先列好。
        const MONTH_OFFSET_TABLE: [i32; 12] =
            [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
        // 1 月、2 月视作上一年的 13、14 月(便于统一处理闰日),
        // 此时 `adjusted_year` 要减 1,与标准实现的 `if m < 3 { y -= 1 }` 等价。
        let adjusted_year: i32 = if self.month() < 3 {
            self.year() - 1
        } else {
            self.year()
        };
        // 计算:年 + 年/4 - 年/100 + 年/400 + 月偏移 + 日,再取模 7。
        // 结果 0 = 周日、1 = 周一 …… 6 = 周六。
        let raw: i32 = (adjusted_year
            + adjusted_year / 4
            - adjusted_year / 100
            + adjusted_year / 400
            + MONTH_OFFSET_TABLE[(self.month() - 1) as usize]
            + self.day() as i32)
            % 7;
        let sunday_based: i32 = ((raw % 7) + 7) % 7;
        // 转换到「0 = 周一 ... 6 = 周日」:
        // 周日基准里的 0 应变成 6,其余 n 变成 n - 1。
        if sunday_based == 0 {
            6
        } else {
            (sunday_based - 1) as u32
        }
    }

    /// 返回星期的中文单字(一 / 二 / 三 / 四 / 五 / 六 / 日)。
    pub fn weekday_text(&self) -> &'static str {
        // 下标 0 对应周一,与 weekday_index 的约定一致。
        const WEEKDAY_LABELS: [&str; 7] = ["一", "二", "三", "四", "五", "六", "日"];
        WEEKDAY_LABELS[self.weekday_index() as usize]
    }

    /// 判断该日期是否为周末(周六或周日)。
    ///
    /// 承运与清关环节在周末的处理时效不同,门面会据此在时间线上追加提示。
    pub fn is_weekend(&self) -> bool {
        // 索引 5 = 周六、6 = 周日。
        let index: u32 = self.weekday_index();
        index >= 5
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : deterministic_code.rs
//! 确定性编码派生工具(FNV-1a 64 位哈希)。
//!
//! ## 为什么需要「确定性」哈希
//!
//! 门面演示里会产出运单号、报关单号、批次号这类标识。它们必须满足两点:
//!
//! 1. **同一份输入永远得到同一个号**——否则每次 `cargo run` 输出都不同,
//!    数值核算与回归比对就无从谈起;
//! 2. **不用随机数**——`rand` 会引入依赖,且随机性本身就是可复现性的敌人。
//!
//! 因此改用「对输入做确定性哈希,取前若干位十六进制」。选 FNV-1a 而不是
//! 标准库 `DefaultHasher` 的理由是:
//!
//! - `DefaultHasher` 的算法**不承诺跨版本稳定**(官方文档明确说明),
//!   一旦 Rust 升级,历史单号会变,这在本工程不可接受;
//! - FNV-1a 实现只有几行、常数公开、可被审阅,完全够用。
//!
//! ## 注意这不是加密哈希
//!
//! FNV-1a 可被轻易构造碰撞,**仅供演示的编号派生使用**。
//! 任何安全相关场景(签名、令牌)必须改用加密哈希,本工具不适用。

/// FNV-1a 64 位偏移基准(官方常数)。
///
/// 这个值是 FNV 规范固定给出的,不可随意改动——改了就与标准实现不兼容。
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;

/// FNV-1a 64 位质数(官方常数)。
///
/// 选这个质数的原因是它能让低位充分扩散——换成 2 的幂会让低位信息丢失,
/// 导致「输入只差最后一位时哈希也只差最后一位」的退化。
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;

/// 计算一段字节串的 FNV-1a 64 位哈希。
///
/// 参数 `bytes`:输入字节切片。
/// 返回:64 位哈希值。
///
/// 这是 `const fn`,因此可以在常量上下文里使用——
/// 本工程的部分预置常量(如默认前缀)就依赖这一点。
pub const fn fnv1a_64(bytes: &[u8]) -> u64 {
    let mut hash: u64 = FNV_OFFSET_BASIS;
    let mut index: usize = 0;
    while index < bytes.len() {
        // 先异或再乘:这是 FNV-1a 与 FNV-1 的唯一区别。
        // 顺序反了(先乘后异或)就退化成 FNV-1,扩散性差一截。
        hash ^= bytes[index] as u64;
        // wrapping_mul 是必须的:64 位乘法必然溢出,
        // 用普通 `*` 会在 debug 构建下 panic。溢出是算法设计的一部分(模 2^64)。
        hash = hash.wrapping_mul(FNV_PRIME);
        index += 1;
    }
    hash
}

/// 把若干文本片段拼成一份「种子字符串」,供后续哈希使用。
///
/// 参数 `segments`:任意数量的片段(如国家码、城市、日期、序号)。
/// 返回:用 `|` 连接的种子字符串。
///
/// **为什么用 `|` 作分隔符**:它能避免「拼接歧义」——
/// 若直接连接,`("AB", "C")` 与 `("A", "BC")` 会得到同一个种子,
/// 从而派生出同一个编号。这类碰撞在业务系统里会变成难排查的数据问题。
pub fn build_seed(segments: &[&str]) -> String {
    // 先估算容量,减少反复扩容(对演示无性能意义,但注释里说明意图便于维护)。
    let estimated_capacity: usize = segments.iter().map(|segment| segment.len() + 1).sum();
    let mut seed: String = String::with_capacity(estimated_capacity);
    for (position, segment) in segments.iter().enumerate() {
        if position > 0 {
            seed.push('|');
        }
        seed.push_str(segment);
    }
    seed
}

/// 由种子与长度派生一个**定长**的十六进制编码。
///
/// 参数 `prefix`:编码前缀(如 `"SF"` 表示顺丰);`seed`:种子字符串;
/// `hex_length`:主体部分的十六进制位数(会被钳制到 1..=16)。
/// 返回:形如 `SF-3A9C1B04` 的编码。
///
/// 为什么把长度钳到 16:64 位哈希最多表达 16 个十六进制位,
/// 要求更长的位数就必然要重复或补零,那是虚假的熵,不如显式拒绝。
pub fn derive_code(prefix: &str, seed: &str, hex_length: usize) -> String {
    // 钳制位数,避免越界;同时保证至少 1 位,不至于产出空主体。
    let clamped_length: usize = hex_length.clamp(1, 16);
    let hash_value: u64 = fnv1a_64(seed.as_bytes());
    // 取高 16 位十六进制,再截取需要的长度。
    // 选高位而不是低位:FNV-1a 的高位扩散更充分。
    let full_hex: String = format!("{:016X}", hash_value);
    let body: &str = &full_hex[..clamped_length];
    if prefix.is_empty() {
        body.to_string()
    } else {
        format!("{}-{}", prefix, body)
    }
}

/// 由种子派生一个 8 位十六进制的短指纹。
///
/// 参数 `seed`:种子字符串。
/// 返回:8 位大写十六进制串。
///
/// 用途:在报表里给「同一个对象的不同阶段快照」标注身份,
/// 8 位(32 比特)在单次演示的规模下碰撞概率可忽略。
pub fn short_fingerprint(seed: &str) -> String {
    // 复用 derive_code,前缀传空串以免多出分隔符。
    derive_code("", seed, 8)
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Facade Pattern 外观模式
//!# 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/5 13:45 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : FacadePattern
//!# File      : text_layout.rs
//! 中文(CJK)宽度感知的定宽文本工具。
//!
//! ## 为什么必须自己实现
//!
//! Rust 标准库的格式化填充(`{:<20}`)按 **char 个数**补空格,而中日韩字符
//! 在等宽终端里占 **2 个显示列**。于是 `format!("{:<10}", "翡翠手镯")`
//! 得到的是「6 列内容 + 4 空格」= 10 列,而 `format!("{:<10}", "Jade")`
//! 得到的是「4 列内容 + 6 空格」= 10 列——两者都以「10 列」为目标,
//! 看起来没问题;但一旦列里混排中文与英文,**同一个 `{:<10}` 得到的显示宽度
//! 就不再相等**,表格会呈锯齿状错位。
//!
//! 本工程的所有报表都走这里的 `pad_right` / `pad_left` / `pad_center`,
//! 保证「填充后的显示宽度」才是相等的。
//!
//! ## 关键约定
//!
//! - [`pad_right`] 等函数在**内容本身已超过目标宽度**时**不截断**,
//!   而是原样返回(宁可撑破表格也不静默丢数据)——报表里少一个字符
//!   比表格错位严重得多。需要截断时调用方必须显式调 [`truncate_to_width`]。
//! - [`truncate_to_width`] 在剩余空间放不下一个宽字符时**放弃半个汉字**
//!   (不会把一个汉字切成两半的乱码)。

/// 判断一个字符在等宽终端里是否占 2 个显示列。
///
/// 判定依据是 Unicode 的 East Asian Width 属性为 `W`(Wide)或 `F`(Fullwidth)
/// 的区段。这里用区间表近似,**区间必须互不重叠**——
/// 若两个区间有交叠,`matches!` 的后续分支会被判定为「不可达模式」而告警
/// (这是本工程踩过的坑,见下方注释)。
///
/// 参数 `character`:待判定的单个字符(按 Unicode 标量值传入)。
/// 返回:占 2 列返回 `true`,占 1 列返回 `false`。
pub fn is_wide_character(character: char) -> bool {
    // 先取码点,便于用区间比较。用 u32 是因为下面的区间是按码点写的。
    let code_point: u32 = character as u32;

    // 注意:下面的区间已按「上界递增、互不重叠」排列。
    // 若把 U+4E00..=U+9FFF(CJK 统一表意文字)与 U+3000..=U+303F
    // 的顺序写反,编译器会以「不可达模式」告警——这一点务必保持有序。
    matches!(code_point,
        // CJK 标点(。、「」等)——中文标点占两列
        0x3000..=0x303F
        // 平假名
        | 0x3040..=0x30FF
        // 注音符号扩展、谚文兼容字母
        | 0x3100..=0x312F
        | 0x3130..=0x318F
        // CJK 统一表意文字(常用汉字主区)
        | 0x4E00..=0x9FFF
        // 谚文音节(韩文)
        | 0xAC00..=0xD7A3
        // CJK 兼容表意文字
        | 0xF900..=0xFAFF
        // 全角 ASCII 变体
        | 0xFF00..=0xFF60
        // 全角符号变体
        | 0xFFE0..=0xFFE6
        // 国标扩展区(部分生僻字)
        | 0x20000..=0x2FFFD
        | 0x30000..=0x3FFFD
    )
}

/// 计算一段文本在等宽终端里占用的**显示列数**(不是字符数)。
///
/// 参数 `text`:任意 UTF-8 文本切片。
/// 返回:显示列总数(宽字符计 2,其余计 1)。
///
/// 举例:`display_width("翡翠手镯")` 返回 8;`display_width("Jade")` 返回 4。
pub fn display_width(text: &str) -> usize {
    // 逐字符累加宽度;累加值用 usize,因为列数不可能是负数。
    // 这里刻意不按字节长度算(那会把中文算成 3 列,更离谱)。
    let mut total_width: usize = 0;
    for character in text.chars() {
        total_width += if is_wide_character(character) { 2 } else { 1 };
    }
    total_width
}

/// 把文本**右填充**到指定显示宽度(文本靠左,空格在右)。
///
/// 参数 `text`:原始文本;`target_width`:目标显示宽度(列)。
/// 返回:填充后的 `String`。
///
/// **超出不截断**(见模块文档):若 `display_width(text) >= target_width`,
/// 原样返回文本。这一条的代价是表格可能被撑破,
/// 但收益是「没有任何数据会在排版环节被悄悄丢掉」。
pub fn pad_right(text: &str, target_width: usize) -> String {
    let current_width: usize = display_width(text);
    // 用 saturating_sub 防止「当前宽度已超标」时下溢 panic;
    // 下溢为 0 时下面的循环不执行,等价于原样返回。
    let padding_count: usize = target_width.saturating_sub(current_width);
    let mut result: String = String::with_capacity(text.len() + padding_count);
    result.push_str(text);
    // 填充用半角空格:1 个空格恰好 1 列,便于精确对齐。
    for _ in 0..padding_count {
        result.push(' ');
    }
    result
}

/// 把文本**左填充**到指定显示宽度(空格在左,文本靠右)。
///
/// 参数 `text`:原始文本;`target_width`:目标显示宽度(列)。
/// 返回:填充后的 `String`。
///
/// 金额列、序号列一律用这个函数右对齐,避免数字位数变化时表格晃动。
pub fn pad_left(text: &str, target_width: usize) -> String {
    let current_width: usize = display_width(text);
    let padding_count: usize = target_width.saturating_sub(current_width);
    let mut result: String = String::with_capacity(text.len() + padding_count);
    for _ in 0..padding_count {
        result.push(' ');
    }
    result.push_str(text);
    result
}

/// 把文本**居中**填充到指定显示宽度(两侧均分空格)。
///
/// 参数 `text`:原始文本;`target_width`:目标显示宽度(列)。
/// 返回:填充后的 `String`。
///
/// 当两侧余量是奇数时,**多出的那一个空格放在右侧**(左少右多)——
/// 这是刻意固定的规则,否则标题在不同行之间会左右跳动。
pub fn pad_center(text: &str, target_width: usize) -> String {
    let current_width: usize = display_width(text);
    let padding_count: usize = target_width.saturating_sub(current_width);
    // 左侧取整除(向下取整),余数自然落到右侧。
    let left_padding: usize = padding_count / 2;
    let right_padding: usize = padding_count - left_padding;
    let mut result: String = String::with_capacity(text.len() + padding_count);
    for _ in 0..left_padding {
        result.push(' ');
    }
    result.push_str(text);
    for _ in 0..right_padding {
        result.push(' ');
    }
    result
}

/// 把文本按**显示宽度**截断到目标宽度以内。
///
/// 参数 `text`:原始文本;`target_width`:允许的最大显示宽度(列)。
/// 返回:截断后的 `String`(若原文本本就在范围内,则原样返回)。
///
/// 关键行为:**绝不切出半个汉字**。若剩余宽度只剩 1 列而下一个字符是宽字符,
/// 则直接停止(宁可少一列,也不产生半个汉字导致的乱码或错位)。
pub fn truncate_to_width(text: &str, target_width: usize) -> String {
    let mut result: String = String::new();
    let mut used_width: usize = 0;

    for character in text.chars() {
        let character_width: usize = if is_wide_character(character) { 2 } else { 1 };
        // 放得下才推进;放不下就整体停止(不是跳过该字符继续,
        // 否则截断结果会前后拼接出与原意不符的文本)。
        if used_width + character_width > target_width {
            break;
        }
        result.push(character);
        used_width += character_width;
    }

    result
}

/// 生成一条指定显示宽度的水平分隔线。
///
/// 参数 `character`:构成分隔线的字符(中文报表里常用 `─`);
/// `target_width`:目标显示宽度(列)。
/// 返回:分隔线字符串。
///
/// 存在的理由:报表里有大量分隔线,若每处都手写
/// `"─".repeat(74)`,一旦宽度口径变化就要改几十处,且中文字符的宽度
/// 与列数不是 1:1,容易算错。这里统一用 `display_width` 递推,
/// 保证分隔线与数据行的宽度**严格相等**。
pub fn horizontal_rule(character: char, target_width: usize) -> String {
    // 单字符自身的宽度(若传入的是宽字符,则每次要推进 2 列)。
    let character_width: usize = if is_wide_character(character) { 2 } else { 1 };
    // 防御除零:宽字符为 2,半角为 1,都不会是 0,但显式判一次更安全。
    if character_width == 0 {
        return String::new();
    }
    let repeat_count: usize = target_width / character_width;
    let mut result: String = String::with_capacity(target_width);
    for _ in 0..repeat_count {
        result.push(character);
    }
    result
}

  

posted @ 2026-10-06 07:06  ®Geovin Du Dream Park™  阅读(3)  评论(0)    收藏  举报