rust: Facade Pattern 续

 

 

//!# 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,
    ));
}

  

 

调用:

//!# 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 &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(&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(&planning_request, &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(&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(&document_request, &[]);

    // ---- 步骤 5:结算(调用方要把五方产出手工拼成费用项) ----
    // 这一段最长,也最能说明问题:每一条费用项都要调用方自己组装,
    // 且必须记住「包装材料成本来自 packaging、单据按张计费、
    // 保价按申报价值比例」这些跨子系统的知识。
    let mut charge_items: Vec<settlement::ChargeItem> = 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 > 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(&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(
        "承运商",
        &format!("{}({})", chosen.carrier_label(), chosen.carrier_code()),
    );
    render_summary_line(
        "路由",
        &format!(
            "{} 段,共 {} 天",
            chosen.route_leg_count(),
            chosen.total_transit_days()
        ),
    );
    render_summary_line("包装材料", &packaging_result.material_summary_text());
    render_summary_line(
        "包装后总重",
        &packaging_result
            .total_weight_with_packaging()
            .formatted_kilograms(),
    );
    render_summary_line(
        "预计送达",
        &timeline_result.estimated_delivery_date().formatted(),
    );
    render_summary_line("单据", &format!("{} 项", checklist.total_count()));
    render_summary_line("应付总额", &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(&request);");
    println!("    // 然后只剩下:把 result 交给报表层排版");
    println!();

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

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

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

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

    render_timeline_section(&result, &timeline_profile);
    render_rule();
    render_document_section(&result);
    render_rule();
    render_charge_section(&result, &charge_breakdown);
    render_rule();
    render_constraint_section(&constraint_report);
    render_rule();
    render_event_section(&result);
    render_rule();
    render_conclusion(&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(&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(&request);
    let result = outcome.result();

    render_shipment_header(result);
    render_summary_line(
        "承运商",
        &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, &timeline_profile);
    render_rule();
    render_document_section(result);
    render_rule();

    let charge_breakdown = build_charge_breakdown(result);
    render_charge_section(result, &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(&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(&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("装配结论", &status_line);
    render_rule();
    render_cargo_section(&result);
    render_rule();
    render_packaging_section(&result);
    render_rule();
    render_route_section(&result);
    render_rule();
    render_document_section(&result);
    render_rule();

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

    render_timeline_section(&result, &timeline_profile);
    render_rule();
    render_charge_section(&result, &charge_breakdown);
    render_rule();
    render_constraint_section(&constraint_report);
    render_rule();
    render_event_section(&result);
    render_rule();
    render_conclusion(&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(&grand_total);
        let mut final_items: Vec<SettlementInputItem> = 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(&processing_fee);

        // 重新计算来源小计(把处理费计入增值服务费)。
        for entry in source_subtotals.iter_mut() {
            if entry.0 == "VALUE_ADDED_SERVICE" {
                entry.1 = entry.1.add(&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<&'static str> = 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(&PACKAGING_BAMBOO_BOX);
    let unit_gain: i64 = packaging::packaging_planner::unit_gain_milligrams(&PACKAGING_BAMBOO_BOX);
    println!(
        "  是否为包装子系统所登记:{}(单位增重 {} 毫克)",
        if is_registered {
            "是"
        } else {
            "否 —— 需在包装子系统登记,否则增重按 0 计"
        },
        unit_gain
    );
    // 对比一个已登记材料,说明查询函数确实在工作。
    let registered_gain: i64 =
        packaging::packaging_planner::unit_gain_milligrams(&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(&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(&request.planned_dispatch_date)
    );
    // 单件重量口径对照:单件重量 vs 行总重(含件数)。
    for line in &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: &[DocumentView] = &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: &[RouteLegView] = &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: &PackagingView = &result.packaging;
    let timeline_view: &ShipmentTimelineView = &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: &[FacadeEvent] = &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(
        &packaging::plan_packaging(&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 &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: &[ChargeEntry] = &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 &audit.entries {
        let _typed_entry: &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 &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 && 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(&planning_request, &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::<Vec<String>>()
            .join(";")
    );
    // 台账的占用率查询(此前是死代码)。
    let capacity_probe = facade::builtin_dispatch::BuiltinCapacityPort::from_ledger(&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(&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(&document_request, &[]);
    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(&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(&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(
        &CalendarDate::from_ymd(2026, 10, 10),
    );
    println!(
        "    周六 2026-10-10 顺延后 = {}({})| 工作日判定 {} | 报关准备天数常量 {}",
        dispatch_adjusted.formatted(),
        dispatch_adjusted.weekday_text(),
        carrier::is_working_day(&dispatch_adjusted),
        carrier::CUSTOMS_PREPARATION_DAYS
    );
    // working_days_between:从周一起算 5 个工作日应落在下周一。
    let five_working_days_later =
        carrier::working_days_between(&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(&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(&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("样例 应付总额", &settlement_result.grand_total());
    render_amount_line("样例 运费小计", &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(&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("报表层 应付总额", &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<documents::DocumentChecklist> = None;
    let _: Option<documents::DocumentStatus> = None;
    let _: Option<fn(&[&dyn Fn(&documents::DocumentRequirement) -> bool]) -> Vec<documents::DocumentRule>> =
        None;
    let _: Option<carrier::TimelineMilestone> = None;
    let _: Option<dispatch::RoutePlanningResult> = None;
    let _: Option<packaging::PackagingPlanLine> = None;
}

  

输出:

image

 

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