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;
}
输出:

哲学管理(学)人生, 文学艺术生活, 自动(计算机学)物理(学)工作, 生物(学)化学逆境, 历史(学)测绘(学)时间, 经济(学)数学金钱(理财), 心理(学)医学情绪, 诗词美容情感, 美学建筑(学)家园, 解构建构(分析)整合学习, 智商情商(IQ、EQ)运筹(学)生存.---Geovin Du(涂聚文)
浙公网安备 33010602011771号