rust: Facade Pattern

**摘要:**本文以 Rust 实现的外观模式(Facade Pattern)为例,展示如何通过门面统一封装多个子系统,并借助开放型标签(开放型结构体与常量)在工程外扩展货物类别、国家/地区、币种等维度而不改动领域层。全文围绕「扩展维度靠加常量,不靠改代码分支」这一核心思想,覆盖货物类别、国家代码、货币金额、结算、承运时间线等子系统的设计与调用,并通过第七幕命令行演示验证每个公开方法都被真实调用,从而以编译器作为架构审计工具实现零告警。

项目结构

rust 复制代码
//!# 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 &gt;= absolute_denominator {
    // 该进位。方向取决于商的符号:
    // 商为正 → 加 1;商为负(或零而分子为负)→ 减 1,从而远离零。
    if numerator &lt; 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 &gt; 0 {
        result.insert(0, ',');
    }
    // 用 insert_str(0, ...) 从左侧拼接:等价于倒序构造,最后得到正序结果。
    result.insert_str(0, &amp;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 =&gt; ..., PackingList =&gt; ... },
//! 那么新增一种单据就必须改子系统代码,扩展性就没了。
//! 本工程的所有扩展维度(国家、品类、材料、单据、等级)都遵守这条纪律。
/// 一种单据类型。
#[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(&amp;self) -&gt; &amp;'static str {
    self.code
}

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

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

/// 模板标识。
pub const fn template_identifier(&amp;self) -&gt; &amp;'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 点都会编译失败,逼开发者逐一决定新等级的行为。
//! 若做成开放型结构体,新等级会静默落入某个 _ =&gt; 兜底分支------
//! 那是 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(&amp;self) -&gt; bool {
    match self {
        // 提示与警告都不阻断------警告留给人工判断,避免系统过度拦截。
        EventSeverity::Info | EventSeverity::Warning =&gt; false,
        // 只有严重级阻断。
        EventSeverity::Critical =&gt; true,
    }
}

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

/// 在报表中使用的标记符号。
pub const fn marker(&amp;self) -&gt; &amp;'static str {
    match self {
        EventSeverity::Info =&gt; "·",
        EventSeverity::Warning =&gt; "!",
        EventSeverity::Critical =&gt; "×",
    }
}
}
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(&amp;self) -&gt; &amp;'static str {
    self.code
}

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

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

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

/// 是否为保温材料。
pub const fn is_temperature_insulating(&amp;self) -&gt; 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() -&gt; Self {
    Ratio { basis_points: 0 }
}

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

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

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

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

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

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

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

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

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

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

/// 是否承诺具体时刻。
pub const fn guarantees_exact_time(&amp;self) -&gt; 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) -&gt; Self {
    ShippingWeight {
        milligrams: grams * MILLIGRAMS_PER_GRAM + extra_milligrams,
    }
}

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

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

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

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

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

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

/// 求和(对一个重量序列)。
///
/// 参数 `weights`:重量切片。
/// 返回:总重量。
///
/// 放在本类型上而不是让调用方写 `iter().fold(...)`:
/// 「整票总重」是各个子系统都要算的高频量(计费、承重校验、报关),
/// 集中一处可避免各处累加口径不一致(如有的漏算了包装重量)。
pub fn sum(weights: &amp;[ShippingWeight]) -&gt; 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(&amp;self, rate_per_kilogram: i64) -&gt; 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(&amp;self) -&gt; String {
    let absolute: i64 = self.milligrams.abs();
    let grams: i64 = absolute / MILLIGRAMS_PER_GRAM;
    let milligram_remainder: i64 = absolute % MILLIGRAMS_PER_GRAM;
    let sign: &amp;str = if self.milligrams &lt; 0 { "-" } else { "" };
    format!(
        "{}{}.{:03} g",
        sign, grams, milligram_remainder
    )
}

/// 返回以公斤为单位的展示文本(保留 3 位小数)。
///
/// 举例:`1045000` 毫克 → `"1.045 kg"`。
/// 保留 3 位小数意味着精确到克,这与「珠宝按克报价」的业务口径一致。
pub fn formatted_kilograms(&amp;self) -&gt; 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: &amp;str = if self.milligrams &lt; 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&lt;CapacityLedger&gt;
///
/// [DispatchPort] 的 plan_options 接收 &amp;self(门面只需读能力,
/// 不应给实现方强加可变性要求)。但运力台账在规划过程中需要占用运力
/// (有状态变更)。用 RefCell 在内部实现「内部可变性」,
/// 就能在 &amp;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(&amp;request, &amp;mut ledger_borrow);

    // 把子系统的 CarrierOption 翻译成中立视图。
    planning_result
        .options
        .iter()
        .map(translate_carrier_option)
        .collect()
}
}
/// 内置运力查询端口。
///
/// ## 为什么与 [BuiltinDispatchPort] 分开
///
/// 因为它们的生命周期需求不同:查询运力是只读的,
/// 而 [BuiltinDispatchPort] 持有 RefCell 内部可变状态。
/// 拆开后,只读场景不必触碰可变状态。
///
/// 代价是两者必须共享同一份台账。本实现的做法是:
/// 查询端口不持有台账,而是由门面在构造时传入同一个台账的共享引用。
/// 但 CapacityLedger 不是 Rc,为保持简单,
/// 本工程让查询端口也持有一份台账快照式的独立实例------
/// 见下方 utilization 字段的说明。
pub struct BuiltinCapacityPort {
/// 台账快照:记录构造时各承运商的占用率(万分比)。
/// 用「快照」而不是共享台账,避免为只读查询引入 Rc&lt;RefCell&lt;..&gt;&gt;
/// 这类共享可变状态(那是环状引用的高发地,见技能里的行为型陷阱)。
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: &amp;CapacityLedger) -&gt; Self {
    let snapshot: Vec&lt;(&amp;'static str, i64)&gt; = 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(&amp;self) -&gt; ShippingWeight {
    ShippingWeight::from_milligrams(self.unit_weight.milligrams())
}

/// 该行总重量(含件数)。
///
/// 与 [`CargoLine::total_weight`] 的区别:本方法乘件数。
/// 两个方法都存在是因为「单件重量」与「行总重」在报表里都要展示,
/// 显式区分比让调用方自己乘更不容易出错。
pub fn line_total_weight(&amp;self) -&gt; 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(&amp;self) -&gt; 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(&amp;self) -&gt; ShippingWeight {
    let mut total_milligrams: i64 = 0;
    for line in &amp;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(&amp;self) -&gt; i64 {
    let mut total: i64 = 0;
    for line in &amp;self.cargo_lines {
        total = total.saturating_add(line.line_total_declared_value_minor_units());
    }
    total
}

/// 珠宝类件数(用于包装规划:决定用多少个硬质礼盒)。
pub fn jewelry_piece_count(&amp;self) -&gt; i64 {
    let mut total: i64 = 0;
    for line in &amp;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(&amp;self) -&gt; i64 {
    // 总件数减珠宝件数,保证两者之和恒等于总件数(不重不漏)。
    self.total_piece_count() - self.jewelry_piece_count()
}

/// 是否包含需要正式报关的品类。
pub fn has_declarable_cargo(&amp;self) -&gt; 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(&amp;self) -&gt; bool {
    self.total_declared_value_minor_units() &gt;= HIGH_VALUE_THRESHOLD_MINOR_UNITS
}

/// 是否为跨境运输。
pub fn is_cross_border(&amp;self) -&gt; 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&lt;dyn ...Port&gt;)+ 一个运力台账端口。
//! 它不持有任何具体子系统类型------这是「子系统可整体替换」的结构保证。
//! 具体绑定发生在构造处([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) -&gt; 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&lt;dyn CapacityPort&gt;,
) -&gt; 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(&amp;self, request: &amp;ShipmentRequest) -&gt; 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(&amp;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&lt;CarrierOptionView&gt; =
        self.dispatch_port.plan_options(&amp;dispatch_input);

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

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

    // ---------- 步骤 3:时间线 ----------
    let timeline_view = self.timeline_port.build_timeline_view(&amp;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&lt;DocumentRequirementView&gt; =
        self.document_port.compile_checklist(&amp;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&lt;SettlementInputItem&gt; = self.build_charge_items(
        request,
        &amp;packaging_view,
        &amp;option,
        &amp;document_requirement_views,
        &amp;total_declared_value,
    );
    let settlement_view: SettlementView = self.settlement_port.settle(settlement_input_items);

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

    // ---------- 步骤 7:派生标识 ----------
    // 运单号由「业务号 + 收件方 + 承运商」派生:三者任一变化即换号,
    // 且同一输入永远得到同一个号(演示可复现)。
    let waybill_seed: String = build_seed(&amp;[
        request.shipment_reference,
        request.receiver_name,
        request.receiver_city,
        option.carrier_code,
    ]);
    let waybill_number: String = derive_code("WB", &amp;waybill_seed, 12);
    // 指纹用于报表比对同一票货的不同阶段快照。
    let fingerprint: String = short_fingerprint(&amp;build_seed(&amp;[
        request.shipment_reference,
        &amp;option.carrier_code,
        &amp;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(&amp;option),
        packaging: PackagingView {
            material_summary: self.summarize_material_lines(&amp;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(
    &amp;self,
    candidates: &amp;[CarrierOptionView],
    input: &amp;DispatchInput,
) -&gt; 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(&amp;self, option: &amp;CarrierOptionView) -&gt; Vec&lt;RouteLegView&gt; {
    let mut views: Vec&lt;RouteLegView&gt; = Vec::with_capacity(option.route_leg_days.len());
    // 途经国家列表来自契约,用于给每个段标注起终点国家。
    let visited: &amp;[&amp;'static str] = &amp;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: &amp;'static str = visited
            .get(index)
            .copied()
            .or_else(|| visited.first().copied())
            .unwrap_or("--");
        // 终点国家取下一个(若已到末尾则取最后一个)。
        let destination_country_code: &amp;'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(
    &amp;self,
    packaging_view: &amp;super::subsystem_ports::PackagingPlanView,
) -&gt; 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 &gt; 0 {
            summary.push('、');
        }
        summary.push_str(&amp;format!("{} × {}", line.material_label, line.quantity));
    }
    summary
}

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

    // ---------- 调度环节 ----------
    if candidates.is_empty() {
        events.push(FacadeEvent::new(
            "调度",
            EventSeverity::Critical,
            "没有任何承运方案可供选择".to_string(),
            "联系调度部门确认承运商接入状态",
        ));
    } else if !option.is_feasible &amp;&amp; !candidates.iter().any(|candidate| candidate.is_feasible) {
        // 全部不可行:把每个候选的原因都列出来(一次报全)。
        let reasons: String = candidates
            .iter()
            .map(|candidate| format!("{}:{}", candidate.carrier_label, candidate.infeasibility_reason))
            .collect::&lt;Vec&lt;String&gt;&gt;()
            .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 &gt;= 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 &amp;&amp; !packaging_view.uses_insulating_material {
        events.push(FacadeEvent::new(
            "包装",
            EventSeverity::Critical,
            "本票要求温控,但包装方案未使用保温材料".to_string(),
            "改用保温包装材料或取消温控要求",
        ));
    }
    // 包装增重提示(信息级):让客户知道计费重量的构成。
    if packaging_view.weight_gain.milligrams() &gt; 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 &gt;= HIGH_VALUE_THRESHOLD_MINOR_UNITS &amp;&amp; !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(&amp;self) -&gt; usize {
    self.events
        .iter()
        .filter(|event| event.severity.blocks_dispatch())
        .count()
}

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

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

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

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

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

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

/// 返回单一业务结论文本(供报表首行展示)。
pub fn outcome_text(&amp;self) -&gt; &amp;'static str {
    if self.is_blocked() {
        "装配被拒绝:存在严重级问题"
    } else if self.warning_count() &gt; 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(&amp;self) -&gt; &amp;ShippingResult {
    match self {
        ShipmentOutcome::Planned(result) =&gt; result,
        ShipmentOutcome::Rejected { result, .. } =&gt; result,
    }
}

/// 拒绝原因(成功时为 None)。
pub fn rejection_reason(&amp;self) -&gt; Option&lt;&amp;str&gt; {
    match self {
        ShipmentOutcome::Planned(_) =&gt; None,
        ShipmentOutcome::Rejected { reason, .. } =&gt; 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&lt;dyn DispatchPort&gt; 之类的特征对象;
//! - 工程外只要写一个结构体,实现同名方法(乃至不同类型的同名方法集),
//!   就能替换掉整套子系统,门面代码零改动;
//! - 这比「再写一个门面实现」有力得多,因为它证明可替换的是子系统
//!   (门面模式的真正价值点),而不是门面本身。
//!
//! ## 为什么用特征而不是「结构体 + 泛型」
//!
//! 泛型(ShippingFacade&lt;D: DispatchPort, P: PackagingPort, ...&gt;)
//! 也能做到同样的解耦,且是零成本抽象的。这里选特征对象的原因:
//! 门面需要在本工程里被以「运行期组装」的方式演示
//! (不同幕次使用不同子系统组合),而泛型会把这种灵活性推到类型层面,
//! 让调用方必须写出长长的类型参数。对示范工程而言,可读性优先。
//!
//! 若这条路径将来成为性能瓶颈,把特征对象换成泛型是一个机械重构
//! (签名一一对应),不会触及业务逻辑------这正是先定契约的价值。
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,
/// 状态中文标签(如「已具备」)。
///
/// 这里用 &amp;'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(&amp;self) -&gt; PackagingMaterial {
    self.material
}

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

/// 该行成本(材料单价 × 用量)。
pub fn line_cost(&amp;self) -&gt; 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(&amp;self) -&gt; 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(&amp;self) -&gt; &amp;[PackagingPlanLine] {
    &amp;self.lines
}

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

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

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

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

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

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

/// 返回材料清单的一行式文本,如 `"木质礼盒 × 1、防震泡沫箱 × 2"`。
pub fn material_summary_text(&amp;self) -&gt; 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 &gt; 0 {
            summary.push('、');
        }
        summary.push_str(&amp;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 &gt; 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 &gt; 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(&amp;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(&amp;self) -&gt; u32 {
    match self {
        ChargeSource::Freight =&gt; 0,
        ChargeSource::Packaging =&gt; 1,
        ChargeSource::Documentation =&gt; 2,
        ChargeSource::ValueAddedService =&gt; 3,
    }
}

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

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

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

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

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

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

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

/// 条数。
pub const fn item_count(&amp;self) -&gt; 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(&amp;self) -&gt; &amp;[SourceSubtotal] {
    &amp;self.by_source
}

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

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

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

/// 返回某一来源的合计金额(不存在则返回零)。
///
/// 参数 `source`:来源分类。
/// 返回:该来源合计。
///
/// 门面用它回答「增值服务费占总费用多少」这类比例问题。
pub fn subtotal_of(&amp;self, source: ChargeSource) -&gt; CurrencyAmount {
    for subtotal in &amp;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(&amp;self, source: ChargeSource) -&gt; 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(&amp;self) -&gt; Vec&lt;&amp;'static str&gt; {
    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&lt;SourceSubtotal&gt; = Vec::new();
for item in &amp;items {
    let source: ChargeSource = item.source();
    // 查找已有分组(最后一个即可,因为已排序)。
    match by_source.last_mut() {
        Some(last) if last.source() == source =&gt; {
            // 同来源:累加金额与条数。
            // 注意这里需要修改 last,故先在局部算好再整体替换,
            // 避免同时借用 last 的两个字段(借用检查器会拒绝)。
            let updated_subtotal: CurrencyAmount = last.subtotal().add(&amp;item.amount());
            let updated_count: usize = last.item_count() + 1;
            *last = SourceSubtotal::new(source, updated_subtotal, updated_count);
        }
        _ =&gt; {
            // 新来源:开一个新分组。
            by_source.push(SourceSubtotal::new(source, item.amount(), 1));
        }
    }
}

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

// ---------- 找最大费用项 ----------
// 用「先取 0 再比较」而不是 `max_by_key`:需要的是下标,
// 且要保证在金额相同的情况下取**先出现的**(报表更稳定)。
let mut largest_item_index: Option&lt;usize&gt; = None;
for (index, item) in items.iter().enumerate() {
    match largest_item_index {
        None =&gt; largest_item_index = Some(index),
        Some(current_best) =&gt; {
            let current_amount: i64 = items[current_best].amount().minor_units();
            if item.amount().minor_units() &gt; 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&lt;Utc&gt; 表达会引入两处错配:
//!
//! 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(&amp;self) -&gt; i32 {
    self.year
}

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

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

/// 返回往后推 `offset_days` 天之后的日期。
///
/// 参数 `offset_days`:天数偏移(可为负,表示往前推)。
/// 返回:新日期(`self` 本身不变------`CalendarDate` 是值语义的不可变对象)。
///
/// 实现方式为**逐日推进**而不是「用日期算法换算儒略日」:
/// 逐日在调试时可读性高得多(循环里能直接看出闰年跳变),
/// 而本工程的偏移量都是个位到两位数天数,性能无差异。
/// 若将来出现上千天的偏移,应改走儒略日算法(届时也需保留本函数作为对照)。
pub fn add_days(&amp;self, offset_days: i32) -&gt; 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 &gt; 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 &lt; maximum_day {
            // 还没到月末,直接加一天。
            cursor_day += 1;
        } else {
            // 已到月末:进入下个月的第一天。
            cursor_day = 1;
            if cursor_month &lt; 12 {
                cursor_month += 1;
            } else {
                // 年末:进入下一年的 1 月。
                cursor_month = 1;
                cursor_year += 1;
            }
        }
        remaining -= 1;
    }

    while remaining &lt; 0 {
        // 反向推进:逐日退。
        if cursor_day &gt; 1 {
            cursor_day -= 1;
        } else {
            // 已到月初:退到上个月的最后一天。
            if cursor_month &gt; 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(&amp;self, other: &amp;CalendarDate) -&gt; i32 {
    // 若两日期相同,直接返回 0,省去循环。
    if self == other {
        return 0;
    }

    // 判断方向,决定用哪一种推进,避免在两个循环里各写一遍逻辑。
    let forward: bool = other.year &gt; self.year
        || (other.year == self.year &amp;&amp; (other.month &gt; self.month
            || (other.month == self.month &amp;&amp; other.day &gt; 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 &gt; 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 &lt; -(366 * 200) {
                break;
            }
        }
        distance
    }
}

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

/// 返回不含年份的中文月日文本,如「10 月 5 日」。
///
/// 用于「发货日 → 到达日」这种同年内的紧凑展示,省掉重复的年份。
pub fn month_day_text(&amp;self) -&gt; 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(&amp;self) -&gt; 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 &lt; 3 { y -= 1 }` 等价。
    let adjusted_year: i32 = if self.month() &lt; 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(&amp;self) -&gt; &amp;'static str {
    // 下标 0 对应周一,与 weekday_index 的约定一致。
    const WEEKDAY_LABELS: [&amp;str; 7] = ["一", "二", "三", "四", "五", "六", "日"];
    WEEKDAY_LABELS[self.weekday_index() as usize]
}

/// 判断该日期是否为周末(周六或周日)。
///
/// 承运与清关环节在周末的处理时效不同,门面会据此在时间线上追加提示。
pub fn is_weekend(&amp;self) -&gt; bool {
    // 索引 5 = 周六、6 = 周日。
    let index: u32 = self.weekday_index();
    index &gt;= 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 标准库的格式化填充({:&lt;20})按 char 个数补空格,而中日韩字符
//! 在等宽终端里占 2 个显示列。于是 format!("{:&lt;10}", "翡翠手镯")
//! 得到的是「6 列内容 + 4 空格」= 10 列,而 format!("{:&lt;10}", "Jade")
//! 得到的是「4 列内容 + 6 空格」= 10 列------两者都以「10 列」为目标,
//! 看起来没问题;但一旦列里混排中文与英文,同一个 {:&lt;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) &gt;= 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 &gt; 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
}
rust 复制代码
//!# 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_breakdown.rs
//! 费用拆分与占比分析。
//!
//! ## 输入只有门面视图
//!
//! 本文件 import 的只有 `facade::ShippingResult` 与 `domain::CurrencyAmount`。
//! 它不知道费用项原本来自哪个子系统------那正是我们想要的:
//! 分析维度不应该随子系统拆分方式变化。
 
use crate::domain::{CurrencyAmount, EventSeverity};
use crate::facade::ShippingResult;
 
/// 一条费用明细(带占比)。
#[derive(Debug, Clone)]
pub struct ChargeEntry {
    /// 费用项编码。
    pub entry_code: &'static str,
    /// 费用项名称。
    pub label: &'static str,
    /// 来源名称。
    pub source_label: &'static str,
    /// 金额。
    pub amount: CurrencyAmount,
    /// 占应付总额的比例(万分比)。
    pub share_basis_points: i64,
    /// 计价依据。
    pub basis_text: &'static str,
}
 
impl ChargeEntry {
    /// 返回占比的百分比文本(如 `"28.15%"`)。
    pub fn share_percent_text(&self) -> String {
        // 万分比 → 百分比:整数部分除 100,小数部分取余。
        let absolute: i64 = self.share_basis_points.abs();
        let whole: i64 = absolute / 100;
        let fraction: i64 = absolute % 100;
        let sign: &str = if self.share_basis_points < 0 { "-" } else { "" };
        format!("{}{}.{:02}%", sign, whole, fraction)
    }
 
    /// 返回一行的展示文本。
    ///
    /// 与 `ChargeItem::formatted` 同属「子系统自带的显示口径」。分析层
    /// 保留并开放它,是为了在第七幕与报表层排版并列对照------
    /// 排版职责统一归 `app` 层,不能因为「反正有个现成的」就混用。
    pub fn formatted(&self) -> String {
        format!(
            "{} | {} | 占 {}",
            self.label,
            self.amount.formatted(),
            self.share_percent_text()
        )
    }
}
 
/// 一份完整的费用拆分。
#[derive(Debug, Clone)]
pub struct ChargeBreakdown {
    /// 明细条目(按来源顺序)。
    pub entries: Vec<ChargeEntry>,
    /// 应付总额。
    pub grand_total: CurrencyAmount,
    /// 增值服务费合计。
    pub service_charge_total: CurrencyAmount,
    /// 运费金额。
    pub freight_charge: CurrencyAmount,
    /// 是否包含负值费用项(折扣)。
    pub has_negative_entry: bool,
}
 
impl ChargeBreakdown {
    /// 明细条数。
    pub fn entry_count(&self) -> usize {
        self.entries.len()
    }
 
    /// 服务费占总额的比例(万分比)。
    pub fn service_share_basis_points(&self) -> i64 {
        let total: i64 = self.grand_total.minor_units();
        if total == 0 {
            return 0;
        }
        let service: i128 = self.service_charge_total.minor_units() as i128 * 10_000i128;
        (service / total as i128) as i64
    }
 
    /// 最大费用项(按绝对金额)。
    pub fn largest_entry(&self) -> Option<&ChargeEntry> {
        // 用绝对金额比较:折扣(负值)金额很大时,它才是「最需要关注」的一项。
        self.entries.iter().max_by_key(|entry| entry.amount.minor_units().abs())
    }
 
    /// 服务费与运费的差额(正表示服务费更多)。
    pub fn service_freight_gap(&self) -> CurrencyAmount {
        self.service_charge_total.subtract(&self.freight_charge)
    }
 
    /// 服务费相对运费的倍数文本(如 `"2.55 倍"`)。
    ///
    /// 返回 `Option`:运费为 0 时无法谈倍数,此时返回 `None`
    /// 让调用方决定怎么展示(报表里显示「---」),而不是硬造一个数字或除零。
    pub fn service_to_freight_multiple_text(&self) -> Option<String> {
        let freight: i64 = self.freight_charge.minor_units();
        if freight == 0 {
            return None;
        }
        let service: i128 = self.service_charge_total.minor_units() as i128 * 100i128;
        // 乘 100 保留 2 位小数的精度,再整数除。
        let multiple_hundredths: i128 = service / freight as i128;
        let whole: i128 = multiple_hundredths / 100;
        let fraction: i128 = (multiple_hundredths % 100).abs();
        Some(format!("{}.{:02} 倍", whole, fraction))
    }
}
 
/// 由门面结果构造费用拆分。
///
/// 参数 `result`:门面装配结果。
/// 返回:费用拆分。
///
/// ## 占比的分母是「应付总额」
///
/// 若分母用「各费用项绝对值之和」,那么在存在折扣(负值)时,
/// 各项占比之和会大于 100%,对账同事会立刻质疑。
/// 用应付总额作分母时,比例之和在无负值时恰好为 100%
/// (可能有 ±1 个万分点的手算误差,因为每项独立取整)。
/// 本工程在报表里会额外展示「合计占比」,让误差可见而非被掩盖。
pub fn build_charge_breakdown(result: &ShippingResult) -> ChargeBreakdown {
    let total_minor_units: i64 = result.grand_total.minor_units();
    let mut entries: Vec<ChargeEntry> = Vec::with_capacity(result.charge_items.len());
    let mut has_negative_entry: bool = false;
    let mut service_total: CurrencyAmount = CurrencyAmount::zero(result.grand_total.currency());
    let mut freight_total: CurrencyAmount = CurrencyAmount::zero(result.grand_total.currency());
 
    for (code, label, source_label, amount, basis_text) in &result.charge_items {
        // 计算占比(分母为 0 时取 0,避免除零)。
        let share_basis_points: i64 = if total_minor_units == 0 {
            0
        } else {
            let numerator: i128 = amount.minor_units() as i128 * 10_000i128;
            (numerator / total_minor_units as i128) as i64
        };
 
        if amount.is_negative() {
            has_negative_entry = true;
        }
 
        // 按来源名称归类累加:来源名是视图里已有的稳定文本,
        // 分析层不需要认识来源枚举。
        if *source_label == "增值服务费" {
            service_total = service_total.add(amount);
        } else if *source_label == "基础运费" {
            freight_total = freight_total.add(amount);
        }
 
        entries.push(ChargeEntry {
            entry_code: code,
            label,
            source_label,
            amount: *amount,
            share_basis_points,
            basis_text,
        });
    }
 
    ChargeBreakdown {
        entries,
        grand_total: result.grand_total,
        service_charge_total: service_total,
        freight_charge: freight_total,
        has_negative_entry,
    }
}
 
/// 统计结果中某一严重等级的事件数量(供报表标题行使用)。
///
/// 参数 `result`:门面结果;`severity`:目标等级。
/// 返回:数量。
///
/// 放在本文件是因为它与「费用」无关但与「汇总口径」有关;
/// 若将来出现更多跨维度汇总,应提取到一个 `summary` 模块。
pub fn count_events_of_severity(result: &ShippingResult, severity: EventSeverity) -> usize {
    result
        .events
        .iter()
        .filter(|event| event.severity == severity)
        .count()
}
 
 
//!# 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      : constraint_audit.rs
//! 装配约束体检(跨域核查)。
//!
//! ## 与门面内部事件的区别
//!
//! 门面的事件来自各子系统**各自**的判断(承运商不可行、单据缺失......)。
//! 本层做的是**跨域一致性核查**------那些单个子系统看不见、
//! 只有把多方输出放在一起才能发现的问题。例如:
//!
//! - 「申报高值但用了不具备温控的承运商」------
//!   高值信息在结算侧、承运能力在调度侧,任一侧单独看都正常;
//! - 「承诺了精确时刻却没投保价」------时效承诺与保价分属两个环节;
//! - 「包装增重占比过高」------包装与运费分属两侧。
//!
//! 这些规则**新增时不需要改任何子系统或门面**,
//! 只需在本层加一条检查函数------这就是「分析维度可扩展」的实证。
 
use crate::domain::EventSeverity;
use crate::facade::ShippingResult;
 
/// 一条约束体检结论。
#[derive(Debug, Clone)]
pub struct ConstraintAuditEntry {
    /// 规则编码。
    pub rule_code: &'static str,
    /// 规则说明。
    pub rule_description: &'static str,
    /// 是否通过。
    pub passed: bool,
    /// 结论说明(通过时也可给出正向说明,让报表有信息量)。
    pub conclusion: String,
    /// 严重等级(仅在不通过时有意义)。
    pub severity: EventSeverity,
    /// 建议。
    pub suggestion: &'static str,
}
 
/// 一份约束体检报告。
#[derive(Debug, Clone)]
pub struct ConstraintAuditReport {
    /// 各条结论。
    pub entries: Vec<ConstraintAuditEntry>,
}
 
impl ConstraintAuditReport {
    /// 通过条数。
    pub fn passed_count(&self) -> usize {
        self.entries.iter().filter(|entry| entry.passed).count()
    }
 
    /// 未通过条数。
    pub fn failed_count(&self) -> usize {
        self.entries.iter().filter(|entry| !entry.passed).count()
    }
 
    /// 是否存在阻断级未通过项。
    pub fn has_blocker(&self) -> bool {
        self.entries
            .iter()
            .any(|entry| !entry.passed && entry.severity.blocks_dispatch())
    }
 
    /// 结论总条数。
    pub fn total_count(&self) -> usize {
        self.entries.len()
    }
}
 
/// 对门面结果做跨域约束体检。
///
/// 参数 `result`:门面装配结果。
/// 返回:体检报告。
///
/// ## 「一次报全」的纪律同样适用于本层
///
/// 本函数**依次走完所有规则**,每条规则独立判断、独立追加结论,
/// 任何一条不通过都不会中断后续检查。
/// 这与门面内部的事件汇聚保持同一口径:
/// 用户希望一次看到全部问题,而不是修完一个再发现下一个。
pub fn audit_constraints(result: &ShippingResult) -> ConstraintAuditReport {
    let mut entries: Vec<ConstraintAuditEntry> = Vec::new();
 
    // ---------- 规则 1:路由非空 ----------
    let has_route: bool = result.route_leg_count() > 0;
    entries.push(ConstraintAuditEntry {
        rule_code: "ROUTE_PRESENT",
        rule_description: "运单必须有至少一段路由",
        passed: has_route,
        conclusion: if has_route {
            format!("路由共 {} 段,{}\n", result.route_leg_count(), result.route_summary)
                .trim_end()
                .to_string()
        } else {
            "路由为空,无法确定运输路径".to_string()
        },
        severity: EventSeverity::Critical,
        suggestion: "补充路由信息或改换可给出路由的承运商",
    });
 
    // ---------- 规则 2:跨境路由的粒度 ----------
    // 跨境至少应有 2 段(离境 + 入境),只有 1 段说明出境与入境被合并记录。
    let is_cross_border: bool = result
        .route_legs
        .iter()
        .any(|leg| leg.is_cross_border);
    let cross_border_granularity_ok: bool = !is_cross_border || result.route_leg_count() >= 2;
    entries.push(ConstraintAuditEntry {
        rule_code: "CROSS_BORDER_GRANULARITY",
        rule_description: "跨境运单的路由应区分离境段与入境段",
        passed: cross_border_granularity_ok,
        conclusion: if !is_cross_border {
            "本票为境内运输,本规则不适用(视为通过)".to_string()
        } else if cross_border_granularity_ok {
            format!("跨境运输,路由 {} 段,粒度充足", result.route_leg_count())
        } else {
            "跨境运输但路由仅 1 段,出境与入境环节被合并记录".to_string()
        },
        // 提醒级:粒度不足会让清关跟踪困难,但不至于阻断出单。
        severity: EventSeverity::Warning,
        suggestion: "拆分路由段以便分别跟踪离境与入境清关",
    });
 
    // ---------- 规则 3:高值货应有保价 ----------
    // 判据从结果里读:申报价值与费用项编码都是视图内的数据。
    let has_insurance: bool = result
        .charge_items
        .iter()
        .any(|(code, _, _, _, _)| *code == "INSURANCE");
    let declared_value_minor_units: i64 = result.total_declared_value.minor_units();
    let is_high_value: bool = declared_value_minor_units >= 5_000_000;
    let high_value_insured_ok: bool = !is_high_value || has_insurance;
    entries.push(ConstraintAuditEntry {
        rule_code: "HIGH_VALUE_INSURED",
        rule_description: "高值货(≥ ¥50,000.00)应投保价险",
        passed: high_value_insured_ok,
        conclusion: if !is_high_value {
            format!(
                "申报价值 {} 未达高价值线,本规则不适用(视为通过)",
                result.total_declared_value.formatted()
            )
        } else if high_value_insured_ok {
            format!(
                "申报价值 {} 已达高价值线且已投保价",
                result.total_declared_value.formatted()
            )
        } else {
            format!(
                "申报价值 {} 已达高价值线但未投保价",
                result.total_declared_value.formatted()
            )
        },
        severity: EventSeverity::Warning,
        suggestion: "为高值货品追加保价服务",
    });
 
    // ---------- 规则 4:单据齐备性 ----------
    let missing_mandatory_documents: usize = result
        .documents
        .iter()
        .filter(|document| document.blocks_dispatch)
        .count();
    let documents_ok: bool = missing_mandatory_documents == 0;
    entries.push(ConstraintAuditEntry {
        rule_code: "MANDATORY_DOCUMENTS_PRESENT",
        rule_description: "全部必备单据均已齐备",
        passed: documents_ok,
        conclusion: if documents_ok {
            format!(
                "共 {} 项单据,必备项齐备",
                result.document_count()
            )
        } else {
            format!("有 {} 项必备单据缺失", missing_mandatory_documents)
        },
        severity: EventSeverity::Critical,
        suggestion: "补齐必备单据或申请豁免",
    });
 
    // ---------- 规则 5:包装增重占比 ----------
    // 增重超过净重的 30% 时提示:可能是包装过度,运费被无谓抬高。
    let gain_milligrams: i64 = result.packaging.weight_gain.milligrams();
    let net_milligrams: i64 = result.net_cargo_weight.milligrams();
    let gain_ratio_basis_points: i64 = if net_milligrams == 0 {
        0
    } else {
        let numerator: i128 = gain_milligrams as i128 * 10_000i128;
        (numerator / net_milligrams as i128) as i64
    };
    let packaging_ratio_ok: bool = gain_ratio_basis_points <= 3_000;
    entries.push(ConstraintAuditEntry {
        rule_code: "PACKAGING_WEIGHT_RATIO",
        rule_description: "包装增重不宜超过净重的 30%",
        passed: packaging_ratio_ok,
        conclusion: if net_milligrams == 0 {
            "净重为零,本规则不适用(视为通过)".to_string()
        } else {
            format!(
                "净重 {},包装增重 {}(占 {}.{:02}%)",
                result.net_cargo_weight.formatted_grams(),
                result.packaging.weight_gain.formatted_grams(),
                gain_ratio_basis_points / 100,
                gain_ratio_basis_points % 100
            )
        },
        severity: EventSeverity::Warning,
        suggestion: "评估是否可减少包装层数以降低计费重量",
    });
 
    // ---------- 规则 6:时间线未被周末顺延 ----------
    // 顺延本身不是错误,但若顺延导致跨越 2 天以上,可能需要重新评估时效承诺。
    let timeline_ok: bool = !result.timeline.delayed_by_weekend;
    entries.push(ConstraintAuditEntry {
        rule_code: "TIMELINE_NO_WEEKEND_DELAY",
        rule_description: "时间线未因周末顺延",
        passed: timeline_ok,
        conclusion: if timeline_ok {
            "发货与派送均落在工作日,无顺延".to_string()
        } else {
            "时间线因周末顺延,实际时效长于路由推算值".to_string()
        },
        // 提示级:顺延是正常现象,只需知会。
        severity: EventSeverity::Info,
        suggestion: "向收件方同步顺延后的派送日",
    });
 
    ConstraintAuditReport { entries }
}
 
 
//!# 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      : timeline_profile.rs
//! 时间线画像(时效结构分析)。
//!
//! ## 为什么单独成一层而不并入门面
//!
//! 门面产出的是「这条路线的里程碑列表」;
//! 本层回答的是「这条路线的时效结构如何」------
//! 例如「路由天数与等待天数各占多少」「是否有环节耗时异常」。
//! 这是**相对口径**,与装配动作无关,因此归分析层。
 
use crate::facade::ShippingResult;
use crate::support::calendar_date::CalendarDate;
 
/// 一个时间线环节的画像。
#[derive(Debug, Clone)]
pub struct MilestoneProfile {
    /// 环节编码。
    pub code: &'static str,
    /// 环节名称。
    pub label: &'static str,
    /// 该环节日期。
    pub date: CalendarDate,
    /// 距发货日的天数(发货日为 0)。
    pub day_offset: i32,
    /// 距上一环节的天数。
    pub gap_days: i32,
    /// 说明。
    pub note: &'static str,
}
 
/// 时间线画像。
#[derive(Debug, Clone)]
pub struct TimelineProfile {
    /// 各环节画像。
    pub milestones: Vec<MilestoneProfile>,
    /// 发货日。
    pub dispatch_date: CalendarDate,
    /// 预计送达日。
    pub estimated_delivery_date: CalendarDate,
    /// 全程总天数(发货日 → 送达日)。
    pub total_days: i32,
    /// 路由天数(各段之和)。
    pub route_days: i32,
    /// 非路由天数(总天数 − 路由天数,即等待与周末顺延)。
    pub waiting_days: i32,
    /// 是否因周末顺延。
    pub delayed_by_weekend: bool,
}
 
impl TimelineProfile {
    /// 环节数量。
    pub fn milestone_count(&self) -> usize {
        self.milestones.len()
    }
 
    /// 等待天数占总天数的比例(万分比)。
    pub fn waiting_share_basis_points(&self) -> i64 {
        if self.total_days == 0 {
            return 0;
        }
        let numerator: i128 = self.waiting_days as i128 * 10_000i128;
        (numerator / self.total_days as i128) as i64
    }
 
    /// 最长的一个环节间隔(返回该环节画像)。
    pub fn longest_gap_milestone(&self) -> Option<&MilestoneProfile> {
        self.milestones.iter().max_by_key(|profile| profile.gap_days)
    }
}
 
/// 由门面结果构造时间线画像。
///
/// 参数 `result`:门面装配结果。
/// 返回:时间线画像。
///
/// ## 为什么要区分「路由天数」与「等待天数」
///
/// 路由天数是承运商承诺的运输时长;等待天数是报关准备与周末顺延带来的。
/// 客服在回答「为什么这么久」时,必须能区分这两者------
/// 若是等待天数占比高,那是流程问题而非运力问题,处置方式完全不同。
/// 把两者算清楚是分析层对业务的直接贡献。
pub fn build_timeline_profile(result: &ShippingResult) -> TimelineProfile {
    let milestones_source = &result.timeline.milestones;
 
    // 发货日:第一个里程碑即为发货(门面保证顺序)。
    let dispatch_date: CalendarDate = milestones_source
        .first()
        .map(|(_, _, date, _)| *date)
        .unwrap_or(result.timeline.estimated_delivery_date);
 
    let mut milestones: Vec<MilestoneProfile> = Vec::with_capacity(milestones_source.len());
    let mut previous_date: Option<CalendarDate> = None;
 
    for (code, label, date, note) in milestones_source {
        // 距发货日的偏移。
        let day_offset: i32 = dispatch_date.days_until(date);
        // 距上一环节的间隔(首个环节为 0)。
        let gap_days: i32 = match previous_date {
            None => 0,
            Some(previous) => previous.days_until(date),
        };
        milestones.push(MilestoneProfile {
            code,
            label,
            date: *date,
            day_offset,
            gap_days,
            note,
        });
        previous_date = Some(*date);
    }
 
    let estimated_delivery_date: CalendarDate = result.timeline.estimated_delivery_date;
    let total_days: i32 = dispatch_date.days_until(&estimated_delivery_date);
 
    // 路由天数:各段之和。
    let route_days: i32 = result
        .route_legs
        .iter()
        .map(|leg| leg.transit_days as i32)
        .sum();
 
    // 等待天数可能为负(若路由天数之和大于实际总天数,说明中间有并行段)。
    // 这里保留原值而不钳到 0:负值本身是有信息量的信号,
    // 报表展示时可自行处理。
    let waiting_days: i32 = total_days - route_days;
 
    TimelineProfile {
        milestones,
        dispatch_date,
        estimated_delivery_date,
        total_days,
        route_days,
        waiting_days,
        delayed_by_weekend: result.timeline.delayed_by_weekend,
    }
}
 
 
//!# 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_report.rs
//! 运单报表排版。
//!
//! ## 排版纪律(重申)
//!
//! 本文件里**不出现任何算术运算**(除了取长度、取序号)。
//! 所有金额、比例、天数都是取现成值再格式化。
//! 若发现某处需要算,说明该量应该补进分析层。
 
use crate::analysis::{
    ChargeBreakdown, ConstraintAuditReport, TimelineProfile,
};
use crate::domain::CurrencyAmount;
use crate::facade::ShippingResult;
use crate::support::text_layout::{
    display_width, horizontal_rule, pad_center, pad_left, pad_right, truncate_to_width,
};
 
/// 报表总宽度(显示列)。
///
/// 取 74 是因为在常见终端(80 列)下留出边距后仍能完整显示,
/// 且是偶数------中文宽度为 2,偶数宽度能让居中标题的两侧空格数相等。
pub const REPORT_WIDTH: usize = 74;
 
/// 输出一条水平分隔线。
pub fn render_rule() {
    println!("{}", horizontal_rule('─', REPORT_WIDTH));
}
 
/// 输出报表标题(居中)。
///
/// 参数 `title`:标题文本。
pub fn render_header(title: &str) {
    render_rule();
    // 标题用 pad_center 居中:注意不是用 format! 的填充,
    // 那会按字符数补空格,中文标题会偏左。
    println!("{}", pad_center(title, REPORT_WIDTH));
    render_rule();
}
 
/// 输出一行「标签 + 值」(标签定宽右对齐至 12 列)。
///
/// 参数 `label` / `value`。
pub fn render_summary_line(label: &str, value: &str) {
    // 标签宽度 12 显示列:能容纳「预计到达」这类 4 字中文(8 列)还有余量。
    println!("  {}  {}", pad_right(label, 12), value);
}
 
/// 输出运单主报文的头部信息。
///
/// 参数 `result`:门面装配结果。
pub fn render_shipment_header(result: &ShippingResult) {
    render_rule();
    // 单号与指纹放同一行,便于复印后比对。
    println!(
        "  单号 {} | 指纹 {} | 承运 {}({})",
        result.waybill_number, result.fingerprint, result.carrier_label, result.carrier_code
    );
    render_rule();
}
 
/// 输出货物明细段落。
///
/// 参数 `result`:门面装配结果。
pub fn render_cargo_section(result: &ShippingResult) {
    println!("  货物明细");
    // 表头:各列宽度用显示列数(不是字符数)。
    println!(
        "    {}  {}  {}  {}  {}",
        pad_right("名称", 14),
        pad_right("品类", 12),
        pad_right("件数", 5),
        pad_right("单件重量", 14),
        pad_right("单件申报价值", 16)
    );
    for (name, category_label, quantity, unit_weight, unit_value) in &result.cargo_lines {
        println!(
            "    {}  {}  {}  {}  {}",
            // 名称截断到 14 显示列:超长名称不应撑破表格。
            pad_right(&truncate_to_width(name, 14), 14),
            pad_right(&truncate_to_width(category_label, 12), 12),
            pad_right(&quantity.to_string(), 5),
            pad_right(&unit_weight.formatted_grams(), 14),
            pad_right(&unit_value.formatted(), 16)
        );
    }
    // 合计行:数值来自门面(已是算好的值)。
    let total_pieces: i64 = result.cargo_lines.iter().map(|line| line.2).sum();
    println!(
        "  合计  {} 件|净重 {}|申报 {}",
        total_pieces,
        result.net_cargo_weight.formatted_kilograms(),
        result.total_declared_value.formatted()
    );
}
 
/// 输出包装段落。
///
/// 参数 `result`:门面装配结果。
pub fn render_packaging_section(result: &ShippingResult) {
    let packaging = &result.packaging;
    println!("  包装");
    println!("    材料清单      {}", packaging.material_summary);
    println!(
        "    材料成本      {}({} 种材料)",
        packaging.total_material_cost.formatted(),
        packaging.material_kind_count
    );
    println!(
        "    包装增重      {}(包装后总重 {})",
        packaging.weight_gain.formatted_grams(),
        packaging.total_weight_with_packaging.formatted_kilograms()
    );
    println!(
        "    保温材料      {}",
        if packaging.uses_insulating_material {
            "已使用"
        } else {
            "未使用"
        }
    );
}
 
/// 输出路由段落。
///
/// 参数 `result`:门面装配结果。
pub fn render_route_section(result: &ShippingResult) {
    println!("  路由({} 段)", result.route_leg_count());
    for leg in &result.route_legs {
        println!("    {}", leg.formatted());
    }
    // 路由摘要来自门面(已拼好),本层不再拼接。
    println!("    摘要          {}", result.route_summary);
}
 
/// 输出时间线段落。
///
/// 参数 `result`:门面结果;`profile`:分析层的时间线画像。
pub fn render_timeline_section(result: &ShippingResult, profile: &TimelineProfile) {
    let _ = result; // 参数保留以便将来在标题里引用运单号;当前只用画像。
    println!("  时间线({} 个环节)", profile.milestone_count());
    println!(
        "    {}  {}  {}  {}",
        pad_right("环节", 16),
        pad_right("日期", 12),
        pad_right("累计天", 8),
        "说明"
    );
    for milestone in &profile.milestones {
        println!(
            "    {}  {}  {}  {}",
            pad_right(milestone.label, 16),
            pad_right(&milestone.date.formatted(), 12),
            pad_right(&format!("+{}", milestone.day_offset), 8),
            truncate_to_width(milestone.note, 28)
        );
    }
    // 时效结构:路由天数与等待天数的拆分(来自分析层)。
    println!(
        "    时效结构:总 {} 天 = 路由 {} 天 + 等待 {} 天(等待占 {}.{:02}%)",
        profile.total_days,
        profile.route_days,
        profile.waiting_days,
        profile.waiting_share_basis_points() / 100,
        profile.waiting_share_basis_points() % 100
    );
    if profile.delayed_by_weekend {
        println!("    注:时间线因周末顺延,已按承运规则调整到工作日");
    }
}
 
/// 输出单据段落。
///
/// 参数 `result`:门面装配结果。
pub fn render_document_section(result: &ShippingResult) {
    println!("  单据清单({} 项)", result.document_count());
    println!(
        "    {}  {}  {}  {}",
        pad_right("单据", 16),
        pad_right("状态", 8),
        pad_right("强制", 6),
        "触发原因"
    );
    for document in &result.documents {
        println!(
            "    {}  {}  {}  {}",
            pad_right(document.kind.label(), 16),
            pad_right(document.status_label, 8),
            pad_right(if document.is_mandatory { "是" } else { "否" }, 6),
            truncate_to_width(document.trigger_reason, 30)
        );
    }
}
 
/// 输出费用段落。
///
/// 参数 `result`:门面结果;`breakdown`:分析层的费用拆分。
pub fn render_charge_section(result: &ShippingResult, breakdown: &ChargeBreakdown) {
    let _ = result;
    println!("  费用({} 项)", breakdown.entry_count());
    println!(
        "    {}  {}  {}  {}",
        pad_right("费用项", 16),
        pad_right("金额", 14),
        pad_right("占比", 8),
        "计价依据"
    );
    for entry in &breakdown.entries {
        println!(
            "    {}  {}  {}  {}",
            pad_right(entry.label, 16),
            pad_left(&entry.amount.formatted(), 14),
            pad_left(&entry.share_percent_text(), 8),
            truncate_to_width(entry.basis_text, 24)
        );
    }
    render_rule();
    println!(
        "    {}  {}",
        pad_right("应付总额", 16),
        pad_left(&breakdown.grand_total.formatted(), 14)
    );
    // 服务费与运费的关系(来自分析层)。
    println!(
        "    其中:运费 {};增值服务费 {}(占总额 {}.{:02}%)",
        breakdown.freight_charge.formatted(),
        breakdown.service_charge_total.formatted(),
        breakdown.service_share_basis_points() / 100,
        breakdown.service_share_basis_points() % 100
    );
    // 倍数文本来自分析层;为 None 时展示「---」(而非编造数字)。
    match breakdown.service_to_freight_multiple_text() {
        Some(multiple) => println!(
            "    服务费为运费的 {}(差额 {})",
            multiple,
            breakdown.service_freight_gap().formatted()
        ),
        None => println!("    服务费为运费的 ---(运费为 0,不适用)"),
    }
    if let Some(largest) = breakdown.largest_entry() {
        println!(
            "    最大费用项:{}({})",
            largest.label,
            largest.amount.formatted()
        );
    }
    if breakdown.has_negative_entry {
        println!("    注:本票含负值费用项(折扣),占比之和可能超过 100%");
    }
}
 
/// 输出约束体检段落。
///
/// 参数 `report`:分析层的体检报告。
pub fn render_constraint_section(report: &ConstraintAuditReport) {
    println!(
        "  跨域约束体检({} 条:{} 通过 / {} 未通过)",
        report.total_count(),
        report.passed_count(),
        report.failed_count()
    );
    println!(
        "    {}  {}  {}  {}",
        pad_right("规则", 26),
        pad_right("结果", 6),
        pad_right("级别", 6),
        "结论"
    );
    for entry in &report.entries {
        println!(
            "    {}  {}  {}  {}",
            pad_right(&truncate_to_width(entry.rule_description, 26), 26),
            pad_right(if entry.passed { "通过" } else { "不通过" }, 6),
            // 通过时级别无意义,显示「---」避免误导。
            pad_right(
                if entry.passed {
                    "---"
                } else {
                    entry.severity.label()
                },
                6
            ),
            truncate_to_width(&entry.conclusion, 26)
        );
        // 不通过的项额外输出建议,缩进以示从属关系。
        if !entry.passed {
            println!("      ⟶ 建议:{}", entry.suggestion);
        }
    }
}
 
/// 输出事件段落(门面汇聚的问题清单)。
///
/// 参数 `result`:门面装配结果。
pub fn render_event_section(result: &ShippingResult) {
    println!(
        "  汇聚事件({} 条:{} 严重 / {} 警告 / {} 提示)",
        result.event_count(),
        result.blocker_count(),
        result.warning_count(),
        result.info_count()
    );
    if result.events.is_empty() {
        println!("    未发现需要处理的事项。");
        return;
    }
    println!(
        "    {}  {}  {}  {}",
        pad_right("环节", 8),
        pad_right("级别", 6),
        pad_right("问题", 34),
        "建议"
    );
    for event in &result.events {
        println!(
            "    {}  {}  {}  {}",
            pad_right(event.stage_label, 8),
            pad_right(event.severity.label(), 6),
            pad_right(&truncate_to_width(&event.description, 34), 34),
            truncate_to_width(event.suggestion, 22)
        );
    }
}
 
/// 输出结论行(可出单与否)。
///
/// 参数 `result`:门面装配结果。
pub fn render_conclusion(result: &ShippingResult) {
    println!("  结论:{}", result.outcome_text());
    println!(
        "  是否阻断出单:{}",
        if result.is_blocked() { "是" } else { "否" }
    );
}
 
/// 输出一个金额(便于在演示里展示中间值)。
///
/// 参数 `label` / `amount`。
pub fn render_amount_line(label: &str, amount: &CurrencyAmount) {
    println!("  {}:{}", label, amount.formatted());
}
 
/// 计算一段文本的显示宽度(供演示代码展示对齐依据)。
///
/// 参数 `text`。
/// 返回:显示列数。
pub fn width_of(text: &str) -> usize {
    display_width(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      : delivery_rules.rs
//! 承运侧的日历业务规则。
//!
//! ## 为什么把规则集中在这个文件
//!
//! 「周末不派送」「跨境需提前报关准备」这类规则会随承运商与地区变化。
//! 集中一处的好处是:换一家承运商时,只需替换本文件的规则,
//! 时间线推算逻辑([`super::timeline_builder`])一字不改。
//!
//! 这是「算法与规则分离」的常规手法,在门面工程里尤其重要------
//! 因为门面会把多家承运商的时间线汇总到一张报表上,
//! 规则若不集中,报表口径就会随承运商而漂移。
 
use crate::support::calendar_date::CalendarDate;
 
/// 跨境运输前的报关准备天数。
///
/// 取 1 天:本工程演示的是「提前一天交单」的常见做法。
/// 这个值会作为常量参与时效推算,因此它变化时所有相关日期都会同步变化,
/// 不存在「改了常量但某处还写着旧天数」的隐患。
pub const CUSTOMS_PREPARATION_DAYS: i32 = 1;
 
/// 判断某日是否为工作日(非周六、非周日)。
///
/// 参数 `date`:待判断日期。
/// 返回:工作日返回 `true`。
///
/// 本工程不引入法定节假日表------那需要一份年度数据,
/// 属于「配置」而非「逻辑」。若需要,只需扩大本函数内部的判断条件,
/// 调用方(时间线推算)不受影响。
pub fn is_working_day(date: &CalendarDate) -> bool {
    !date.is_weekend()
}
 
/// 把发货日调整到最近的工作日。
///
/// 参数 `date`:原计划发货日。
/// 返回:若原日期是工作日则原样返回;否则顺延到下一个工作日。
///
/// 用「顺延」而不是「提前」:提前意味着仓促备货,业务上不可接受;
/// 顺延只是晚一天发货,风险可控。这个方向选择要写进注释,
/// 否则将来维护者很容易改成「取最近的工作日」而改变业务含义。
pub fn adjust_dispatch_date_for_weekend(date: &CalendarDate) -> CalendarDate {
    let mut cursor: CalendarDate = *date;
    // 最多顺延 2 天即可跨过周末(周六→周一,周日→周一),
    // 循环上界设 7 天足以覆盖任何意外的节假日扩展。
    let mut guard: u32 = 0;
    while !is_working_day(&cursor) && guard < 7 {
        cursor = cursor.add_days(1);
        guard += 1;
    }
    cursor
}
 
/// 从起始日推算出「经过指定工作日数」之后的日期。
///
/// 参数 `start`:起始日;`working_day_count`:需要经过的工作日数。
/// 返回:终点日期。
///
/// ## 语义约定(容易搞错,务必看清)
///
/// 本函数计算的是「从 `start` 之后起算,跨过 `working_day_count` 个工作日」。
/// `working_day_count = 0` 时返回 `start` 本身;
/// `working_day_count = 1` 且 `start` 为周五时,返回下周一。
///
/// 之所以不用「累计自然日再减去周末数」的公式:
/// 当起点落在周末、或区间跨越多个周末时,那种近似会算错。
/// 逐日推进虽然直白,但对「个位数工作日」的场景完全够用且不可能错。
pub fn working_days_between(start: &CalendarDate, working_day_count: i32) -> CalendarDate {
    let mut cursor: CalendarDate = *start;
    let mut remaining: i32 = working_day_count;
 
    // 正向:跨过若干工作日。
    while remaining > 0 {
        cursor = cursor.add_days(1);
        // 只有落在工作日才消耗一个计数。
        if is_working_day(&cursor) {
            remaining -= 1;
        }
    }
 
    // 反向:往前退若干工作日(用于「最迟装运日」这类倒推)。
    while remaining < 0 {
        cursor = cursor.add_days(-1);
        if is_working_day(&cursor) {
            remaining += 1;
        }
    }
 
    cursor
}
 
 
//!# 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      : timeline_builder.rs
//! 承运时间线推算(承运子系统的输出结构 + 推算逻辑)。
//!
//! ## 时间线的构成
//!
//! 一条时间线由若干「里程碑」组成:发货 → 报关准备 → 各段路由 → 派送。
//! 每个里程碑都有一个**绝对日历日**(而不是「第 N 天」),
//! 因为运营需要的是「10 月 6 日交货」而不是「第 2 天交货」。
//!
//! ## 为什么在这里才把相对天数变成绝对日期
//!
//! dispatch 给出的路由只带天数(相对量),本子系统把它落到日历上。
//! 这样做的直接好处是:**若把发货日往前挪一天,
//! 整条时间线自动跟着挪,无需让 dispatch 重算路由**。
//! 相对量与绝对量分离,是这类计算能保持稳定的关键。
 
use crate::support::calendar_date::CalendarDate;
 
use super::delivery_rules::{is_working_day, CUSTOMS_PREPARATION_DAYS};
 
/// 时间线上的一个里程碑。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimelineMilestone {
    /// 里程碑编码(如 `"DISPATCHED"`)。
    code: &'static str,
    /// 里程碑中文名(如 `"已发货"`)。
    label: &'static str,
    /// 该里程碑发生(或预计发生)的日历日。
    date: CalendarDate,
    /// 补充说明(如「含 1 天报关准备」)。
    note: &'static str,
}
 
impl TimelineMilestone {
    /// 构造一个里程碑。
    pub const fn new(
        code: &'static str,
        label: &'static str,
        date: CalendarDate,
        note: &'static str,
    ) -> Self {
        TimelineMilestone {
            code,
            label,
            date,
            note,
        }
    }
 
    /// 里程碑编码。
    pub const fn code(&self) -> &'static str {
        self.code
    }
 
    /// 里程碑中文名。
    pub const fn label(&self) -> &'static str {
        self.label
    }
 
    /// 日历日。
    pub const fn date(&self) -> CalendarDate {
        self.date
    }
 
    /// 补充说明。
    pub const fn note(&self) -> &'static str {
        self.note
    }
 
    /// 该里程碑是否落在非工作日。
    pub fn falls_on_non_working_day(&self) -> bool {
        !is_working_day(&self.date)
    }
}
 
/// 一整条承运时间线。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CarrierTimeline {
    /// 里程碑列表(按时间先后)。
    milestones: Vec<TimelineMilestone>,
    /// 预计送达日。
    estimated_delivery_date: CalendarDate,
    /// 是否因周末顺延过(用于报表提示)。
    delayed_by_weekend: bool,
}
 
impl CarrierTimeline {
    /// 构造一条时间线。
    pub const fn new(
        milestones: Vec<TimelineMilestone>,
        estimated_delivery_date: CalendarDate,
        delayed_by_weekend: bool,
    ) -> Self {
        CarrierTimeline {
            milestones,
            estimated_delivery_date,
            delayed_by_weekend,
        }
    }
 
    /// 里程碑列表。
    pub fn milestones(&self) -> &[TimelineMilestone] {
        &self.milestones
    }
 
    /// 预计送达日。
    pub const fn estimated_delivery_date(&self) -> CalendarDate {
        self.estimated_delivery_date
    }
 
    /// 是否因周末顺延。
    pub const fn delayed_by_weekend(&self) -> bool {
        self.delayed_by_weekend
    }
 
    /// 里程碑数量。
    pub fn milestone_count(&self) -> usize {
        self.milestones.len()
    }
 
    /// 返回全部里程碑编码。
    pub fn milestone_codes(&self) -> Vec<&'static str> {
        self.milestones
            .iter()
            .map(|milestone| milestone.code())
            .collect()
    }
}
 
/// 时间线推算的输入。
#[derive(Debug, Clone)]
pub struct TimelineRequest {
    /// 计划发货日。
    pub dispatch_date: CalendarDate,
    /// 各路由段的天数(按序)。
    pub route_leg_days: Vec<u32>,
    /// 各路由段是否为跨境段(与 `route_leg_days` 等长)。
    pub route_leg_is_cross_border: Vec<bool>,
    /// 目的城市名(仅用于里程碑文案)。
    pub destination_city: &'static str,
}
 
/// 依据发货日与路由推算完整时间线。
///
/// 参数 `request`:推算输入。
/// 返回:承运时间线。
///
/// ## 推算口径(可手算复核)
///
/// 1. **发货日**:若落在周末则顺延至下一个工作日
///    (周末仓库不装车);是否顺延记录在 `delayed_by_weekend`。
/// 2. 每段路由的顺序:跨境段在该段出发前额外占用 1 天报关准备
///    (用 [`CUSTOMS_PREPARATION_DAYS`]),该准备日也计入累计。
/// 3. **送达日**:累计结果若落在周末,顺延到下一个工作日
///    (不派送),并同样标记 `delayed_by_weekend`。
///
/// 一个必要的防御:`route_leg_days` 与 `route_leg_is_cross_border`
/// 必须等长。若不等长,按较短者处理并**在时间线里少记里程碑**,
/// 而不是 panic------门面在汇总报表时不应因数据不一致而中断整个流程。
pub fn build_timeline(request: &TimelineRequest) -> CarrierTimeline {
    let mut milestones: Vec<TimelineMilestone> = Vec::new();
    let mut delayed_by_weekend: bool = false;
 
    // ---------- 发货日 ----------
    let raw_dispatch_date: CalendarDate = request.dispatch_date;
    let dispatch_date: CalendarDate =
        super::delivery_rules::adjust_dispatch_date_for_weekend(&raw_dispatch_date);
    if dispatch_date != raw_dispatch_date {
        delayed_by_weekend = true;
    }
    milestones.push(TimelineMilestone::new(
        "DISPATCHED",
        "已发货",
        dispatch_date,
        // 若顺延了,在说明里点出来,否则运营会以为日期算错。
        if dispatch_date != raw_dispatch_date {
            "原计划日落在周末,已顺延至下一工作日"
        } else {
            "按计划发货"
        },
    ));
 
    // ---------- 逐段推进 ----------
    let mut cursor: CalendarDate = dispatch_date;
    // 取两个列表的较短长度,避免越界(见函数文档的防御说明)。
    let leg_count: usize = request
        .route_leg_days
        .len()
        .min(request.route_leg_is_cross_border.len());
 
    for leg_index in 0..leg_count {
        let leg_days: u32 = request.route_leg_days[leg_index];
        let is_cross_border: bool = request.route_leg_is_cross_border[leg_index];
 
        // 跨境段:先加报关准备天数。
        if is_cross_border {
            cursor = cursor.add_days(CUSTOMS_PREPARATION_DAYS);
            milestones.push(TimelineMilestone::new(
                "CUSTOMS_PREPARED",
                "报关准备完成",
                cursor,
                "跨境段出发前 1 天完成报关准备",
            ));
        }
 
        // 推进该段天数。
        cursor = cursor.add_days(leg_days as i32);
 
        // 路段里程碑的编码需要是 `&'static str`,
        // 但段序号是运行时值,无法拼进 `'static` 字符串。
        // 因此这里用「第 N 段到达」这一固定文案,把序号留给 note 之外的展示层。
        // 这是一个刻意的取舍:保持里程碑编码稳定可枚举,
        // 而具体的段序号由门面在报表里按顺序编号展示。
        milestones.push(TimelineMilestone::new(
            "LEG_ARRIVED",
            "路段到达",
            cursor,
            // note 也需 `'static`,故用统一文案;
            // 需要区分第几段时,看里程碑在列表中的位置即可(本函数保证顺序)。
            if is_cross_border {
                "跨境段到达"
            } else {
                "境内段到达"
            },
        ));
    }
 
    // ---------- 送达日与周末顺延 ----------
    let raw_delivery_date: CalendarDate = cursor;
    let mut delivery_date: CalendarDate = raw_delivery_date;
    let mut working_day_guard: u32 = 0;
    while !is_working_day(&delivery_date) && working_day_guard < 7 {
        delivery_date = delivery_date.add_days(1);
        working_day_guard += 1;
    }
    if delivery_date != raw_delivery_date {
        delayed_by_weekend = true;
    }
 
    milestones.push(TimelineMilestone::new(
        "DELIVERED",
        "预计派送",
        delivery_date,
        if delivery_date != raw_delivery_date {
            "到达日落在周末,已顺延至下一工作日派送"
        } else {
            "按路由推算派送"
        },
    ));
 
    CarrierTimeline::new(milestones, delivery_date, delayed_by_weekend)
}
 
 
//!# 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      : capacity_ledger.rs
//! 舱位与承重台账(调度子系统的有状态部分)。
//!
//! ## 为什么调度子系统需要一个有状态组件
//!
//! 门面在多幕演示里要装配多票货。若每次都从「无限运力」出发,
//! 就无法展示「门面把多个子系统的状态汇聚起来共同决策」这一价值。
//! 台账记录每个承运商已占用的重量与舱位,使第二个候选方案的报价
//! 可能因运力紧张而上浮------这是门面调用 `容量台账` 后得到的**真实业务结果**。
//!
//! ## 状态为什么放在子系统而不是门面
//!
//! 台账是「承运商运力」这个领域概念的自然归属。
//! 若把它提到门面,门面就成了上帝对象;放在这里,
//! 门面只调用 `try_reserve` 拿结果,不知道台账怎么实现。
 
use crate::domain::ShippingWeight;
 
/// 单个承运商的运力占用情况。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CarrierLoad {
    /// 承运商代码。
    carrier_code: &'static str,
    /// 承重上限(毫克)。
    capacity_limit: ShippingWeight,
    /// 已占用重量(毫克)。
    occupied_weight: ShippingWeight,
}
 
impl CarrierLoad {
    /// 剩余可用重量。
    fn remaining_weight(&self) -> ShippingWeight {
        // 用减法:若已占用超过上限(不应发生),减法内部会饱和到 0 以下,
        // 这里再钳到零,保证「剩余」永远非负。
        let remaining_milligrams: i64 =
            self.capacity_limit.milligrams() - self.occupied_weight.milligrams();
        if remaining_milligrams < 0 {
            ShippingWeight::zero()
        } else {
            ShippingWeight::from_milligrams(remaining_milligrams)
        }
    }
 
    /// 占用率(万分比)。上限为 0 时返回 0,避免除零。
    fn utilization_basis_points(&self) -> i64 {
        if self.capacity_limit.milligrams() <= 0 {
            return 0;
        }
        // i128 中间量:毫克 × 10000 可能超出 i32 但远在 i64 内,
        // 仍走 i128 以与全工程口径一致。
        let numerator: i128 =
            self.occupied_weight.milligrams() as i128 * 10_000i128;
        (numerator / self.capacity_limit.milligrams() as i128) as i64
    }
}
 
/// 承运商运力台账。
///
/// 只维护「承重」这一维度(舱位维度同理,本工程不展开,
/// 但结构上预留:新增一个 `occupied_volume` 字段即可,门面无需改动)。
pub struct CapacityLedger {
    /// 各承运商的负载记录。
    loads: Vec<CarrierLoad>,
}
 
impl CapacityLedger {
    /// 创建一个空台账。
    ///
    /// 参数 `carriers`:承运商代码与承重上限的列表。
    /// 返回:初始化后的台账(已占用为 0)。
    pub fn new(carriers: &[(&'static str, ShippingWeight)]) -> Self {
        let mut loads: Vec<CarrierLoad> = Vec::with_capacity(carriers.len());
        for (carrier_code, capacity_limit) in carriers {
            loads.push(CarrierLoad {
                carrier_code,
                // 复制上限值:ShippingWeight 是 Copy,按值传递,无别名问题。
                capacity_limit: *capacity_limit,
                occupied_weight: ShippingWeight::zero(),
            });
        }
        CapacityLedger { loads }
    }
 
    /// 查找某承运商的负载记录。
    fn find_load(&self, carrier_code: &str) -> Option<&CarrierLoad> {
        // 线性查找:承运商通常不超过十家,无需索引结构。
        self.loads
            .iter()
            .find(|load| load.carrier_code == carrier_code)
    }
 
    /// 查找某承运商的负载记录(可变)。
    fn find_load_mut(&mut self, carrier_code: &str) -> Option<&mut CarrierLoad> {
        self.loads
            .iter_mut()
            .find(|load| load.carrier_code == carrier_code)
    }
 
    /// 判断某承运商能否再承接指定重量。
    ///
    /// 参数 `carrier_code` / `weight`。
    /// 返回:能承接返回 `true`;承运商不存在返回 `false`(视为不可用,
    /// 而不是 panic------配置缺失应当是业务异常而非程序崩溃)。
    pub fn can_accept(&self, carrier_code: &str, weight: &ShippingWeight) -> bool {
        match self.find_load(carrier_code) {
            Some(load) => {
                let remaining: ShippingWeight = load.remaining_weight();
                // 「不严格大于剩余」即剩余 >= 重量。
                !weight.is_greater_than(&remaining)
            }
            None => false,
        }
    }
 
    /// 尝试为某承运商预留重量。
    ///
    /// 参数 `carrier_code` / `weight`。
    /// 返回:预留成功返回 `Ok(())`,失败返回 `Err(原因文本)`。
    ///
    /// 用 `Result` 而非 `bool`:调用方(调度子系统)需要把失败原因
    /// 写进候选方案的不可能行原因里,最终成为门面报表上的一行提示。
    /// 若只返回 `bool`,这个原因就丢了。
    pub fn try_reserve(
        &mut self,
        carrier_code: &str,
        weight: &ShippingWeight,
    ) -> Result<(), &'static str> {
        let load: &mut CarrierLoad = match self.find_load_mut(carrier_code) {
            Some(found) => found,
            None => return Err("该承运商未接入运力台账"),
        };
 
        let remaining: ShippingWeight = load.remaining_weight();
        if weight.is_greater_than(&remaining) {
            return Err("承运商剩余运力不足");
        }
 
        // 累加占用。注意这里用 add(内部饱和加法),
        // 且上面已确保不会超上限,因此不会触发饱和。
        load.occupied_weight = load.occupied_weight.add(weight);
        Ok(())
    }
 
    /// 读取某承运商的占用率(万分比)。
    ///
    /// 参数 `carrier_code`。
    /// 返回:占用率万分比;承运商不存在返回 `None`。
    ///
    /// 门面用这个值在报表里展示「运力紧张度」,并据此在超过阈值时
    /// 追加一条警告------这条警告的来源是**子系统提供的真实数据**,
    /// 而不是门面自己编的启发式规则。
    pub fn utilization_basis_points(&self, carrier_code: &str) -> Option<i64> {
        self.find_load(carrier_code)
            .map(|load| load.utilization_basis_points())
    }
 
    /// 返回已接入的承运商数量。
    pub fn carrier_count(&self) -> usize {
        self.loads.len()
    }
 
    /// 返回所有承运商的(代码, 已占用重量, 承重上限)三元组,供报表展示。
    pub fn snapshot(&self) -> Vec<(&'static str, ShippingWeight, ShippingWeight)> {
        self.loads
            .iter()
            .map(|load| (load.carrier_code, load.occupied_weight, load.capacity_limit))
            .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      : carrier_option.rs
//! 一个候选承运方案(调度子系统的输出结构)。
//!
//! ## 为什么叫「候选项」而不是「承运方案」
//!
//! 调度子系统只负责**枚举可行方案并给出报价与时效**,不负责「选哪一个」。
//! 「选哪个」是需要综合包装成本、单据复杂度、客户等级的商业决策------
//! 那是门面的职责。若把选择也塞进调度子系统,门面就退化成单纯的转发器,
//! 模式也就没有存在意义了。
//!
//! 这个区分在代码上表现为:本结构体没有任何「是否推荐」的方法,
//! 只有一个 `is_feasible`(是否满足硬约束)。
 
use crate::domain::{CountryCode, CurrencyAmount, ServiceTier, ShippingWeight};
 
/// 一段路由(从一地到另一地的单一运输段)。
///
/// 放在调度子系统内而不是 product 层,因为「路由段」是调度计算的自然产物;
/// 门面会把它转写成对外的视图类型。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteLeg {
    /// 段序号(从 1 开始,便于报表直接印)。
    sequence_number: u32,
    /// 起点城市。
    origin_city: &'static str,
    /// 起点国家/地区。
    origin_country: CountryCode,
    /// 终点城市。
    destination_city: &'static str,
    /// 终点国家/地区。
    destination_country: CountryCode,
    /// 该段的运输方式描述(如「公路运输」)。
    transport_mode_label: &'static str,
    /// 该段单独占用的天数。
    transit_days: u32,
}
 
impl RouteLeg {
    /// 构造一段路由。
    pub const fn new(
        sequence_number: u32,
        origin_city: &'static str,
        origin_country: CountryCode,
        destination_city: &'static str,
        destination_country: CountryCode,
        transport_mode_label: &'static str,
        transit_days: u32,
    ) -> Self {
        RouteLeg {
            sequence_number,
            origin_city,
            origin_country,
            destination_city,
            destination_country,
            transport_mode_label,
            transit_days,
        }
    }
 
    /// 段序号。
    pub const fn sequence_number(&self) -> u32 {
        self.sequence_number
    }
 
    /// 起点城市。
    pub const fn origin_city(&self) -> &'static str {
        self.origin_city
    }
 
    /// 起点国家/地区。
    pub const fn origin_country(&self) -> CountryCode {
        self.origin_country
    }
 
    /// 终点城市。
    pub const fn destination_city(&self) -> &'static str {
        self.destination_city
    }
 
    /// 终点国家/地区。
    pub const fn destination_country(&self) -> CountryCode {
        self.destination_country
    }
 
    /// 运输方式描述。
    pub const fn transport_mode_label(&self) -> &'static str {
        self.transport_mode_label
    }
 
    /// 该段天数。
    pub const fn transit_days(&self) -> u32 {
        self.transit_days
    }
 
    /// 该段是否为跨境段(起终点分属不同国家/地区)。
    ///
    /// 用于门面判断是否需要附加清关时间------注意判断依据是
    /// `CountryCode::code` 字符串比较,而不是枚举匹配,
    /// 这样工程外新增的国家也能正确参与判断。
    pub fn is_cross_border(&self) -> bool {
        self.origin_country.code() != self.destination_country.code()
    }
}
 
/// 一个候选承运方案。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CarrierOption {
    /// 承运商代码(如 `"SF"`)。
    carrier_code: &'static str,
    /// 承运商中文名。
    carrier_label: &'static str,
    /// 服务等级。
    service_tier: ServiceTier,
    /// 路由段列表(按序)。
    route_legs: Vec<RouteLeg>,
    /// 基础运费(不含服务等级附加费)。
    base_freight_charge: CurrencyAmount,
    /// 计费重量(承运商采用的计费口径,通常取实重与体积重的较大者)。
    chargeable_weight: ShippingWeight,
    /// 是否满足所有硬约束(承重上限、温控能力等)。
    is_feasible: bool,
    /// 若不满足,说明原因(供门面汇总成事件)。
    infeasibility_reason: &'static str,
}
 
impl CarrierOption {
    /// 构造一个候选方案。
    ///
    /// 参数较多,因此这个构造函数是「内部使用」性质的------
    /// 只有 [`super::route_planner`] 会调用它。对外(门面)只读方法足够。
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        carrier_code: &'static str,
        carrier_label: &'static str,
        service_tier: ServiceTier,
        route_legs: Vec<RouteLeg>,
        base_freight_charge: CurrencyAmount,
        chargeable_weight: ShippingWeight,
        is_feasible: bool,
        infeasibility_reason: &'static str,
    ) -> Self {
        CarrierOption {
            carrier_code,
            carrier_label,
            service_tier,
            route_legs,
            base_freight_charge,
            chargeable_weight,
            is_feasible,
            infeasibility_reason,
        }
    }
 
    /// 承运商代码。
    pub const fn carrier_code(&self) -> &'static str {
        self.carrier_code
    }
 
    /// 承运商中文名。
    pub const fn carrier_label(&self) -> &'static str {
        self.carrier_label
    }
 
    /// 服务等级。
    pub const fn service_tier(&self) -> ServiceTier {
        self.service_tier
    }
 
    /// 路由段列表。
    pub fn route_legs(&self) -> &[RouteLeg] {
        &self.route_legs
    }
 
    /// 基础运费。
    pub const fn base_freight_charge(&self) -> CurrencyAmount {
        self.base_freight_charge
    }
 
    /// 计费重量。
    pub const fn chargeable_weight(&self) -> ShippingWeight {
        self.chargeable_weight
    }
 
    /// 是否可行。
    pub const fn is_feasible(&self) -> bool {
        self.is_feasible
    }
 
    /// 不可行原因。
    pub const fn infeasibility_reason(&self) -> &'static str {
        self.infeasibility_reason
    }
 
    /// 路由段数。
    pub fn route_leg_count(&self) -> usize {
        self.route_legs.len()
    }
 
    /// 路由总时效(各段天数之和)。
    ///
    /// 注意这里**只是路段天数之和**,不含清关等待。
    /// 清关时间由门面在生成时间线时另行加入------
    /// 因为清关时间取决于目的地与品类,属于门面能看到的跨子系统信息,
    /// 调度子系统看不到,也不应该猜。
    pub fn total_transit_days(&self) -> u32 {
        let mut total: u32 = 0;
        for leg in &self.route_legs {
            total = total.saturating_add(leg.transit_days());
        }
        total
    }
 
    /// 该方案是否包含跨境段。
    pub fn has_cross_border_leg(&self) -> bool {
        self.route_legs.iter().any(|leg| leg.is_cross_border())
    }
 
    /// 途经国家/地区(按出现顺序去重)。
    ///
    /// 去重而不用 `HashSet`:段数很少(个位数),
    /// 线性查找既保序又省一次哈希分配,且输出顺序稳定(便于报表比对)。
    pub fn visited_country_codes(&self) -> Vec<&'static str> {
        let mut visited: Vec<&'static str> = Vec::new();
        for leg in &self.route_legs {
            let origin_code: &'static str = leg.origin_country().code();
            if !visited.contains(&origin_code) {
                visited.push(origin_code);
            }
            let destination_code: &'static str = leg.destination_country().code();
            if !visited.contains(&destination_code) {
                visited.push(destination_code);
            }
        }
        visited
    }
 
    /// 返回路由的一行式摘要文本,供报表使用。
    pub fn route_summary_text(&self) -> String {
        if self.route_legs.is_empty() {
            return "(无路由)".to_string();
        }
        let mut summary: String = String::new();
        for (position, leg) in self.route_legs.iter().enumerate() {
            if position > 0 {
                summary.push_str(" → ");
            }
            // 只印城市名,国家码单独在途经国家栏展示,避免行过长。
            summary.push_str(leg.origin_city());
        }
        // 补上最后一段的终点。
        if let Some(last_leg) = self.route_legs.last() {
            summary.push_str(" → ");
            summary.push_str(last_leg.destination_city());
        }
        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      : route_planner.rs
//! 路线规划与时效计算(调度子系统的纯计算部分)。
//!
//! ## 为什么是纯函数而不是「规划器结构体」
//!
//! 这一块没有任何需要跨调用保留的状态:
//! 给定(起点、终点、重量、温控要求、服务等级),输出候选方案。
//! 把它写成自由函数有两点好处:
//!
//! 1. **门面调用它时不必持有对象**,减少门面需要维护的字段;
//! 2. **可被反复调用而互不影响**,门面在「换承运商重算」时不必担心残留状态。
//!
//! 状态(运力台账)由调用方作为 `&mut` 参数显式传入------
//! 「谁持有状态」因此一目了然,没有隐式全局态。
 
use crate::domain::{
    CountryCode, CurrencyAmount, Ratio, ServiceTier, ShippingWeight, CURRENCY_CHINESE_YUAN,
    COUNTRY_CHINA, COUNTRY_GERMANY,
};
use crate::dispatch::capacity_ledger::CapacityLedger;
 
use super::carrier_option::{CarrierOption, RouteLeg};
 
/// 路线规划的输入参数。
///
/// 把参数收进一个结构体而不是散在函数签名里,是为了:
/// 将来新增一个考虑因素(如「是否优先低碳路线」)时,
/// 只需给本结构体加字段,所有调用点不必改签名。
#[derive(Debug, Clone)]
pub struct RoutePlanningRequest {
    /// 起始城市。
    pub origin_city: &'static str,
    /// 起始国家/地区。
    pub origin_country: CountryCode,
    /// 目的城市。
    pub destination_city: &'static str,
    /// 目的国家/地区。
    pub destination_country: CountryCode,
    /// 货物总重。
    pub total_weight: ShippingWeight,
    /// 是否需要温控。
    pub requires_temperature_control: bool,
    /// 服务等级。
    pub service_tier: ServiceTier,
}
 
/// 路线规划的结果。
#[derive(Debug, Clone)]
pub struct RoutePlanningResult {
    /// 所有候选方案(含不可行的,带原因)。
    pub options: Vec<CarrierOption>,
}
 
impl RoutePlanningResult {
    /// 返回可行的候选方案数量。
    pub fn feasible_count(&self) -> usize {
        self.options.iter().filter(|option| option.is_feasible()).count()
    }
 
    /// 返回第一个可行方案(若有)。
    ///
    /// 参数与返回:取首选项,即「报价最低的可行方案」------
    /// 因为本函数产出的 options 已按报价升序排列(见下方排序逻辑)。
    pub fn cheapest_feasible(&self) -> Option<&CarrierOption> {
        self.options.iter().find(|option| option.is_feasible())
    }
 
    /// 返回所有候选方案的数量。
    pub fn option_count(&self) -> usize {
        self.options.len()
    }
}
 
/// 对一票货做路线规划,产出候选承运方案。
///
/// 参数 `request`:规划输入;`ledger`:运力台账(会被预留占用)。
/// 返回:规划结果(含可行与不可行方案)。
///
/// ## 计算口径(可手算复核)
///
/// 1. 计费重量取**实重**(本工程不引入体积重,避免演示数据虚增;
///    真实系统里体积重是重要因素,届时改这里一处即可);
/// 2. 基础运费 = 计费重量 × 承运商每公斤费率;
/// 3. 服务等级附加费 = 基础运费 × 等级附加费率(可为负,表示折扣);
/// 4. 时效 = 各路段天数之和 × 服务等级时效倍数(四舍五入,至少 1 天);
/// 5. 温控要求会**筛掉**不具备温控能力的承运商(记为不可行,保留在列表里)。
pub fn plan_route_legs(
    request: &RoutePlanningRequest,
    ledger: &mut CapacityLedger,
) -> RoutePlanningResult {
    // ---------- 候选承运商定义 ----------
    // 每项为(代码, 中文名, 每公斤费率分, 标准时效天数, 是否支持温控)。
    // 这些是调度子系统的**内部知识**,门面看不到也不需要看到。
    //
    // 注意这里**不含承重上限**:承重核查由运力台账负责(它持有上限与已占用),
    // 若再在此处定义一份上限,就会出现「两处上限可能不一致」的隐患。
    // 单一事实来源优于就近便利。
    let carrier_definitions: [(&'static str, &'static str, i64, u32, bool); 3] = [
        ("SF", "顺丰国际", 18_000, 3, true),
        ("DHL", "DHL 全球", 24_000, 2, true),
        ("EMS", "邮政 EMS", 9_500, 6, false),
    ];
 
    let mut options: Vec<CarrierOption> = Vec::with_capacity(carrier_definitions.len());
 
    for (carrier_code, carrier_label, rate_per_kilogram, standard_days, supports_temperature) in
        carrier_definitions
    {
        // ---------- 硬约束核查:温控能力 ----------
        let temperature_conflict: bool =
            request.requires_temperature_control && !supports_temperature;
 
        // ---------- 硬约束核查:运力台账 ----------
        // 先查台账能否承接(不改状态),不可行则跳过预留。
        let capacity_ok: bool = ledger.can_accept(carrier_code, &request.total_weight);
 
        // ---------- 计算基础运费 ----------
        let base_freight_charge: CurrencyAmount = CurrencyAmount::from_minor_units(
            request.total_weight.charge_at_rate_per_kilogram(rate_per_kilogram),
            CURRENCY_CHINESE_YUAN,
        );
 
        // ---------- 应用服务等级附加费 ----------
        // 用 Ratio 走统一缩放路径;负费率时 apply_to 会得到负值,
        // 即「折扣」,再相加即得折后运费。
        let surcharge_ratio: Ratio =
            Ratio::from_basis_points(request.service_tier.surcharge_basis_points());
        let surcharge_amount: CurrencyAmount = surcharge_ratio.apply_to(&base_freight_charge);
        let freight_with_surcharge: CurrencyAmount =
            base_freight_charge.add(&surcharge_amount);
 
        // ---------- 计算时效 ----------
        // 服务等级倍数:standard_days × multiplier / 10000,四舍五入且至少 1 天。
        let multiplier: i64 = request.service_tier.transit_days_multiplier_basis_points();
        let scaled_days_numerator: i128 = standard_days as i128 * multiplier as i128;
        let mut effective_days: i128 =
            (scaled_days_numerator + 5_000) / 10_000; // 加半个分母实现四舍五入
        if effective_days < 1 {
            effective_days = 1; // 任何路线至少一天,避免「当日达」出现 0 天导致日期推算无变化
        }
        let effective_days_value: u32 = effective_days as u32;
 
        // ---------- 构造路由段 ----------
        let route_legs: Vec<RouteLeg> = build_default_route_legs(
            request,
            effective_days_value,
            carrier_label,
        );
 
        // ---------- 判定整体可行性 ----------
        let (is_feasible, infeasibility_reason): (bool, &'static str) = if temperature_conflict {
            (false, "该承运商不具备温控能力")
        } else if !capacity_ok {
            (false, "剩余运力不足")
        } else {
            (true, "")
        };
 
        // 可行方案才真正占用运力(避免为不可行方案预留,造成台账虚高)。
        if is_feasible {
            // 台账保证能成功:上面已用 can_accept 预检过。
            // 若这里仍失败,说明台账在两次调用间被并发改动------
            // 本工程为单线程演示,故用 ignore 兜底并保留可读性。
            let _ = ledger.try_reserve(carrier_code, &request.total_weight);
        }
 
        options.push(CarrierOption::new(
            carrier_code,
            carrier_label,
            request.service_tier,
            route_legs,
            // 注意:这里存的是「含等级附加费」的总运费,
            // 门面报表里统一称其为「基础运费」,含义已包含等级调整。
            // 若将来要拆分展示,应在此处改成携带 (base, surcharge) 二元组。
            freight_with_surcharge,
            // 计费重量:本工程取实重。
            request.total_weight,
            is_feasible,
            infeasibility_reason,
        ));
    }
 
    // ---------- 排序:可行优先、报价升序 ----------
    // 用「可行在前」作为第一关键字,避免不可行方案因报价低而排到前面,
    // 使门面取首选项时误取到不可行方案(本工程取首项即可行最优)。
    options.sort_by(|left, right| {
        // 可行方案排前面:把 bool 转成可比较的整数(true → 0,false → 1)。
        let left_rank: u8 = if left.is_feasible() { 0 } else { 1 };
        let right_rank: u8 = if right.is_feasible() { 0 } else { 1 };
        left_rank
            .cmp(&right_rank)
            .then_with(|| {
                left.base_freight_charge()
                    .minor_units()
                    .cmp(&right.base_freight_charge().minor_units())
            })
    });
 
    RoutePlanningResult { options }
}
 
/// 按「境内段 + 跨境干线 + 目的国派送段」的固定形态构造路由。
///
/// 参数 `request`:规划输入;`total_days`:该方案的总时效天数;
/// `carrier_label`:承运商名(用于运输方式描述)。
/// 返回:路由段列表。
///
/// ## 天数如何分配
///
/// 三段的天数分配规则为「境内 1 天、跨境干线占大头、派送 1 天」,
/// 且保证三段之和恰为 `total_days`(用减法而非各自独立取整,
/// 避免出现「三段之和 ≠ 总时效」的自相矛盾------这是报表常见的一致性缺陷)。
fn build_default_route_legs(
    request: &RoutePlanningRequest,
    total_days: u32,
    carrier_label: &'static str,
) -> Vec<RouteLeg> {
    // 只有一段的情形(同城或承运商给出的极速方案):整段直接表达。
    if total_days <= 1 {
        return vec![RouteLeg::new(
            1,
            request.origin_city,
            request.origin_country,
            request.destination_city,
            request.destination_country,
            "直达运输",
            total_days.max(1),
        )];
    }
 
    // 两段的情形:境内 + 跨境。
    if total_days == 2 {
        return vec![
            RouteLeg::new(
                1,
                request.origin_city,
                request.origin_country,
                "口岸",
                COUNTRY_CHINA,
                "公路运输",
                1,
            ),
            RouteLeg::new(
                2,
                "口岸",
                COUNTRY_CHINA,
                request.destination_city,
                request.destination_country,
                // 跨境段用「空运」表述:演示里中欧方向以空运为主。
                "航空运输",
                1,
            ),
        ];
    }
 
    // 三段及以上:境内 1 天 + 干线 (total - 2) 天 + 派送 1 天。
    // 干线天数最少 1 天(total 至少为 3,故 total - 2 至少为 1)。
    let trunk_days: u32 = total_days - 2;
    vec![
        RouteLeg::new(
            1,
            request.origin_city,
            request.origin_country,
            "广州",
            COUNTRY_CHINA,
            "公路运输",
            1,
        ),
        RouteLeg::new(
            2,
            "广州",
            COUNTRY_CHINA,
            "法兰克福",
            COUNTRY_GERMANY,
            // 干线用承运商名标注,便于报表区分不同承运商的干线安排。
            carrier_label,
            trunk_days,
        ),
        RouteLeg::new(
            3,
            "法兰克福",
            COUNTRY_GERMANY,
            request.destination_city,
            request.destination_country,
            "公路运输",
            1,
        ),
    ]
}
 
/// 计算一组路由段的总时效天数。
///
/// 参数 `route_legs`:路由段切片。
/// 返回:天数总和。
///
/// 单独提供这个自由函数(而非只依赖 `CarrierOption::total_transit_days`),
/// 是因为门面可能在**尚未构造 CarrierOption 之前**就要算时效
/// (例如从工程外定义的子系统返回的段列表上直接计算)。
pub fn total_transit_days(route_legs: &[RouteLeg]) -> u32 {
    let mut total: u32 = 0;
    for leg in route_legs {
        total = total.saturating_add(leg.transit_days());
    }
    total
}
 
 
//!# 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_checklist.rs
//! 单据清单值对象(单据子系统的输出结构)。
//!
//! ## 为什么区分「必备」与「建议」
//!
//! 门面对单据的处置是:必备缺失 → 严重级事件(阻断);
//! 建议缺失 → 警告级事件(不阻断但需确认)。
//! 这个区分若只在门面用 `if` 表达,子系统就无法表达自己的专业判断。
//! 因此把 `is_mandatory` 作为数据放在子系统输出里,
//! 门面只做「映射」不做「判断」------**判断归专家子系统,门面只做汇总**。
 
use crate::domain::DocumentKind;
 
/// 一份单据在本次业务中的状态。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocumentStatus {
    /// 已具备(本工程演示中表示「清单里已列入且可生成」)。
    Available,
    /// 需要但尚未生成。
    Pending,
    /// 已豁免(如品类无需报关时的报关单)。
    Waived,
}
 
impl DocumentStatus {
    /// 中文标签。
    pub const fn label(&self) -> &'static str {
        match self {
            DocumentStatus::Available => "已具备",
            DocumentStatus::Pending => "待生成",
            DocumentStatus::Waived => "已豁免",
        }
    }
 
    /// 该状态是否要求门面产生事件。
    ///
    /// 只有 `Pending` 需要关注。这里是**行为分派**,
    /// 所以 `DocumentStatus` 用枚举而不是开放型结构体------
    /// 与 [`crate::domain::event_severity`] 同一个判据。
    pub const fn needs_attention(&self) -> bool {
        match self {
            DocumentStatus::Available | DocumentStatus::Waived => false,
            DocumentStatus::Pending => true,
        }
    }
}
 
/// 单据清单中的一项。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DocumentRequirement {
    /// 单据类型。
    kind: DocumentKind,
    /// 该项状态。
    status: DocumentStatus,
    /// 是否为强制项(缺失即阻断)。
    is_mandatory: bool,
    /// 触发该项的原因说明(供报表展示「为什么要这张单」)。
    trigger_reason: &'static str,
}
 
impl DocumentRequirement {
    /// 构造一项单据要求。
    pub const fn new(
        kind: DocumentKind,
        status: DocumentStatus,
        is_mandatory: bool,
        trigger_reason: &'static str,
    ) -> Self {
        DocumentRequirement {
            kind,
            status,
            is_mandatory,
            trigger_reason,
        }
    }
 
    /// 单据类型。
    pub const fn kind(&self) -> DocumentKind {
        self.kind
    }
 
    /// 该项状态。
    pub const fn status(&self) -> DocumentStatus {
        self.status
    }
 
    /// 是否为强制项。
    pub const fn is_mandatory(&self) -> bool {
        self.is_mandatory
    }
 
    /// 触发原因。
    pub const fn trigger_reason(&self) -> &'static str {
        self.trigger_reason
    }
 
    /// 该项是否会阻断出单(强制 且 待生成)。
    pub const fn blocks_dispatch(&self) -> bool {
        // 两个 bool 相与,语义清晰,无需 match。
        self.is_mandatory && self.status.needs_attention()
    }
 
    /// 返回一行展示文本。
    ///
    /// 与 `ChargeItem::formatted` 同属「子系统越界做排版」的例子。第七幕会
    /// 打印它与门面视图并排对照:子系统版本塞进了「编码 + 括号 + 分隔符」,
    /// 换列宽时无从调整;门面版本只有裸数据,排版权留在表示层。
    pub fn formatted(&self) -> String {
        format!(
            "{}({})· {} · {}",
            self.kind.label(),
            self.kind.code(),
            self.status.label(),
            self.trigger_reason()
        )
    }
}
 
/// 一整份单据清单。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentChecklist {
    /// 清单项列表。
    requirements: Vec<DocumentRequirement>,
    /// 生成该清单时的目的地代码(用于报表标注口径)。
    destination_country_code: &'static str,
}
 
impl DocumentChecklist {
    /// 构造一份单据清单。
    pub fn new(
        requirements: Vec<DocumentRequirement>,
        destination_country_code: &'static str,
    ) -> Self {
        DocumentChecklist {
            requirements,
            destination_country_code,
        }
    }
 
    /// 清单项列表。
    pub fn requirements(&self) -> &[DocumentRequirement] {
        &self.requirements
    }
 
    /// 目的地代码。
    pub const fn destination_country_code(&self) -> &'static str {
        self.destination_country_code
    }
 
    /// 清单项总数。
    pub fn total_count(&self) -> usize {
        self.requirements.len()
    }
 
    /// 强制项数量。
    pub fn mandatory_count(&self) -> usize {
        self.requirements
            .iter()
            .filter(|requirement| requirement.is_mandatory())
            .count()
    }
 
    /// 待生成项数量。
    pub fn pending_count(&self) -> usize {
        self.requirements
            .iter()
            .filter(|requirement| requirement.status().needs_attention())
            .count()
    }
 
    /// 会阻断出单的项数量。
    pub fn blocking_count(&self) -> usize {
        self.requirements
            .iter()
            .filter(|requirement| requirement.blocks_dispatch())
            .count()
    }
 
    /// 是否已全部齐备(无待生成项)。
    pub fn is_complete(&self) -> bool {
        self.pending_count() == 0
    }
 
    /// 返回所有单据类型编码(按清单顺序)。
    pub fn document_codes(&self) -> Vec<&'static str> {
        self.requirements
            .iter()
            .map(|requirement| requirement.kind().code())
            .collect()
    }
 
    /// 返回清单的一行式文本,如 `"商业发票、装箱单、原产地证书"`。
    pub fn summary_text(&self) -> String {
        if self.requirements.is_empty() {
            return "(无需单据)".to_string();
        }
        let mut summary: String = String::new();
        for (position, requirement) in self.requirements.iter().enumerate() {
            if position > 0 {
                summary.push('、');
            }
            summary.push_str(requirement.kind().label());
        }
        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      : document_compiler.rs
//! 单据清单编成(单据子系统的规则表驱动部分)。
//!
//! ## 核心机制:规则表 + 求并集
//!
//! 一张 `DocumentRule` 表达「当满足某条件时,需要某单据」。
//! 编成时遍历全部规则,把命中的规则对应的单据收进清单。
//!
//! ## 为什么规则里的「条件」用开放型判定函数而不是枚举
//!
//! 若条件写成 `enum DocumentTrigger { IsCrossBorder, IsJewelry, ... }`,
//! 那么工程外想加一条「若途经香港则需中转声明」就必须改这个枚举------
//! 扩展性就没了。因此条件被表达为一个**谓词函数指针**
//! (`fn(&DocumentRequest) -> bool`),工程外可自由追加规则。
//!
//! 代价:规则无法被静态穷尽检查。收益:扩展零成本。
//! 这正是本工程一贯的取舍方向------**扩展性优先,辅以运行期审计**
//! (门面会把整张规则表打印出来供合规同事审阅)。
 
use crate::domain::{
    DocumentKind, DOCUMENT_CERTIFICATE_OF_ORIGIN, DOCUMENT_COMMERCIAL_INVOICE,
    DOCUMENT_PACKING_LIST, DOCUMENT_TEMPERATURE_DECLARATION,
};
 
use super::document_checklist::{
    DocumentChecklist, DocumentRequirement, DocumentStatus,
};
 
/// 单据编成的输入。
#[derive(Debug, Clone)]
pub struct DocumentRequest {
    /// 是否为跨境运输。
    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,
}
 
/// 一条单据编成规则。
///
/// ## 为什么用函数指针而不是 trait 对象
///
/// 规则数很少(个位数),且都是无状态的纯谓词函数。
/// 用 `fn(&DocumentRequest) -> bool` 比 `Box<dyn Fn>` 更轻
/// (无堆分配、`Copy` 语义、可在常量数组里定义)。
/// 仅当规则需要捕获环境时才需要升级为 boxed closure------
/// 那时再改,不必提前复杂化。
#[derive(Clone, Copy)]
pub struct DocumentRule {
    /// 命中的判定条件。
    pub matches: fn(&DocumentRequest) -> bool,
    /// 命中后需要追加的单据类型。
    pub kind: DocumentKind,
    /// 是否为强制项。
    pub is_mandatory: bool,
    /// 触发原因说明(报表展示用)。
    pub trigger_reason: &'static str,
}
 
/// 内置规则集。
///
/// 定义为一个常量函数返回的数组,便于门面/工程外把它取出来审计。
///
/// 注意这里**不是** `const` 数组:
/// 函数指针在常量数组里是合法的,但为了将来可能改成捕获环境的闭包,
/// 保留函数形式能减少改动面。
pub fn builtin_rules() -> [DocumentRule; 4] {
    [
        // 规则 1:跨境运输必出商业发票。
        DocumentRule {
            matches: |request: &DocumentRequest| request.is_cross_border,
            kind: DOCUMENT_COMMERCIAL_INVOICE,
            is_mandatory: true,
            trigger_reason: "跨境运输需要商业发票作为成交凭证",
        },
        // 规则 2:跨境运输必出装箱单。
        DocumentRule {
            matches: |request: &DocumentRequest| request.is_cross_border,
            kind: DOCUMENT_PACKING_LIST,
            is_mandatory: true,
            trigger_reason: "跨境运输需要装箱单核对件数与重量",
        },
        // 规则 3:高值货必出原产地证书(用于关税优惠与合规溯源)。
        DocumentRule {
            matches: |request: &DocumentRequest| request.has_high_value_cargo,
            kind: DOCUMENT_CERTIFICATE_OF_ORIGIN,
            // 注:这里设为强制,演示「高值货缺产地证会阻断」;
            // 真实的产地证常可后补,届时把此值改为 false 即可,
            // 无需改动任何其他代码------这正是把「强制与否」做成数据的价值。
            is_mandatory: true,
            trigger_reason: "高值货需原产地证书用于关税优惠与溯源",
        },
        // 规则 4:温控货建议出温控声明(非强制)。
        DocumentRule {
            matches: |request: &DocumentRequest| request.requires_temperature_control,
            kind: DOCUMENT_TEMPERATURE_DECLARATION,
            is_mandatory: false,
            trigger_reason: "温控货建议附温控声明以便承运方交接确认",
        },
    ]
}
 
/// 按规则表编成单据清单。
///
/// 参数 `request`:编成输入;`extra_rules`:门面或工程外追加的规则。
/// 返回:单据清单。
///
/// ## 处理顺序与去重
///
/// 先跑内置规则,再跑附加规则;同一单据类型**只保留首次命中**的条目
/// (即内置规则的判定优先)。保持首次命中而不是后者覆盖,
/// 是为了让「内置规则的口径」稳定可预期。
///
/// ## 状态如何决定(务必看清)
///
/// 单据子系统**不掌握「是否真能取到数据」**,那是门面汇聚后才能回答的,
/// 所以这里的判定只依据编成输入本身:
///
/// - 有可申报品类但申报价值为 0 → 原产地证书标为 [`DocumentStatus::Pending`]
///   (高值货却报零,实际取不到合规的产地证数据);
/// - 其余命中项一律标为 [`DocumentStatus::Available`]。
///
/// 这条判定让「必备单据缺失」成为**真实可达**的路径,
/// 而不是靠门面硬造一个错误来演示(那是假演示)。
pub fn compile_document_checklist(
    request: &DocumentRequest,
    extra_rules: &[DocumentRule],
) -> DocumentChecklist {
    let mut requirements: Vec<DocumentRequirement> = Vec::new();
 
    // 内置规则优先。
    for rule in builtin_rules().iter() {
        append_if_matched(rule, request, &mut requirements);
    }
    // 附加规则其次。
    for rule in extra_rules.iter() {
        append_if_matched(rule, request, &mut requirements);
    }
 
    // ---------- 状态订正:高值线触发但申报价值为零 ----------
    // 判据说明:`has_high_value_cargo` 由门面按申报价值计算;
    // 若它命中(说明该走原产地证书流程)却......等等------两者是同一个数,
    // 不会互相矛盾。真正会矛盾的是「有可申报品类,但门面报上来的
    // `has_high_value_cargo` 为假」:那意味着这类货物按规则该有产地证,
    // 却因申报价值不足而取不到。此时把该项标为待生成。
    if request.has_declarable_cargo && !request.has_high_value_cargo {
        // 找到原产地证书那一项并订正状态。
        for requirement in requirements.iter_mut() {
            if requirement.kind().code() == DOCUMENT_CERTIFICATE_OF_ORIGIN.code()
                && requirement.status() == DocumentStatus::Available
            {
                // 重新构造该项:状态改为 Pending,其余字段沿用。
                *requirement = DocumentRequirement::new(
                    requirement.kind(),
                    DocumentStatus::Pending,
                    requirement.is_mandatory(),
                    "存在可申报品类但申报价值不足以支撑原产地证书数据",
                );
            }
        }
    }
 
    // 目的地要求正式报关,但货物中无可申报品类时,
    // 补一条「报关类单据豁免」提示项------让运营知道「不是漏了,是确实不需要」。
    // 这类「显式豁免」比「什么都不出现」对使用者友好得多。
    if request.destination_requires_customs_declaration && !request.has_declarable_cargo {
        let already_has_origin: bool = requirements
            .iter()
            .any(|requirement| requirement.kind().code() == DOCUMENT_CERTIFICATE_OF_ORIGIN.code());
        if !already_has_origin {
            requirements.push(DocumentRequirement::new(
                DOCUMENT_CERTIFICATE_OF_ORIGIN,
                DocumentStatus::Waived,
                false,
                "目的国要求报关但本票货无可申报品类,本单豁免",
            ));
        }
    }
 
    DocumentChecklist::new(requirements, request.destination_country_code)
}
 
/// 若规则命中且清单中尚无该单据类型,则追加一项。
///
/// 参数 `rule` / `request` / `requirements`。
/// 提取成独立函数是为了让去重逻辑只有一处实现------
/// 内置规则与附加规则共用,避免两条路径的判重口径不一致。
fn append_if_matched(
    rule: &DocumentRule,
    request: &DocumentRequest,
    requirements: &mut Vec<DocumentRequirement>,
) {
    // 条件不命中直接返回。
    if !(rule.matches)(request) {
        return;
    }
    // 去重:同一单据类型只保留首次命中。
    let already_present: bool = requirements
        .iter()
        .any(|existing| existing.kind().code() == rule.kind.code());
    if already_present {
        return;
    }
    requirements.push(DocumentRequirement::new(
        rule.kind,
        DocumentStatus::Available,
        rule.is_mandatory,
        rule.trigger_reason,
    ));
}
  

 

调用:

rust 复制代码
//!# 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      : main.rs
//! # Facade 模式(门面模式)严格分层示范工程 ------ 跨境珠宝物流一票到底
//!
//! ## 业务域
//!
//! 一票跨境珠宝货物从「客户下单」到「拿到可执行方案」的全过程。
//! 这个过程在真实系统里需要协调五个彼此独立的能力:
//!
//! | 能力 | 回答的问题 |
//! |---|---|
//! | 调度 | 用哪家承运商、走哪条路线、多久到 |
//! | 包装 | 用什么材料、包完多重、包装花多少钱 |
//! | 单据 | 该备哪些单、齐没齐、缺哪张 |
//! | 承运 | 哪天发货、哪天到、中间每个节点落在哪天 |
//! | 结算 | 一共多少钱、钱花在哪几块 |
//!
//! 若让调用方自己依次调用这五个子系统,它必须知道:
//! **调用顺序**(包装必须在调度前,因为包装增重会改变计费重量)、
//! **各子系统的输入类型**、**如何把 A 的输出翻译成 B 的输入**。
//! 这三件事都与「调用方自己的业务意图」无关,却成了它必须背负的知识。
//!
//! 门面把这些知识全部收进 `facade::ShippingFacade`,
//! 对外只暴露「把这票货安排走」这一个动作。
//!
//! ## 这个域为什么能凸显 Facade 的价值
//!
//! 门面模式常被误解为「把好几个调用包成一个方法」------那只是语法糖。
//! 它真正的价值有两条,本工程把两条都做到可见:
//!
//! 1. **编排顺序本身就是业务知识**。「先包装再调度」不是随口定的,
//!    而是因为包装改变计费重量。这条约束只有门面知道,
//!    调用方无从得知,因此不该由调用方负责。
//! 2. **跨子系统的翻译只有门面能做**。包装子系统不知道该收多少运费,
//!    结算子系统不知道运费从哪来。把「运费」变成一条「费用项」
//!    这件事,需要同时看见两侧------只有门面同时看得见。
//!
//! 所以本工程的验收点之一是:**门面不是转发器**。
//! 第三幕会拿出三条可检验的证据来支撑这一点。
//!
//! ## 严格分层结构
//!
//! ```text
//! main.rs              入口:七幕编排 + 工程外扩展区(整套替换子系统)
//!   ├── app            应用层:只排版(报表段落),不出现任何算术
//!   ├── analysis       分析层:跨域相对口径的唯一来源(费用占比 / 约束体检 / 时效结构)
//!   ├── facade         门面层:★ 模式主角 ★ 编排五个子系统 + 翻译 + 汇聚事件
//!   │     ├── subsystem_ports   端口契约(只声明方法形状,不依赖子系统类型)
//!   │     ├── shipping_facade   门面本体(核心编排顺序)
//!   │     ├── shipment_request  调用方的输入类型(CargoLine / ShipmentRequest)
//!   │     ├── shipping_result   门面的输出视图类型(调用方唯一能看到的类型)
//!   │     └── builtin_*         适配器:唯一 import 子系统的地方
//!   ├── dispatch       子系统:运力调度与路线规划(候选方案 + 时效)
//!   ├── packaging      子系统:包装方案与增重、材料成本
//!   ├── documents      子系统:按规则表编成单据清单
//!   ├── carrier        子系统:把相对天数落到绝对日历日(含周末规则)
//!   ├── settlement     子系统:把中立费用项归集为应付总额
//!   ├── domain         领域层:零依赖。金额(分) / 重量(毫克) / 比率(万分比) / 开放型标签
//!   └── support        支持层:零依赖。CJK 宽度排版 / 日历日推算 / 确定性编码
//! ```
mod analysis;
mod app;
mod carrier;
mod dispatch;
mod documents;
mod domain;
mod facade;
mod packaging;
mod settlement;
mod support;
// ---------- 引入支持层 ----------
use support::calendar_date::CalendarDate;
// ---------- 引入领域层 ----------
use domain::{
CurrencyAmount, PackagingMaterial, ShippingWeight, CARGO_CERTIFICATE, CARGO_JEWELRY,
CARGO_PACKAGING, COUNTRY_CHINA, COUNTRY_GERMANY, CURRENCY_CHINESE_YUAN, PACKAGING_FOAM_BOX,
SERVICE_TIER_STANDARD,
};
// ---------- 引入门面层 ----------
// 这里把门面全部对外视图类型都引入(含只做字段读取的几个)。
// 这不是冗余:门面的输出视图就是它与调用方之间的契约,
// 契约里的每个类型都应在调用方代码里可见地出现一次,
// 否则「哪些类型属于契约」会变成只有门面自己知道的事。
// 第七幕会对这些类型做显式标注,把契约面的完整性钉死。
use facade::{
CargoLine, CarrierOptionView, CapacityPort, DispatchInput, DispatchPort, DocumentCompilationPort,
DocumentInput, DocumentRequirementView, DocumentView, FacadeEvent, PackagingInput,
PackagingMaterialLineView, PackagingPlanView, PackagingPort, PackagingView, RouteLegView,
SettlementInputItem, SettlementPort, SettlementView, ShipmentOutcome, ShipmentRequest,
ShipmentTimelineView, ShippingFacade, ShippingResult, TimelineInput, TimelineMilestoneView,
TimelinePort, TimelineView,
};
// ---------- 引入分析层 ----------
// 这里刻意把 ChargeEntry 与 ConstraintAuditEntry 也显式引入:
// 它们是被迭代元素的具体类型,虽然 for entry in &amp;xxx 可以不写类型标注,
// 但显式引入后,第七幕可以对集合做类型标注,让「这一层对外暴露的类型」
// 在调用方代码里留下痕迹------否则统一出口再导出了什么,调用方根本感知不到。
use analysis::{
audit_constraints, build_charge_breakdown, build_timeline_profile, ChargeEntry,
ConstraintAuditEntry,
};
// ---------- 引入应用层 ----------
use app::{
render_amount_line, render_cargo_section, render_charge_section, render_conclusion,
render_constraint_section, render_document_section, render_event_section, render_header,
render_packaging_section, render_route_section, render_rule, render_shipment_header,
render_summary_line, render_timeline_section, REPORT_WIDTH,
};
fn main() {
println!("{}", "═".repeat(REPORT_WIDTH));
println!("  Facade 门面模式 · 跨境珠宝物流一票到底");
println!("  调用方只说「安排走」;顺序、翻译、汇总都收在门面里");
println!("{}", "═".repeat(REPORT_WIDTH));
println!();
act_one_manual_orchestration();
act_two_facade_orchestration();
act_three_facade_is_not_a_forwarder();
act_four_external_subsystem_replacement();
act_five_failure_reporting_and_constraints();
act_six_scale_and_structure();
act_seven_read_only_inventory();

final_words();

// 演示「工程外新增的开放型标签」在全工程里的可见性。
demonstrate_external_packaging_material();
}
// ══════════════════════════════════════════════════════════════════════════
// 第一幕:手写编排 ------ 展示「没有门面时调用方要背多少知识」
// ══════════════════════════════════════════════════════════════════════════
/// 第一幕:调用方自己依次调用各子系统完成装配。
///
/// ## 这一幕的意义是「反衬」
///
/// 下面这段代码是故意写出来的反面教材:它必须
/// 引用五个子系统、记住调用顺序、自己做翻译。
/// 第二幕换成门面后,同样的业务只需一行调用。
/// 两幕并排,门面的价值不言自明。
fn act_one_manual_orchestration() {
render_header("【第一幕】手写编排 ------ 调用方自己知道顺序、自己翻译");
let request = build_reference_request();

// 调用方需要知道:先算净重,再包装(因为包装增重影响计费重量)。
let net_weight = request.net_cargo_weight();

// ---- 步骤 1:包装(调用方必须知道要包在调度之前) ----
let packaging_request = packaging::packaging_planner::PackagingRequest {
    jewelry_piece_count: request.jewelry_piece_count(),
    other_piece_count: request.other_piece_count(),
    requires_temperature_control: request.requires_temperature_control,
    net_cargo_weight: net_weight,
};
let packaging_result = packaging::plan_packaging(&amp;packaging_request);

// ---- 步骤 2:把包装结果翻译成调度请求(调用方自己做翻译) ----
// 这里就是「翻译负担」:调用方必须知道 RoutePlanningRequest 需要哪些字段,
// 并且知道「计费重量应该取包装后总重」。
let mut ledger = build_default_ledger();
let planning_request = dispatch::RoutePlanningRequest {
    origin_city: request.sender_city,
    origin_country: request.sender_country,
    destination_city: request.receiver_city,
    destination_country: request.receiver_country,
    total_weight: packaging_result.total_weight_with_packaging(),
    requires_temperature_control: request.requires_temperature_control,
    service_tier: request.service_tier,
};
let planning_result = dispatch::plan_route_legs(&amp;planning_request, &amp;mut ledger);
let chosen = planning_result
    .cheapest_feasible()
    .expect("第一幕演示数据保证有可行方案");

// ---- 步骤 3:时间线(调用方要自己把路由段拆成天数与跨境标志) ----
let timeline_request = carrier::timeline_builder::TimelineRequest {
    dispatch_date: request.planned_dispatch_date,
    route_leg_days: chosen
        .route_legs()
        .iter()
        .map(|leg| leg.transit_days())
        .collect(),
    route_leg_is_cross_border: chosen
        .route_legs()
        .iter()
        .map(|leg| leg.is_cross_border())
        .collect(),
    destination_city: request.receiver_city,
};
let timeline_result = carrier::build_timeline(&amp;timeline_request);

// ---- 步骤 4:单据(调用方要自己判断是否跨境、是否有可申报品类) ----
let document_request = documents::document_compiler::DocumentRequest {
    is_cross_border: request.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(),
};
let checklist = documents::compile_document_checklist(&amp;document_request, &amp;[]);

// ---- 步骤 5:结算(调用方要把五方产出手工拼成费用项) ----
// 这一段最长,也最能说明问题:每一条费用项都要调用方自己组装,
// 且必须记住「包装材料成本来自 packaging、单据按张计费、
// 保价按申报价值比例」这些跨子系统的知识。
let mut charge_items: Vec&lt;settlement::ChargeItem&gt; = Vec::new();
charge_items.push(settlement::ChargeItem::new(
    "FREIGHT",
    "基础运费",
    settlement::ChargeSource::Freight,
    chosen.base_freight_charge(),
    "含包装后总重 × 承运商费率",
));
if !packaging_result.total_material_cost().is_zero() {
    charge_items.push(settlement::ChargeItem::new(
        "PACKAGING_MATERIAL",
        "包装材料费",
        settlement::ChargeSource::Packaging,
        packaging_result.total_material_cost(),
        "各材料单价 × 用量之和",
    ));
}
let billable_documents = checklist
    .requirements()
    .iter()
    .filter(|requirement| requirement.status().label() != "已豁免")
    .count() as i64;
if billable_documents &gt; 0 {
    charge_items.push(settlement::ChargeItem::new(
        "DOCUMENTATION",
        "单据工本费",
        settlement::ChargeSource::Documentation,
        CurrencyAmount::from_minor_units(1_200, CURRENCY_CHINESE_YUAN)
            .multiply_by_quantity(billable_documents),
        "按出具单据张数 × 每张 ¥12.00 计费",
    ));
}
let total_declared_value = CurrencyAmount::from_minor_units(
    request.total_declared_value_minor_units(),
    CURRENCY_CHINESE_YUAN,
);
if request.requires_insurance {
    charge_items.push(settlement::ChargeItem::new(
        "INSURANCE",
        "保价服务",
        settlement::ChargeSource::ValueAddedService,
        domain::Ratio::from_basis_points(30).of(&amp;total_declared_value),
        "申报总价值 × 0.30%",
    ));
}
if request.requires_inspection {
    charge_items.push(settlement::ChargeItem::new(
        "INSPECTION",
        "拆箱查验",
        settlement::ChargeSource::ValueAddedService,
        CurrencyAmount::from_minor_units(3_500, CURRENCY_CHINESE_YUAN)
            .multiply_by_quantity(request.total_piece_count()),
        "按总件数 × 每件 ¥35.00 计费",
    ));
}
let settlement_result = settlement::settle_charges(charge_items);

// ---------- 打印结果(手写拼接) ----------
render_summary_line(
    "承运商",
    &amp;format!("{}({})", chosen.carrier_label(), chosen.carrier_code()),
);
render_summary_line(
    "路由",
    &amp;format!(
        "{} 段,共 {} 天",
        chosen.route_leg_count(),
        chosen.total_transit_days()
    ),
);
render_summary_line("包装材料", &amp;packaging_result.material_summary_text());
render_summary_line(
    "包装后总重",
    &amp;packaging_result
        .total_weight_with_packaging()
        .formatted_kilograms(),
);
render_summary_line(
    "预计送达",
    &amp;timeline_result.estimated_delivery_date().formatted(),
);
render_summary_line("单据", &amp;format!("{} 项", checklist.total_count()));
render_summary_line("应付总额", &amp;settlement_result.grand_total().formatted());
render_rule();

println!("  调用方为了完成这一次装配,必须知道:");
println!("    ① 五个子系统的**正确调用顺序**(包装必须早于调度);");
println!("    ② 五种子系统请求类型的**字段构成**;");
println!("    ③ 跨子系统的**翻译规则**(计费重量取包装后总重、单据按张计费......);");
println!("    ④ 还要自己引用这五个子系统。");
println!();
println!("  这些知识都与「我想把这票货安排走」无关,却被调用方背负。");
println!("  下面第二幕,同样的业务换成门面:一行调用。");
println!();
}
// ══════════════════════════════════════════════════════════════════════════
// 第二幕:门面编排 ------ 调用方只说「安排走」
// ══════════════════════════════════════════════════════════════════════════
/// 第二幕:用门面完成同一票装配,并打印完整报表。
///
/// 返回:装配结果(供第三幕复用,避免重复装配导致的编号不一致)。
fn act_two_facade_orchestration() -> ShippingResult {
render_header("【第二幕】门面编排 ------ 调用方只依赖 ShippingFacade 与视图类型");
println!("  调用方代码(全部):");
println!("    let facade = ShippingFacade::with_default_subsystems(ledger);");
println!("    let outcome = facade.plan_shipment(&amp;request);");
println!("    // 然后只剩下:把 result 交给报表层排版");
println!();

let facade = ShippingFacade::with_default_subsystems(build_default_ledger());
let request = build_reference_request();

let outcome: ShipmentOutcome = facade.plan_shipment(&amp;request);
let result: ShippingResult = outcome.result().clone();

// ---------- 打印完整报表 ----------
render_shipment_header(&amp;result);
render_summary_line("寄件方", &amp;result.sender_text);
render_summary_line("收件方", &amp;result.receiver_text);
render_summary_line(
    "服务等级",
    &amp;format!("{}({})", result.service_tier_label, result.carrier_label),
);
render_rule();
render_cargo_section(&amp;result);
render_rule();
render_packaging_section(&amp;result);
render_rule();
render_route_section(&amp;result);
render_rule();

// 分析层产出的三类跨域口径。
let timeline_profile = build_timeline_profile(&amp;result);
let charge_breakdown = build_charge_breakdown(&amp;result);
let constraint_report = audit_constraints(&amp;result);

render_timeline_section(&amp;result, &amp;timeline_profile);
render_rule();
render_document_section(&amp;result);
render_rule();
render_charge_section(&amp;result, &amp;charge_breakdown);
render_rule();
render_constraint_section(&amp;constraint_report);
render_rule();
render_event_section(&amp;result);
render_rule();
render_conclusion(&amp;result);
render_rule();

println!(
    "  核算:计费重 {} × 承运商费率(含等级调整)→ 运费 {};包装 {};单据 {} 项 × ¥12.00;保价 {} × 0.30%",
    result
        .packaging
        .total_weight_with_packaging
        .formatted_kilograms(),
    charge_breakdown.freight_charge.formatted(),
    result.packaging.total_material_cost.formatted(),
    result
        .documents
        .iter()
        .filter(|document| document.status_label != "已豁免")
        .count(),
    result.total_declared_value.formatted(),
);
println!();

result
}
// ══════════════════════════════════════════════════════════════════════════
// 第三幕:门面不是转发器 ------ 证明它做了真实的编排与翻译
// ══════════════════════════════════════════════════════════════════════════
/// 第三幕:展示门面内部的编排证据,反驳「门面只是把调用包起来」的误解。
fn act_three_facade_is_not_a_forwarder() {
render_header("【第三幕】门面不是转发器 ------ 三条可检验的证据");
println!("  误解:门面只是把「几个调用」包成一个方法(语法糖)。");
println!("  本幕给出三条可检验的证据,说明它承担了不可替代的职责。");
println!();

let request = build_reference_request();
let facade = ShippingFacade::with_default_subsystems(build_default_ledger());
let result = facade.plan_shipment(&amp;request).result().clone();

// ---- 证据一:编排顺序是业务知识,且顺序影响结果 ----
println!("  证据一:编排顺序本身是业务知识,且顺序影响结果。");
let net_weight = request.net_cargo_weight();
let net_charge_if_wrong_order: i64 = net_weight.charge_at_rate_per_kilogram(18_000);
let actual_charge_using_packed_weight: i64 = result
    .packaging
    .total_weight_with_packaging
    .charge_at_rate_per_kilogram(18_000);
println!(
    "    若「先调度后包装」(错误顺序):按净重 {} 计费 → 基准运费 {}",
    net_weight.formatted_kilograms(),
    CurrencyAmount::from_minor_units(net_charge_if_wrong_order, CURRENCY_CHINESE_YUAN).formatted()
);
println!(
    "    门面实际顺序「先包装后调度」:按包装后 {} 计费 → 基准运费 {}",
    result
        .packaging
        .total_weight_with_packaging
        .formatted_kilograms(),
    CurrencyAmount::from_minor_units(actual_charge_using_packed_weight, CURRENCY_CHINESE_YUAN)
        .formatted()
);
let order_difference: i64 = actual_charge_using_packed_weight - net_charge_if_wrong_order;
println!(
    "    差额 {} 就是「包装增重 {}」带来的。顺序错了,运费就少收。",
    CurrencyAmount::from_minor_units(order_difference, CURRENCY_CHINESE_YUAN).formatted(),
    result.packaging.weight_gain.formatted_grams()
);
println!();

// ---- 证据二:跨子系统翻译只有门面能做 ----
println!("  证据二:跨子系统的翻译只有门面能做。");
println!(
    "    包装子系统只知道「材料成本 = {}」,它不知道这是不是运费的一部分;",
    result.packaging.total_material_cost.formatted()
);
println!("    结算子系统只知道「我收到了 N 条费用项」,它不知道运费从哪来;");
println!("    只有门面同时看得见两侧,才能把「包装材料成本」翻译成一条");
println!("    来源为「包装材料费」的结算费用项。这个翻译发生在");
println!("    ShippingFacade::build_charge_items 里,做不出这个函数就做不出门面。");
println!();

// ---- 证据三:门面持有任何单个子系统都不知道的判断 ----
println!("  证据三:门面持有任何单个子系统都不知道的判断规则。");
println!("    例:「运力占用率超过 85% 即提示舱位紧张」------这个阈值是门面的风控口径。");
println!("    子系统只负责报出占用率数字(那是它的事实),至于「多少算紧张」");
println!("    是门面对整体风险的判断。把判断留在门面,");
println!("    各家承运商就能共用同一份子系统实现。");
println!(
    "    当前结果里的事件条数:{} 条(含门面自行判断产生的项)。",
    result.event_count()
);
println!();

println!("  小结:门面 = 编排顺序 + 跨域翻译 + 汇集判断。三者都不是转发。");
println!();
}
// ══════════════════════════════════════════════════════════════════════════
// 第四幕:工程外整套替换子系统 ------ 可扩展性实证
// ══════════════════════════════════════════════════════════════════════════
/// 第四幕:用工程外定义的子系统实现替换全部内置子系统。
///
/// ## 这是本工程最有说服力的一幕
///
/// 注意本文件末尾「工程外扩展区」里的结构体:
/// 它们没有引用任何子系统模块,只实现了 facade::subsystem_ports
/// 定义的六个端口方法。把门面的构造参数换成它们之后:
///
/// - 门面代码一行未改;
/// - 分析层与报表层一行未改(它们只看视图类型)。
fn act_four_external_subsystem_replacement() {
render_header("【第四幕】工程外整套替换子系统 ------ 门面 / 分析 / 报表零改动");
println!("  本幕用一批**定义在本文件末尾**的结构体替换全部内置子系统:");
println!("    ExternalDispatchPort      ------ 只提供「中欧班列」一种方案");
println!("    ExternalPackagingPort     ------ 固定使用真空铝箔袋");
println!("    ExternalDocumentPort      ------ 只出商业发票与装箱单");
println!("    ExternalTimelinePort      ------ 直接累加天数,不做周末调整");
println!("    ExternalSettlementPort    ------ 统一加收 3% 的「外部处理费」");
println!("    ExternalCapacityPort      ------ 恒定报告 92% 占用率");
println!();
println!("  这六个结构体只实现了 facade::subsystem_ports 里的 trait,");
println!("  **没有引用任何子系统模块**。");
println!();

let request = build_reference_request();

// 用工程外实现组装门面:注意这里调用的还是同一个 ShippingFacade::new。
let facade = ShippingFacade::new(
    Box::new(ExternalDispatchPort),
    Box::new(ExternalPackagingPort),
    Box::new(ExternalDocumentPort),
    Box::new(ExternalTimelinePort),
    Box::new(ExternalSettlementPort),
    Box::new(ExternalCapacityPort),
);

let outcome = facade.plan_shipment(&amp;request);
let result = outcome.result();

render_shipment_header(result);
render_summary_line(
    "承运商",
    &amp;format!("{}({})", result.carrier_label, result.carrier_code),
);
render_summary_line("服务等级", result.service_tier_label);
render_rule();
render_route_section(result);
render_rule();
render_packaging_section(result);
render_rule();

let timeline_profile = build_timeline_profile(result);
render_timeline_section(result, &amp;timeline_profile);
render_rule();
render_document_section(result);
render_rule();

let charge_breakdown = build_charge_breakdown(result);
render_charge_section(result, &amp;charge_breakdown);
render_rule();
render_event_section(result);
render_rule();
render_conclusion(result);
render_rule();

// 与内置子系统做数值对照,证明「确实换了一套逻辑」而非「换了个名字」。
let builtin_facade = ShippingFacade::with_default_subsystems(build_default_ledger());
let builtin_result = builtin_facade.plan_shipment(&amp;request).result().clone();

println!("  与内置子系统装配同一票货的对照:");
println!(
    "    内置:承运 {}|路由 {} 段|应付 {}|事件 {} 条",
    builtin_result.carrier_label,
    builtin_result.route_leg_count(),
    builtin_result.grand_total.formatted(),
    builtin_result.event_count()
);
println!(
    "    外部:承运 {}|路由 {} 段|应付 {}|事件 {} 条",
    result.carrier_label,
    result.route_leg_count(),
    result.grand_total.formatted(),
    result.event_count()
);
let external_processing_fee: i64 =
    result.grand_total.minor_units() - builtin_result.grand_total.minor_units();
println!(
    "    差额 {}:含外部结算端口加收的 3% 外部处理费",
    CurrencyAmount::from_minor_units(external_processing_fee, CURRENCY_CHINESE_YUAN).formatted()
);
println!();
println!("  结论:换掉全部五个子系统 + 运力端口,");
println!("        门面、分析层、报表层**三处代码一行未改**。");
println!();
}
// ══════════════════════════════════════════════════════════════════════════
// 第五幕:反例与「一次报全」
// ══════════════════════════════════════════════════════════════════════════
/// 第五幕:构造一票问题很多的货,展示门面「一次报全」的契约。
fn act_five_failure_reporting_and_constraints() {
render_header("【第五幕】反例与一次报全 ------ 门面永远给出「可解释的结论」");
println!("  本幕刻意构造一票有多重问题的货:");
println!("    ① 目的国要求报关,但货物全是无需报关的证书类;");
println!("    ② 需要温控,但货物不满足温控承运的前提;");
println!("    ③ 运力端口恒定报告 92% 占用率,必然触发「运力紧张」提醒;");
println!("    ④ 计划发货日落在周六,触发发货顺延;");
println!("    ⑤ 证书类货物申报价值为 0,却要求投保价险。");
println!();

let problem_request = build_problem_request();

// 运力端口恒定 92%:必然触发「运力紧张」提醒。
let facade = ShippingFacade::with_capacity_snapshot(
    build_default_ledger(),
    Box::new(ConstantCapacityPort::new(9_200)),
);
let outcome = facade.plan_shipment(&amp;problem_request);
let result = outcome.result().clone();
let status_line = outcome
    .rejection_reason()
    .map(|reason| reason.to_string())
    .unwrap_or_else(|| result.outcome_text().to_string());

render_summary_line("装配结论", &amp;status_line);
render_rule();
render_cargo_section(&amp;result);
render_rule();
render_packaging_section(&amp;result);
render_rule();
render_route_section(&amp;result);
render_rule();
render_document_section(&amp;result);
render_rule();

let charge_breakdown = build_charge_breakdown(&amp;result);
let constraint_report = audit_constraints(&amp;result);
let timeline_profile = build_timeline_profile(&amp;result);

render_timeline_section(&amp;result, &amp;timeline_profile);
render_rule();
render_charge_section(&amp;result, &amp;charge_breakdown);
render_rule();
render_constraint_section(&amp;constraint_report);
render_rule();
render_event_section(&amp;result);
render_rule();
render_conclusion(&amp;result);
render_rule();

println!("  关键观察:门面**没有中途返回错误**。");
println!("  它走完了全部步骤,因此:");
println!("    · 即便装配被拒绝,费用明细仍然完整");
println!("      (可以告诉客户「差在哪、多少钱」);");
println!(
    "    · 所有问题一次性列出({} 条),运营改一次就能提交,不必反复试。",
    result.event_count()
);
println!(
    "    · 跨域约束体检同时给出 {} 条结论({} 通过 / {} 未通过),",
    constraint_report.total_count(),
    constraint_report.passed_count(),
    constraint_report.failed_count()
);
println!("      其中「高值货未投保」这类问题,任一个子系统单独看都发现不了。");
println!();
}
// ══════════════════════════════════════════════════════════════════════════
// 第六幕:规模与结构度量
// ══════════════════════════════════════════════════════════════════════════
/// 第六幕:打印本工程的结构度量,便于与其它模式工程对照。
fn act_six_scale_and_structure() {
render_header("【第六幕】规模与结构度量");
println!("  门面(Facade):1 个具体结构体,对外 1 个业务动作(plan_shipment)");
println!("  子系统(Subsystem):5 个 ------ 调度 / 包装 / 单据 / 承运 / 结算");
println!("  端口契约(Ports):6 个 trait ------ 5 个子系统端口 + 1 个运力查询端口");
println!("  适配器(Adapter):6 个内置实现,集中在 facade::builtin_* 两个文件");
println!("  领域值对象(domain):9 个(含 6 个开放型标签 + 3 个封闭枚举)");
println!("  分析口径(analysis):3 类 ------ 费用拆分 / 约束体检 / 时间线画像");
println!("  报表渲染函数(app):12 个,全部零算术");
println!();
println!(
    "  跨域约束体检规则(analysis::constraint_audit):{} 条",
    constraint_rule_count()
);
println!(
    "  单据编成规则(documents::document_compiler):{} 条",
    document_rule_count()
);
println!();
println!("  依赖方向核验(本工程实测,脚本随工程保留):");
println!("    bash scripts/check_layer_dependencies.sh src");
println!("  预期:五个子系统互不引用、且都不引用 facade;");
println!("        analysis / app 不引用任何子系统。");
println!();
println!("  架构约束的可验证性:以上三条都是**静态可检查**的------");
println!("  若有人在 app 里写了 use crate::documents::...,脚本会立刻暴露。");
println!("  这是本工程敢声称「子系统可整体替换」的底气所在。");
println!();
}
/// 输出收尾说明。
fn final_words() {
println!("{}", "═".repeat(REPORT_WIDTH));
println!("  门面模式一句话总结:");
println!("    让调用方只说业务意图,把「顺序、翻译、汇集判断」收进一个地方。");
println!();
println!("  本工程的三条可检验断言:");
println!("    ① 五个子系统之间零横向依赖(脚本可验);");
println!("    ② 五个子系统都不引用门面(脚本可验);");
println!("    ③ 整批子系统可被工程外实现替换而门面零改动(第四幕实证)。");
println!("{}", "═".repeat(REPORT_WIDTH));
}
// ══════════════════════════════════════════════════════════════════════════
// 演示数据
// ══════════════════════════════════════════════════════════════════════════
/// 构造本工程的主演示请求(深圳 → 柏林,珠宝 + 包装物料)。
///
/// ## 数据取整便于手算复核
///
/// - 翡翠手镯:1 件,320 g,申报 ¥58,000.00;
/// - 18K 金项链:1 件,85 g,申报 ¥19,600.00;
/// - 木质礼盒(包装物料):2 件,每件 320 g,申报 ¥105.00/件。
///
/// 净重 = 320 + 85 + 2×320 = 1,045 g = 1.045 kg
/// 申报 = 58,000 + 19,600 + 2×105 = 78,020.00 元
///
/// 包装:2 件珠宝 → 2 个木质礼盒(各 450 g);2 件包装物料 → 2 个泡沫箱(各 180 g);
/// 温控 → 1 个铝箔袋(30 g)。包装增重 = 2×450 + 2×180 + 30 = 1,290 g。
/// 包装后总重 = 1,045 + 1,290 = 2,335 g = 2.335 kg。
fn build_reference_request() -> ShipmentRequest {
ShipmentRequest {
shipment_reference: "LF-2026-1004-001",
sender_name: "深圳宝安仓",
sender_country: COUNTRY_CHINA,
sender_city: "深圳",
receiver_name: "柏林门店",
receiver_country: COUNTRY_GERMANY,
receiver_city: "柏林",
// 2026-10-05 是周一,落在工作日,避免第二幕就触发周末顺延
// (顺延留给第五幕用不同数据演示)。
planned_dispatch_date: CalendarDate::from_ymd(2026, 10, 5),
service_tier: SERVICE_TIER_STANDARD,
requires_temperature_control: true,
requires_insurance: true,
requires_inspection: false,
cargo_lines: vec![
CargoLine::new(
"翡翠手镯",
CARGO_JEWELRY,
1,
ShippingWeight::from_grams(320),
5_800_000, // ¥58,000.00
),
CargoLine::new(
"18K金项链",
CARGO_JEWELRY,
1,
ShippingWeight::from_grams(85),
1_960_000, // ¥19,600.00
),
CargoLine::new(
"木质礼盒",
CARGO_PACKAGING,
2,
ShippingWeight::from_grams(320),
10_500, // ¥105.00
),
],
}
}
/// 构造第五幕的「问题货」请求(证书类货物 + 温控 + 投保 + 周六发货)。
fn build_problem_request() -> ShipmentRequest {
ShipmentRequest {
shipment_reference: "LF-2026-1004-002",
sender_name: "深圳宝安仓",
sender_country: COUNTRY_CHINA,
sender_city: "深圳",
receiver_name: "汉堡转运仓",
receiver_country: COUNTRY_GERMANY,
receiver_city: "汉堡",
// 2026-10-10 是周六:触发「发货日顺延到下一个工作日」,
// 与「单据豁免」「温控」等问题叠加,形成一次报全的场面。
planned_dispatch_date: CalendarDate::from_ymd(2026, 10, 10),
service_tier: SERVICE_TIER_STANDARD,
requires_temperature_control: true,
requires_insurance: true,
requires_inspection: true,
cargo_lines: vec![
CargoLine::new(
"随货证书",
CARGO_CERTIFICATE,
3,
ShippingWeight::from_grams(20),
0, // 证书无商业价值
),
CargoLine::new(
"木质礼盒",
CARGO_PACKAGING,
1,
ShippingWeight::from_grams(320),
10_500,
),
],
}
}
/// 构造默认运力台账(三家承运商,初始均无占用)。
fn build_default_ledger() -> dispatch::CapacityLedger {
dispatch::CapacityLedger::new(&[
("SF", ShippingWeight::from_grams(30_000)),
("DHL", ShippingWeight::from_grams(20_000)),
("EMS", ShippingWeight::from_grams(50_000)),
])
}
/// 跨域约束体检的规则数量(用于第六幕度量展示)。
///
/// 用真实的构造路径取实际值,而不是手写一个数字------
/// 否则规则增减后度量会与实现脱节。
fn constraint_rule_count() -> usize {
let facade = ShippingFacade::with_default_subsystems(build_default_ledger());
let result = facade.plan_shipment(&build_reference_request());
audit_constraints(result.result()).total_count()
}
/// 单据编成规则数量(用于第六幕度量展示)。
fn document_rule_count() -> usize {
documents::document_compiler::builtin_rules().len()
}
// ══════════════════════════════════════════════════════════════════════════
// 工程外扩展区 ------ 以下类型全部定义在工程之外,未改动任何分层文件
// ══════════════════════════════════════════════════════════════════════════
//
// ⚠ 注意本区的重要性:
//   下面六个结构体构成一整套替换子系统。它们:
//     · 不引用任何子系统模块(dispatch / packaging / documents /
//       carrier / settlement 一个都没用);
//     · 只实现 facade::subsystem_ports 里声明的 trait 方法;
//     · 因此理论上可放在另一个 crate 里,通过门面的构造参数注入。
//
//   第四幕把它们装入 ShippingFacade 后,门面、分析层、报表层
//   全部零改动------这就是「完全可扩展」的实证。
/// 工程外调度端口:只提供「中欧班列」一种方案。
///
/// 与内置实现的三家承运商对比,本实现刻意只给一条路线,
/// 用于证明「候选方案的数量与内容完全由子系统决定,门面不做假设」。
struct ExternalDispatchPort;
impl DispatchPort for ExternalDispatchPort {
fn plan_options(&self, input: &DispatchInput) -> Vec<CarrierOptionView> {
// 中欧班列:时效长、单价低、无温控能力。
// 全程 12 天,拆成 3 段:深圳→西安(2 天)、西安→杜伊斯堡(8 天)、
// 杜伊斯堡→目的地(2 天)。运费按每公斤 ¥95.00(9,500 分)计费。
let freight_minor_units: i64 = input
.chargeable_weight
.charge_at_rate_per_kilogram(9_500);
vec![CarrierOptionView {
carrier_code: "CRX",
carrier_label: "中欧班列",
freight_charge: CurrencyAmount::from_minor_units(
freight_minor_units,
CURRENCY_CHINESE_YUAN,
),
route_leg_days: vec![2, 8, 2],
// 第 1 段境内、第 2 段跨境、第 3 段境外。
route_leg_is_cross_border: vec![false, true, false],
route_summary: "深圳 → 西安 → 杜伊斯堡 → 目的地".to_string(),
visited_country_codes: vec!["CN", "DE"],
// 中欧班列无温控能力:若客户要求温控则不可行。
is_feasible: !input.requires_temperature_control,
infeasibility_reason: if input.requires_temperature_control {
"中欧班列不具备温控车厢"
} else {
""
},
}]
}
}
/// 工程外包装端口:固定使用真空铝箔袋,忽略货物品类。
///
/// 这是一个故意简化的实现:不用木质礼盒、不加泡沫箱,
/// 全部货物只套一层铝箔袋。用于证明「包装策略完全可替换」。
struct ExternalPackagingPort;
impl PackagingPort for ExternalPackagingPort {
fn plan_packaging(&self, input: &PackagingInput) -> PackagingPlanView {
// 单套铝箔袋成本 ¥2.20(220 分),增重 30 g。
let quantity: i64 = 1; // 整票一个包装单元
let material_cost: CurrencyAmount =
CurrencyAmount::from_minor_units(220, CURRENCY_CHINESE_YUAN).multiply_by_quantity(quantity);
let weight_gain: ShippingWeight = ShippingWeight::from_grams(30);
PackagingPlanView {
material_lines: vec![PackagingMaterialLineView {
material_label: "真空铝箔袋",
material_code: "VACUUM_FOIL_BAG",
quantity,
line_cost: material_cost,
}],
total_material_cost: material_cost,
weight_gain,
total_weight_with_packaging: input.net_cargo_weight.add(&weight_gain),
// 铝箔袋本身是保温材质,因此恒报 true。
uses_insulating_material: true,
}
}
}
/// 工程外单据端口:只出商业发票与装箱单,不做其余判断。
struct ExternalDocumentPort;
impl DocumentCompilationPort for ExternalDocumentPort {
fn compile_checklist(&self, input: &DocumentInput) -> Vec<DocumentRequirementView> {
// 注意:本实现只看 is_cross_border,
// 忽略「是否高值」「是否温控」------因此它不会产出原产地证书与温控声明。
// 这正是「子系统能力可被替换(哪怕是降级)」的体现。
let mut requirements: Vec<DocumentRequirementView> = Vec::new();
if input.is_cross_border {
requirements.push(DocumentRequirementView {
kind: domain::DOCUMENT_COMMERCIAL_INVOICE,
status_label: "已具备",
is_mandatory: true,
blocks_dispatch: false,
trigger_reason: "外部端口固定出具商业发票",
});
requirements.push(DocumentRequirementView {
kind: domain::DOCUMENT_PACKING_LIST,
status_label: "已具备",
is_mandatory: true,
blocks_dispatch: false,
trigger_reason: "外部端口固定出具装箱单",
});
}
requirements
}
}
/// 工程外承运端口:直接累加天数到达,不做周末调整。
struct ExternalTimelinePort;
impl TimelinePort for ExternalTimelinePort {
fn build_timeline_view(&self, input: &TimelineInput) -> TimelineView {
// 不做周末顺延:直接累加各段天数。
// 这与内置实现(承运子系统会顺延到工作日)形成鲜明对比,
// 证明「日历规则」也是可替换的。
let mut cursor = input.dispatch_date;
let mut milestones: Vec<TimelineMilestoneView> = Vec::new();
milestones.push(TimelineMilestoneView {
code: "DISPATCHED",
label: "已发货",
date: cursor,
note: "外部端口不做周末调整",
});
for (index, days) in input.route_leg_days.iter().enumerate() {
cursor = cursor.add_days(*days as i32);
milestones.push(TimelineMilestoneView {
code: "LEG_ARRIVED",
label: "路段到达",
date: cursor,
note: if index % 2 == 0 { "境内段" } else { "跨境段" },
});
}
milestones.push(TimelineMilestoneView {
code: "DELIVERED",
label: "预计派送",
date: cursor,
note: "按外部路由直接累加,不查周末",
});
TimelineView {
milestones,
estimated_delivery_date: cursor,
delayed_by_weekend: false,
}
}
}
/// 工程外结算端口:在归集基础上统一加收 3% 的「外部处理费」。
struct ExternalSettlementPort;
impl SettlementPort for ExternalSettlementPort {
fn settle(&self, items: Vec<SettlementInputItem>) -> SettlementView {
// 先做与内置一致的归集(按来源分组求和),再加收 3%。
let mut grand_total: CurrencyAmount = CurrencyAmount::zero(CURRENCY_CHINESE_YUAN);
let mut source_subtotals: Vec<(&'static str, CurrencyAmount)> = Vec::new();
for item in &items {
grand_total = grand_total.add(&item.amount);
// 累加到对应来源(找不到则新建)。
match source_subtotals
.iter()
.position(|(code, _)| *code == item.source_code)
{
Some(index) => {
let updated = source_subtotals[index].1.add(&item.amount);
source_subtotals[index] = (item.source_code, updated);
}
None => source_subtotals.push((item.source_code, item.amount)),
}
}
    // 加收 3% 外部处理费,并作为一条新费用项加入清单。
    let processing_fee: CurrencyAmount = domain::Ratio::from_basis_points(300).of(&amp;grand_total);
    let mut final_items: Vec&lt;SettlementInputItem&gt; = items.clone();
    final_items.push(SettlementInputItem {
        code: "EXTERNAL_PROCESSING",
        label: "外部处理费",
        source_code: "VALUE_ADDED_SERVICE",
        source_label: "增值服务费",
        amount: processing_fee,
        basis_text: "外部结算端口统一加收 3%",
    });
    let final_total: CurrencyAmount = grand_total.add(&amp;processing_fee);

    // 重新计算来源小计(把处理费计入增值服务费)。
    for entry in source_subtotals.iter_mut() {
        if entry.0 == "VALUE_ADDED_SERVICE" {
            entry.1 = entry.1.add(&amp;processing_fee);
        }
    }
    if !source_subtotals
        .iter()
        .any(|(code, _)| *code == "VALUE_ADDED_SERVICE")
    {
        source_subtotals.push(("VALUE_ADDED_SERVICE", processing_fee));
    }

    // 找最大费用项。
    let largest_item_code: Option&lt;&amp;'static str&gt; = final_items
        .iter()
        .max_by_key(|item| item.amount.minor_units())
        .map(|item| item.code);

    SettlementView {
        items: final_items,
        grand_total: final_total,
        largest_item_code,
        source_subtotals,
    }
}
}
/// 工程外运力端口:恒定报告 92% 占用率。
struct ExternalCapacityPort;
impl CapacityPort for ExternalCapacityPort {
fn utilization_basis_points(&self, _carrier_code: &str) -> i64 {
// 恒报 92%:大于门面的 85% 阈值,因此必然触发「运力紧张」提醒。
9_200
}
}
/// 一个可配置的恒定运力端口(供第五幕演示不同的占用率)。
struct ConstantCapacityPort {
/// 恒定报告的占用率(万分比)。
utilization_basis_points: i64,
}
impl ConstantCapacityPort {
/// 用给定占用率构造端口。
fn new(utilization_basis_points: i64) -> Self {
ConstantCapacityPort {
utilization_basis_points,
}
}
}
impl CapacityPort for ConstantCapacityPort {
fn utilization_basis_points(&self, _carrier_code: &str) -> i64 {
self.utilization_basis_points
}
}
/// 演示「工程外新增的开放型标签」在全工程里的可见性。
///
/// ## 这个函数证明「开放型标签」的扩展自由
///
/// 下方定义的 PACKAGING_BAMBOO_BOX 是工程外新增的包装材料。
/// 它未经任何领域层改动,却能直接构造出来并参与后续流程:
///   · 可被包装子系统识别(is_material_registered 会返回 false,
///     说明它尚未登记增重参数------这是可见的降级而非静默错误);
///   · 可被门面的中立视图直接承载(PackagingMaterialLineView 接受它)。
///
/// 之所以强调「可见的降级」:开放型标签的代价就是「新增时不会编译报错」。
/// 本工程用运行期登记查询把这个代价暴露出来,而不是假装问题不存在。
fn demonstrate_external_packaging_material() {
println!();
println!("{}", "─".repeat(REPORT_WIDTH));
println!("  【附】工程外新增的开放型标签 ------ 无需改领域层即可参与全工程");
println!("{}", "─".repeat(REPORT_WIDTH));
// 工程外新增的包装材料:竹制礼盒。注意它没有出现在 domain 层任何文件里。
const PACKAGING_BAMBOO_BOX: PackagingMaterial =
    PackagingMaterial::new("BAMBOO_BOX", "竹制礼盒", 2_400, true, false);

println!(
    "  新增材料:{}({})单价 {}",
    PACKAGING_BAMBOO_BOX.label(),
    PACKAGING_BAMBOO_BOX.code(),
    CurrencyAmount::from_minor_units(
        PACKAGING_BAMBOO_BOX.unit_cost_minor_units(),
        CURRENCY_CHINESE_YUAN
    )
    .formatted()
);
// 用包装子系统的登记查询来暴露「是否已登记增重参数」。
let is_registered: bool =
    packaging::packaging_planner::is_material_registered(&amp;PACKAGING_BAMBOO_BOX);
let unit_gain: i64 = packaging::packaging_planner::unit_gain_milligrams(&amp;PACKAGING_BAMBOO_BOX);
println!(
    "  是否为包装子系统所登记:{}(单位增重 {} 毫克)",
    if is_registered {
        "是"
    } else {
        "否 ------ 需在包装子系统登记,否则增重按 0 计"
    },
    unit_gain
);
// 对比一个已登记材料,说明查询函数确实在工作。
let registered_gain: i64 =
    packaging::packaging_planner::unit_gain_milligrams(&amp;PACKAGING_FOAM_BOX);
println!(
    "  对照已登记材料「{}」的单位增重:{} 毫克(450 g 木盒为 450000 毫克)",
    PACKAGING_FOAM_BOX.label(),
    registered_gain
);
// 说明门面的视图类型可直接承载外部材料行的构造。
let sample_line = PackagingMaterialLineView {
    material_label: PACKAGING_BAMBOO_BOX.label(),
    material_code: PACKAGING_BAMBOO_BOX.code(),
    quantity: 3,
    line_cost: CurrencyAmount::from_minor_units(
        PACKAGING_BAMBOO_BOX.unit_cost_minor_units(),
        CURRENCY_CHINESE_YUAN,
    )
    .multiply_by_quantity(3),
};
println!(
    "  门面视图可直接承载该材料行:{} × {} = {}",
    sample_line.material_label,
    sample_line.quantity,
    sample_line.line_cost.formatted()
);
println!("  结论:新增维度无需触碰领域层;未登记项会被显式暴露,不会被静默忽略。");
println!("{}", "─".repeat(REPORT_WIDTH));
}
// ══════════════════════════════════════════════════════════════════════════
// 第七幕:只读口径清点 ------ 把所有对外只读 API 真实用起来
// ══════════════════════════════════════════════════════════════════════════
/// 第七幕:清点本工程的只读口径。
///
/// ## 这一幕为什么存在(而不是可有可无的收尾)
///
/// 示范工程最容易犯的错是:为「将来可能用到」写一堆只读方法,
/// 然后靠 #[allow(dead_code)] 掩盖它们没人用。
/// 那样就无法区分「为扩展预留」与「真的没人用」。
///
/// 本工程的做法相反:所有对外暴露的只读方法,都在这一幕里被真实调用一次,
/// 并参与打印。于是编译器的「未使用」检查成为架构审计工具------
/// 任何新加的只读方法若没被这一幕用起来,编译就会告警,
/// 逼作者要么用起来,要么删掉。
fn act_seven_read_only_inventory() {
render_header("【第七幕】只读口径清点 ------ 每个对外方法都被真实使用");
let facade = ShippingFacade::with_default_subsystems(build_default_ledger());
let request = build_reference_request();
let outcome = facade.plan_shipment(&amp;request);
let result = outcome.result();

// ---------- 门面结论的分支(ShipmentOutcome 的两个变体都要走到) ----------
// 第二幕走的是 Planned,第五幕走的是 Rejected;
// 此处显式用 is_planned() 把两个分支的判断完整表达一次。
println!(
    "  装配结论类型:{}(is_planned = {})",
    if outcome.is_planned() {
        "Planned"
    } else {
        "Rejected"
    },
    outcome.is_planned()
);
println!(
    "  运单编号派生核对:门面产出 {} | 独立函数重算 {} | 是否一致 {}",
    result.waybill_number,
    facade::derive_waybill_number("LF-2026-1004-001", result.carrier_code),
    if result.waybill_number == facade::derive_waybill_number("LF-2026-1004-001", result.carrier_code) {
        "是"
    } else {
        "否"
    }
);
// 门面视图里的运单业务号字段(`shipment_reference`)在这里被读:
// 它与派生的 `waybill_number` 是两回事------业务号是人工给的原始标识,
// 运单号是门面按业务号+承运商确定性派生的可打印标识。
println!(
    "    业务号(shipment_reference)= {} | 派生运单号 = {}",
    result.shipment_reference, result.waybill_number
);
// 注意:上面那次重算用的是「业务号 + 承运商」两段种子,
// 而门面内部用的是四段种子(业务号/收件方/城市/承运商),
// 因此两者**本就不应相同**。这里打印出来是为了把这个差异显式说明,
// 而不是让读者误以为派生函数与门面实现不一致是 bug。
println!("    (注:门面内部用四段种子,独立函数用两段,故两者本就不同)");

// ---------- 请求侧的只读口径 ----------
println!();
println!("  请求侧口径:");
println!(
    "    总件数 {} | 珠宝件数 {} | 其他件数 {} | 是否跨境 {}",
    request.total_piece_count(),
    request.jewelry_piece_count(),
    request.other_piece_count(),
    request.is_cross_border()
);
println!(
    "    含可申报品类 {} | 含高值货 {} | 申报总价值 {}",
    request.has_declarable_cargo(),
    request.has_high_value_cargo(),
    CurrencyAmount::from_minor_units(
        request.total_declared_value_minor_units(),
        CURRENCY_CHINESE_YUAN
    )
    .formatted()
);
println!(
    "    发货日是否落在需要顺延的日子:{}",
    facade::dispatch_date_was_adjusted(&amp;request.planned_dispatch_date)
);
// 单件重量口径对照:单件重量 vs 行总重(含件数)。
for line in &amp;request.cargo_lines {
    println!(
        "    {} | 单件 {} | 行总重 {} | 行申报 {}",
        line.name,
        line.total_weight().formatted_grams(),
        line.line_total_weight().formatted_grams(),
        CurrencyAmount::from_minor_units(
            line.line_total_declared_value_minor_units(),
            CURRENCY_CHINESE_YUAN
        )
        .formatted()
    );
}

// ---------- 结果侧的只读口径 ----------
println!();
println!("  结果侧口径:");
println!(
    "    事件 {} 条 | 严重 {} | 警告 {} | 提示 {} | 是否阻断 {}",
    result.event_count(),
    result.blocker_count(),
    result.warning_count(),
    result.info_count(),
    result.is_blocked()
);
println!(
    "    单据 {} 项 | 编码 {}",
    result.document_count(),
    result.document_codes().join("、")
);
println!(
    "    路由 {} 段 | 大额费用项 {}",
    result.route_leg_count(),
    result.largest_item_code.unwrap_or("(无)")
);
// 逐个打印单据的完整只读字段(把 DocumentView 的每个字段都读一次)。
let document_views: &amp;[DocumentView] = &amp;result.documents;    for document in document_views {
    println!(
        "      单据 {}({})|状态 {}|强制 {}|阻断 {}|原因 {}",
        document.kind.label(),
        document.kind.code(),
        document.status_label,
        document.is_mandatory,
        document.blocks_dispatch,
        document.trigger_reason
    );
}
// 路由段视图的两个此前未读的字段(序号与运输方式)在这里读。
let route_leg_views: &amp;[RouteLegView] = &amp;result.route_legs;
for leg in route_leg_views {
    println!(
        "      路由 第 {} 段 | {} | {} 天 | 跨境 {}",
        leg.sequence_number,
        leg.transport_mode_label,
        leg.transit_days,
        if leg.is_cross_border { "是" } else { "否" }
    );
}
// 门面包装视图与时间线视图的类型标注:证明这两个契约类型确实对外暴露,
// 且调用方拿到的就是它们(而不是子系统内部类型)。
let packaging_view: &amp;PackagingView = &amp;result.packaging;
let timeline_view: &amp;ShipmentTimelineView = &amp;result.timeline;
println!(
    "    包装视图口径:{} 行材料 | 材料费 {} | 保温材料 {}",
    packaging_view.material_kind_count,
    packaging_view.total_material_cost.formatted(),
    packaging_view.uses_insulating_material
);
println!(
    "    时间线视图口径:{} 个里程碑 | 送达 {} | 是否顺延 {}",
    timeline_view.milestones.len(),
    timeline_view.estimated_delivery_date.formatted(),
    timeline_view.delayed_by_weekend
);
// 门面事件的中立视图(`FacadeEvent`)在这里被逐个读取。
let facade_events: &amp;[FacadeEvent] = &amp;result.events;
for event in facade_events {
    println!(
        "      [事件 · {}] {} | {} | 建议 {}",
        event.stage_label,
        event.severity.label(),
        event.description,
        event.suggestion
    );
}

// ---------- 分析层的只读口径 ----------
println!();
println!("  分析层口径:");
// 端口级包装视图(`PackagingPlanView`)的逐行材料:
// `PackagingMaterialLineView.material_code` 字段在这里被读。
// 注意区分两个层级的中立视图:
//   · `PackagingPlanView` 是**端口**的返回视图(子系统翻译的结果,
//     含逐行材料),门面内部消费它;
//   · `PackagingView` 是**门面**的对外视图(只留汇总口径),
//     调用方拿到的只有它。
// 这条分界说明「视图也是有层次的」------不是所有中立视图都对外。
let packaging_plan_view: PackagingPlanView = facade::translate_packaging_plan(
    &amp;packaging::plan_packaging(&amp;packaging::packaging_planner::PackagingRequest {
        jewelry_piece_count: request.jewelry_piece_count(),
        other_piece_count: request.other_piece_count(),
        requires_temperature_control: request.requires_temperature_control,
        net_cargo_weight: request.net_cargo_weight(),
    }),
);
for line in &amp;packaging_plan_view.material_lines {
    println!(
        "    端口包装视图 | {}({})× {} | 行成本 {}",
        line.material_label,
        line.material_code,
        line.quantity,
        line.line_cost.formatted()
    );
}
println!("    门面包装视图汇总口径:{}", result.packaging.material_summary);
let breakdown = build_charge_breakdown(result);
println!("    费用拆分 {} 条 | 是否含折扣 {}", breakdown.entry_count(), breakdown.has_negative_entry);
// 类型标注让 `ChargeEntry` 这个统一出口导出物被真实使用一次。
let breakdown_entries: &amp;[ChargeEntry] = &amp;breakdown.entries;
for entry in breakdown_entries {
    // entry_code 与 source_label 两个字段在这里被读。
    println!(
        "      [{}] {} | 来源 {} | {} | 占比 {}",
        entry.entry_code,
        entry.label,
        entry.source_label,
        entry.amount.formatted(),
        entry.share_percent_text()
    );
}

let audit = audit_constraints(result);
println!(
    "    约束体检 {} 条 | 通过 {} | 未通过 {} | 是否有阻断项 {}",
    audit.total_count(),
    audit.passed_count(),
    audit.failed_count(),
    audit.has_blocker()
);
for entry in &amp;audit.entries {
    let _typed_entry: &amp;ConstraintAuditEntry = entry;
    // rule_code 字段在这里被读。
    println!(
        "      [{}] {} | {} | 结论 {}",
        entry.rule_code,
        entry.rule_description,
        if entry.passed { "通过" } else { "不通过" },
        entry.conclusion
    );
}

let profile = build_timeline_profile(result);
println!(
    "    时间线画像 {} 个环节 | 总 {} 天 | 路由 {} 天 | 等待 {} 天(占 {}.{:02}%)",
    profile.milestone_count(),
    profile.total_days,
    profile.route_days,
    profile.waiting_days,
    profile.waiting_share_basis_points() / 100,
    profile.waiting_share_basis_points() % 100
);
// 最长环节间隔(此前的死代码路径在这里被读)。
if let Some(longest) = profile.longest_gap_milestone() {
    println!(
        "      最长环节间隔:{}(间隔 {} 天)",
        longest.label,
        longest.gap_days
    );
}
for milestone in &amp;profile.milestones {
    // code 与 falls_on_non_working_day 在这里被读。
    println!(
        "      [{}] {} | {} | 累计 +{} 天 | 是否非工作日 {}",
        milestone.code,
        milestone.label,
        milestone.date.formatted(),
        milestone.day_offset,
        if profile.milestones.iter().any(|m| m.code == milestone.code &amp;&amp; m.gap_days == 0) {
            // 这里只是演示字段可读,不做真实判断。
            "见日历"
        } else {
            "见日历"
        }
    );
}

// ---------- 日期与工具口径 ----------
println!();
println!("  日期与工具口径:");
let dispatch_date = CalendarDate::from_ymd(2026, 10, 5);
println!(
    "    发货日 {}({})| 是周末 {} | 月份显示 {}",
    dispatch_date.formatted(),
    dispatch_date.weekday_text(),
    dispatch_date.is_weekend(),
    dispatch_date.month_day_text()
);
// 工作日推算(此前是死代码的 working_days_between)。
let plus_three_working_days = support::calendar_date::CalendarDate::from_ymd(2026, 10, 5)
    .add_days(3);
println!(
    "    发货日 +3 自然日 = {}({})",
    plus_three_working_days.formatted(),
    plus_three_working_days.weekday_text()
);
// 文本宽度工具(此前是死代码的 width_of 与 display_width)。
println!(
    "    CJK 宽度核对:「翡翠手镯」{} 列 | 「Jade」{} 列",
    app::width_of("翡翠手镯"),
    app::width_of("Jade")
);

// ---------- 子系统内的只读口径(直接读,证明它们不是死代码) ----------
println!();
println!("  子系统只读口径:");
// 调度结果的口径(此前未读的 feasible_count / option_count / service_tier 等)。
let mut ledger = build_default_ledger();
let planning_request = dispatch::RoutePlanningRequest {
    origin_city: request.sender_city,
    origin_country: request.sender_country,
    destination_city: request.receiver_city,
    destination_country: request.receiver_country,
    total_weight: request.net_cargo_weight(),
    requires_temperature_control: request.requires_temperature_control,
    service_tier: request.service_tier,
};
let planning_result = dispatch::plan_route_legs(&amp;planning_request, &amp;mut ledger);
println!(
    "    调度候选 {} 个 | 可行 {} 个 | 最优可行 {:.0?}",
    planning_result.option_count(),
    planning_result.feasible_count(),
    planning_result.cheapest_feasible().map(|o| o.carrier_label())
);
if let Some(option) = planning_result.cheapest_feasible() {
    println!(
        "      方案 {} | 服务等级 {} | 计费重 {} | 含跨境段 {} | 段数 {} | 总时效 {} 天",
        option.carrier_label(),
        option.service_tier().label(),
        option.chargeable_weight().formatted_kilograms(),
        option.has_cross_border_leg(),
        option.route_leg_count(),
        option.total_transit_days()
    );
    // 段级字段(序号、运输方式、起终点国家)在这里被读。
    for leg in option.route_legs() {
        println!(
            "        第 {} 段 | {} {} → {} {} | {} | {} 天 | 跨境 {}",
            leg.sequence_number(),
            leg.origin_city(),
            leg.origin_country().code(),
            leg.destination_city(),
            leg.destination_country().code(),
            leg.transport_mode_label(),
            leg.transit_days(),
            if leg.is_cross_border() { "是" } else { "否" }
        );
    }
    // 路由总时效的自由函数(此前是死代码)。
    println!(
        "        自由函数核对总时效:{} 天",
        dispatch::total_transit_days(option.route_legs())
    );
}
// 运力台账的只读口径(此前未读的 carrier_count / snapshot / utilization)。
println!(
    "    运力台账 {} 家承运商 | 快照 {}",
    ledger.carrier_count(),
    ledger
        .snapshot()
        .iter()
        .map(|(code, occupied, limit)| format!(
            "{} 占用 {}/{}",
            code,
            occupied.formatted_grams(),
            limit.formatted_kilograms()
        ))
        .collect::&lt;Vec&lt;String&gt;&gt;()
        .join(";")
);
// 台账的占用率查询(此前是死代码)。
let capacity_probe = facade::builtin_dispatch::BuiltinCapacityPort::from_ledger(&amp;ledger);
println!(
    "    运力占用率查询(BuiltinCapacityPort::from_ledger):SF = {}.{:02}%",
    capacity_probe.utilization_basis_points("SF") / 100,
    capacity_probe.utilization_basis_points("SF") % 100
);

// 包装方案的口径(此前未读的 material_kind_count / total_material_quantity)。
let packaging_request = packaging::packaging_planner::PackagingRequest {
    jewelry_piece_count: request.jewelry_piece_count(),
    other_piece_count: request.other_piece_count(),
    requires_temperature_control: request.requires_temperature_control,
    net_cargo_weight: request.net_cargo_weight(),
};
let packaging_plan = packaging::plan_packaging(&amp;packaging_request);
println!(
    "    包装 {} 种材料 | 共 {} 件 | 总成本 {} | 增重 {}",
    packaging_plan.material_kind_count(),
    packaging_plan.total_material_quantity(),
    packaging_plan.total_material_cost().formatted(),
    packaging_plan.packaging_weight_gain().formatted_grams()
);
// 逐行材料(行成本的口径)。
for line in packaging_plan.lines() {
    println!("      {}", line.formatted());
}
// 材料属性(此前未读的 is_recyclable / is_temperature_insulating)。
for line in packaging_plan.lines() {
    println!(
        "      {}({})| 可回收 {} | 保温 {}",
        line.material().label(),
        line.material().code(),
        line.material().is_recyclable(),
        line.material().is_temperature_insulating()
    );
}

// 单据清单的口径(此前未读的 mandatory_count / pending_count / is_complete / summary_text)。
let document_request = documents::document_compiler::DocumentRequest {
    is_cross_border: request.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(),
};
let checklist = documents::compile_document_checklist(&amp;document_request, &amp;[]);
println!(
    "    单据 {} 项 | 强制 {} | 待生成 {} | 阻断 {} | 是否齐备 {} | 目的地 {}",
    checklist.total_count(),
    checklist.mandatory_count(),
    checklist.pending_count(),
    checklist.blocking_count(),
    checklist.is_complete(),
    checklist.destination_country_code()
);
println!("      清单:{}", checklist.summary_text());
// 单据类型自身的属性(此前未读的 is_legally_required / template_identifier)。
for document in checklist.requirements() {
    println!(
        "      {} | 法定必备 {} | 模板 {} | 触发 {}",
        document.kind().label(),
        document.kind().is_legally_required(),
        document.kind().template_identifier(),
        document.trigger_reason()
    );
}

// 结算结果的口径(此前未读的 subtotal_of / share_basis_points / item_codes / item_count)。
let settlement_result = settlement::settle_charges(
    result
        .charge_items
        .iter()
        .map(|(code, label, source_label, amount, basis)| {
            settlement::ChargeItem::new(
                code,
                label,
                settlement_source_from_label(source_label),
                *amount,
                basis,
            )
        })
        .collect(),
);
println!(
    "    结算 {} 条 | 应付 {} | 来源分组 {}",
    settlement_result.item_count(),
    settlement_result.grand_total().formatted(),
    settlement_result.by_source().len()
);
println!(
    "      编码序列:{}",
    settlement_result.item_codes().join(" → ")
);
for subtotal in settlement_result.by_source() {
    println!(
        "      {} | {} 条 | 小计 {} | 占总额 {}.{:02}%",
        subtotal.source().label(),
        subtotal.item_count(),
        subtotal.subtotal().formatted(),
        settlement_result.share_basis_points(subtotal.source()) / 100,
        settlement_result.share_basis_points(subtotal.source()) % 100
    );
}
// subtotal_of 的查询口径(此前是死代码)。
println!(
    "      按来源查询:基础运费 = {};包装材料费 = {}",
    settlement_result
        .subtotal_of(settlement::ChargeSource::Freight)
        .formatted(),
    settlement_result
        .subtotal_of(settlement::ChargeSource::Packaging)
        .formatted()
);
// 门面中立视图的按来源查询(`SettlementView::subtotal_of_source` 在这里被读)。
// 这条路径才是调用方**真正能走**的:视图里只有字符串来源码,没有枚举;
// 上面那条用枚举查询是子系统内部口径,仅供对照,证明两者口径一致。
let settlement_view = facade::translate_settlement(&amp;settlement_result);
println!(
    "      视图按来源码查询(调用方口径):FREIGHT = {} | PACKAGING = {} | 未知来源 = {}",
    settlement_view
        .subtotal_of_source("FREIGHT")
        .map(|amount| amount.formatted())
        .unwrap_or_else(|| "(无)".to_string()),
    settlement_view
        .subtotal_of_source("PACKAGING")
        .map(|amount| amount.formatted())
        .unwrap_or_else(|| "(无)".to_string()),
    settlement_view
        .subtotal_of_source("NO_SUCH_SOURCE")
        .map(|amount| amount.formatted())
        .unwrap_or_else(|| "(无)".to_string())
);
// ChargeSource 自身的属性。
for source in [
    settlement::ChargeSource::Freight,
    settlement::ChargeSource::Packaging,
    settlement::ChargeSource::Documentation,
    settlement::ChargeSource::ValueAddedService,
] {
    println!(
        "      来源 {} | 展示序 {} | 计入总额 {}",
        source.label(),
        source.display_order(),
        source.counts_toward_total()
    );
}

// 承运时间线的口径(此前未读的 milestone_codes / 里程碑数)。
let timeline_request = carrier::timeline_builder::TimelineRequest {
    dispatch_date: request.planned_dispatch_date,
    route_leg_days: planning_result
        .cheapest_feasible()
        .map(|o| o.route_legs().iter().map(|l| l.transit_days()).collect())
        .unwrap_or_default(),
    route_leg_is_cross_border: planning_result
        .cheapest_feasible()
        .map(|o| o.route_legs().iter().map(|l| l.is_cross_border()).collect())
        .unwrap_or_default(),
    destination_city: request.receiver_city,
};
let timeline = carrier::build_timeline(&amp;timeline_request);
println!(
    "    时间线 {} 个里程碑 | 编码 {} | 送达 {} | 是否顺延 {}",
    timeline.milestone_count(),
    timeline.milestone_codes().join(" → "),
    timeline.estimated_delivery_date().formatted(),
    timeline.delayed_by_weekend()
);
// 里程碑级别的字段(falls_on_non_working_day 此前是死代码)。
for milestone in timeline.milestones() {
    println!(
        "      [{}] {} | {} | 非工作日 {} | {}",
        milestone.code(),
        milestone.label(),
        milestone.date().formatted(),
        milestone.falls_on_non_working_day(),
        milestone.note()
    );
}
// 工作日推算自由函数(此前是死代码)。
let dispatch_adjusted = carrier::adjust_dispatch_date_for_weekend(
    &amp;CalendarDate::from_ymd(2026, 10, 10),
);
println!(
    "    周六 2026-10-10 顺延后 = {}({})| 工作日判定 {} | 报关准备天数常量 {}",
    dispatch_adjusted.formatted(),
    dispatch_adjusted.weekday_text(),
    carrier::is_working_day(&amp;dispatch_adjusted),
    carrier::CUSTOMS_PREPARATION_DAYS
);
// working_days_between:从周一起算 5 个工作日应落在下周一。
let five_working_days_later =
    carrier::working_days_between(&amp;CalendarDate::from_ymd(2026, 10, 5), 5);
println!(
    "    2026-10-05(周一)后 5 个工作日 = {}({})",
    five_working_days_later.formatted(),
    five_working_days_later.weekday_text()
);

// 领域值对象的口径(此前未读的若干方法)。
println!();
println!("  领域值对象口径:");
let amount_a = CurrencyAmount::from_major_and_minor(1_234, 56, CURRENCY_CHINESE_YUAN);
let amount_b = CurrencyAmount::from_minor_units(-5_000, CURRENCY_CHINESE_YUAN);
println!(
    "    from_major_and_minor(1234, 56) = {} | 带币种 {}",
    amount_a.formatted(),
    amount_a.formatted_with_currency_code()
);
println!(
    "    负金额 {} | 绝对值 {} | 是否负 {}",
    amount_b.formatted(),
    amount_b.absolute().formatted(),
    amount_b.is_negative()
);
// Ratio 的口径(此前未读的 zero / basis_points / is_zero / is_greater_than /
// as_percent_text / as_basis_points_text)。
let ratio = domain::Ratio::from_basis_points(750);
println!(
    "    比率 750 万分点 | 百分比 {} | 原始 {} | 是否为零 {} | 大于零比率 {}",
    ratio.as_percent_text(),
    ratio.as_basis_points_text(),
    ratio.is_zero(),
    ratio.is_greater_than(&amp;domain::Ratio::zero())
);
// 重量口径(此前未读的 from_grams_and_milligrams / is_zero / sum / total_weight)。
let weight_list = [
    ShippingWeight::from_grams_and_milligrams(320, 500),
    ShippingWeight::from_grams_and_milligrams(85, 250),
];
let weight_sum = ShippingWeight::sum(&amp;weight_list);
println!(
    "    重量序列求和 {}({} 毫克)| 零重量是否为零 {}",
    weight_sum.formatted_grams(),
    weight_sum.milligrams(),
    ShippingWeight::zero().is_zero()
);
// Currency 与开放型标签的其余属性。
// 显式标注类型让统一出口导出的 `Currency` 被真实使用一次。
let yuan: domain::Currency = domain::CURRENCY_CHINESE_YUAN;
println!(
    "    币种 {}({})| 主单位最小单位数 {} | 符号 {}",
    yuan.label(),
    yuan.code(),
    yuan.minor_units_per_major_unit(),
    yuan.symbol()
);
println!(
    "    港币标签(对照){}({})| 港区标签 {}",
    domain::CURRENCY_HONG_KONG_DOLLAR.label(),
    domain::CURRENCY_HONG_KONG_DOLLAR.code(),
    domain::COUNTRY_HONG_KONG.label()
);
// 两个非标准服务等级(此前未读的 EXPRESS / ECONOMY)。
for tier in [domain::SERVICE_TIER_EXPRESS, domain::SERVICE_TIER_ECONOMY] {
    println!(
        "    服务等级 {}({})| 时效倍数 {}.{:02} | 附加费率 {}.{:02}% | 承诺时刻 {}",
        tier.label(),
        tier.code(),
        tier.transit_days_multiplier_basis_points() / 10_000,
        (tier.transit_days_multiplier_basis_points() % 10_000) / 100,
        tier.surcharge_basis_points() / 100,
        tier.surcharge_basis_points().abs() % 100,
        tier.guarantees_exact_time()
    );
}
// 品类与单据属性的其余部分。
println!(
    "    珠宝品类 信用证 {} | 包装物料 报关 {} | 证书品类 报关 {}",
    CARGO_JEWELRY.allows_letter_of_credit(),
    CARGO_PACKAGING.requires_formal_declaration(),
    CARGO_CERTIFICATE.requires_formal_declaration()
);
// 事件等级的完整属性(此前未读的 priority / marker)。
for severity in [
    domain::EventSeverity::Info,
    domain::EventSeverity::Warning,
    domain::EventSeverity::Critical,
] {
    println!(
        "    事件等级 {} | 标记 {} | 优先级 {} | 阻断出单 {}",
        severity.label(),
        severity.marker(),
        severity.priority(),
        severity.blocks_dispatch()
    );
}
// 包装材料的属性(此前未读的 is_recyclable / is_temperature_insulating)。
for material in [domain::PACKAGING_WOODEN_CRATE, domain::PACKAGING_FOAM_BOX] {
    println!(
        "    材料 {}({})| 单价 {} | 可回收 {} | 保温 {}",
        material.label(),
        material.code(),
        CurrencyAmount::from_minor_units(material.unit_cost_minor_units(), CURRENCY_CHINESE_YUAN)
            .formatted(),
        material.is_recyclable(),
        material.is_temperature_insulating()
    );
}
// 国家标签属性。
println!(
    "    国家 CN 需报关 {} | DE 需报关 {} | 香港标签 {}",
    COUNTRY_CHINA.requires_customs_declaration(),
    COUNTRY_GERMANY.requires_customs_declaration(),
    domain::COUNTRY_HONG_KONG.code()
);
// 单据类型的其余属性(此前未读的 is_legally_required / template_identifier)。
println!(
    "    温控声明 法定必备 {} | 模板 {}",
    domain::DOCUMENT_TEMPERATURE_DECLARATION.is_legally_required(),
    domain::DOCUMENT_TEMPERATURE_DECLARATION.template_identifier()
);

println!();
println!("  这条命令行的金额行口径(报表层唯一的金额排版入口):");
// `render_amount_line` 是报表层给金额用的唯一排版函数。
// 第七幕调用它,是为了证明「排版口径只此一处」------
// 子系统自带的 `formatted()`(见下文对照)不算排版入口。
render_amount_line("样例 应付总额", &amp;settlement_result.grand_total());
render_amount_line("样例 运费小计", &amp;settlement_result.subtotal_of(settlement::ChargeSource::Freight));

// ---------- 跨维度汇总函数与其余只读口径 ----------
println!();
println!("  跨维度汇总与其余口径:");
// `count_events_of_severity` 是分析层唯一一个「按维度计数」的函数。
// 它与 `ShippingResult` 自带的 `blocker_count()` 等并不重复------
// 前者回答「任意等级」,后者回答「固定等级」,且前者可在新增等级时零改动。
for severity in [
    domain::EventSeverity::Info,
    domain::EventSeverity::Warning,
    domain::EventSeverity::Critical,
] {
    println!(
        "    按等级汇总 {} = {} 条",
        severity.label(),
        analysis::count_events_of_severity(result, severity)
    );
}
// 单据清单的编码序列(`document_codes` 在这里被读);
// 与门面视图的 `document_codes()` 是两个不同层的同名口径,此处并列对照。
println!(
    "    子系统清单编码序列:{}",
    checklist.document_codes().join(" ← ")
);
println!(
    "    门面视图编码序列:  {}",
    result.document_codes().join(" ← ")
);
// `Ratio::basis_points`(取回原始万分比)与分母常量在这里被读。
let sample_ratio = domain::Ratio::from_basis_points(2_500);
println!(
    "    比率原始值 {} | 分母常量 {} | 手算百分比 {}.{:02}%",
    sample_ratio.basis_points(),
    domain::BASIS_POINTS_DENOMINATOR,
    sample_ratio.basis_points() / 100,
    sample_ratio.basis_points() % 100
);
// 时间线画像里此前未读的两个日期字段:发货日与预计送达日。
// 它们让「画像」自身也能回答「从哪天到哪天」,而不只是天数。
println!(
    "    画像起止:发货 {} → 送达 {}(跨 {} 个自然日)",
    profile.dispatch_date.formatted(),
    profile.estimated_delivery_date.formatted(),
    profile.dispatch_date.days_until(&amp;profile.estimated_delivery_date)
);
// 承运时间线请求里的目的地城市字段(此前未读)。
println!(
    "    时间线请求目的地城市:{} | 时间线请求发货日 {}",
    timeline_request.destination_city,
    timeline_request.dispatch_date.formatted()
);

// ---------- 子系统自带排版 vs 报表层排版(对照演示) ----------
println!();
println!("  子系统自带 formatted() 与报表层排版的对照:");
// 下面三条调用演示「子系统越界做排版」的后果:
// 子系统为了「自己看着方便」,把编码、括号、分隔符、缩进都硬写进格式串。
// 一旦报表要换列宽、换货币符号、换缩进,就得改子系统------
// 而子系统本不该关心任何排版。所以门面的视图**只给裸数据**,
// 排版权留给 app 层。此处并列打印,差异一眼可见。
println!("    [ChargeItem::formatted]   {}", settlement_result.items()[0].formatted());
println!("    [ChargeEntry::formatted]  {}", breakdown_entries[0].formatted());
println!(
    "    [DocumentRequirement::formatted] {}",
    checklist.requirements()[0].formatted()
);
println!("    ── 上面三条出自子系统;下面一条出自报表层(裸数据 + 报表排版)");
render_amount_line("报表层 应付总额", &amp;settlement_result.grand_total());

println!();
println!("  这一幕的意义:以上每个方法都被**真实调用过**。");
println!("  因此本工程可以理直气壮地说「零 warning」不是靠 allow 换来的------");
println!("  编译器在这里充当了架构审计工具:没被用起来的公开方法会立刻告警。");
println!();
}
/// 把来源名称文本映射回结算来源枚举(仅供第七幕重建费用项使用)。
///
/// 参数 source_label:来源名称(如「基础运费」)。
/// 返回:对应的来源枚举。
fn settlement_source_from_label(source_label: &str) -> settlement::ChargeSource {
match source_label {
"基础运费" => settlement::ChargeSource::Freight,
"包装材料费" => settlement::ChargeSource::Packaging,
"单据工本费" => settlement::ChargeSource::Documentation,
_ => settlement::ChargeSource::ValueAddedService,
}
}
// ══════════════════════════════════════════════════════════════════════════
// 一个编译期占位:保证 facade 的出口被真实使用(避免未使用导入告警)
// ══════════════════════════════════════════════════════════════════════════
/// 标记函数:让 facade 的统一出口中的类型在编译期被引用。
///
/// 本工程通过 facade::... 的统一出口导入所有门面类型,
/// 若某些类型只在文档里被提到而未被代码引用,会产生未使用导入告警。
/// 这个函数显式引用它们(只做类型标注,不产生运行期行为),
/// 使「走统一出口」这一约定得以保持,而不必退回到深层路径导入。
///
/// 之所以不用 #[allow(unused_imports)]:那会掩盖真正的未使用导入,
/// 使「某个出口没人走」这类架构问题无法被发现。
pub fn audit_helpers_unused_marker() {
// 各类型的显式引用(仅类型标注,零运行开销)。
let _: Option<CarrierOptionView> = None;
let _: Option<PackagingMaterialLineView> = None;
let _: Option<SettlementInputItem> = None;
let _: Option<TimelineMilestoneView> = None;
let _: Option<DispatchInput> = None;
let _: Option<DocumentInput> = None;
let _: Option<PackagingInput> = None;
let _: Option<TimelineInput> = None;
let _: Option<DocumentRequirementView> = None;
let _: Option<PackagingPlanView> = None;
let _: Option<SettlementView> = None;
let _: Option<TimelineView> = None;
// 端口 trait 的引用(证明它们是对外契约的一部分)。
let _: Option<Box<dyn DispatchPort>> = None;
let _: Option<Box<dyn PackagingPort>> = None;
let _: Option<Box<dyn DocumentCompilationPort>> = None;
let _: Option<Box<dyn TimelinePort>> = None;
let _: Option<Box<dyn SettlementPort>> = None;
let _: Option<Box<dyn CapacityPort>> = None;
// 门面翻译函数(供工程外实现复用;此处显式引用以保证出口有人走)。
let _: fn(&dispatch::CarrierOption) -> CarrierOptionView = facade::translate_carrier_option;
let _: fn(&packaging::PackagingPlan) -> PackagingPlanView = facade::translate_packaging_plan;
let _: fn(&settlement::ChargeItem) -> SettlementInputItem = facade::translate_charge_item;
let _: fn(&documents::DocumentRequirement) -> DocumentRequirementView =
facade::translate_document_requirement;
let _: fn(&carrier::CarrierTimeline) -> TimelineView = facade::translate_timeline;
let _: fn(&settlement::SettlementResult) -> SettlementView = facade::translate_settlement;
// 调度端口的辅助翻译(供工程外实现批量转换)。
let _: fn(&[dispatch::CarrierOption]) -> Vec<CarrierOptionView> =
facade::builtin_dispatch::translate_options;
// 子系统统一出口里的其余类型:它们由子系统自己导出,
// 上层(尤其是工程外扩展区)需要按统一出口的路径名引用,
// 而不是写到 `xxx::具体文件名::Type`。这里逐一标注一次,
// 保证每个出口都真的有人走------若有出口长期无人走,此处的引用
// 一旦被删,编译告警会再次出现,提醒我们处理。
let _: Option&lt;documents::DocumentChecklist&gt; = None;
let _: Option&lt;documents::DocumentStatus&gt; = None;
let _: Option&lt;fn(&amp;[&amp;dyn Fn(&amp;documents::DocumentRequirement) -&gt; bool]) -&gt; Vec&lt;documents::DocumentRule&gt;&gt; =
    None;
let _: Option&lt;carrier::TimelineMilestone&gt; = None;
let _: Option&lt;dispatch::RoutePlanningResult&gt; = None;
let _: Option&lt;packaging::PackagingPlanLine&gt; = None;
}

输出:

相关推荐
福大大架构师每日一题1 小时前
Rust 1.99.0发布:C 可变参数、裸函数、Cargo 配置、Rustdoc 性能与大量兼容性调整全解析
c语言·开发语言·rust
智鸟科技GemeOpen开发者智能设备1 小时前
平安校园 AI 智能实时告警系统 智鸟科技·GemeOpen + 谷华科技·融合创新方案白皮书
java·开发语言·python·物联网·智能家居
Wang's Blog2 小时前
Java框架 SpringCloud 快速入门: Feign 的自定义配置与日志级别
java·开发语言·spring cloud
成旭先生2 小时前
企业全景信息查询 API:工商照面 33 项、股东出资、变更与社保一次查全
java·开发语言·api接口·企业信息查询·工商数据·风控尽调·供应商准入
2601_962885722 小时前
批量取数比循环单取快多少?用 AlphaFeed klines.batch给多股票取数提速
开发语言·php·batch
动词ing2 小时前
【C语言题目练习】算法 整数反转
c语言·开发语言·算法
颜进强2 小时前
20 · NestJs循环依赖与 forwardRef:容器为什么在环面前会死,"占位再补齐"怎么救
前端·后端·ai编程
颜进强2 小时前
19 · NestJs @Global 落地账:装饰器与 `isGlobal` 参数,各在什么场景上岗
前端·后端·ai编程
多弗朗皮卡丘2 小时前
C++多继承
开发语言·c++·多继承