rust: Abstract Factory pattern

项目结构:

rust 复制代码
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : channel_code.rs
//! 销售渠道编码(开放型标签)。
 
/// 销售渠道编码------标识单据套件属于哪个销售渠道。
///
/// ## 为什么是开放型结构体
/// 本工程的核心验证目标是「工程外新增一个渠道,不修改任何既有分层文件」。
/// 若渠道写成枚举,`main.rs` 里的直播渠道就无法在不改动本文件的情况下登记,
/// 扩展性验证会当场失败。
///
/// 用 `const fn new` 之后,工程外可以这样定义全新渠道:
///
/// ```ignore
/// const LIVE_STREAM: ChannelCode = ChannelCode::new("LIVE_STREAM");
/// ```
///
/// ## 为什么内部只存一个字符串
/// 渠道编码是**标识**,不是状态。它只需要可比较、可排序、可打印。
/// 渠道的其它属性(税率、质保期、币种)不属于编码本身,
/// 而属于「产品族」------由各渠道的工厂与产品各自持有。
/// 把那些属性塞进编码里,会让编码变成上帝对象。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ChannelCode {
    /// 渠道的机器标识,建议大写蛇形命名
    code: &'static str,
}
 
impl ChannelCode {
    /// 构造一个渠道编码。
    ///
    /// # 参数
    /// - `code`:渠道机器标识
    pub const fn new(code: &'static str) -> Self {
        Self {
            // 只存标识本身
            code,
        }
    }
 
    /// 读取渠道机器标识。
    pub const fn as_str(&self) -> &'static str {
        // 直接暴露内部静态切片
        self.code
    }
 
    /// 预置:内地线下门店。
    pub const RETAIL_STORE: ChannelCode = ChannelCode::new("RETAIL_STORE");
 
    /// 预置:线上商城。
    pub const ONLINE_MALL: ChannelCode = ChannelCode::new("ONLINE_MALL");
 
    /// 预置:市内免税店。
    pub const DUTY_FREE_STORE: ChannelCode = ChannelCode::new("DUTY_FREE_STORE");
}
 
impl std::fmt::Display for ChannelCode {
    /// 对外展示渠道机器标识。
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(formatter, "{}", self.code)
    }
}
 
 
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : currency.rs
//! 货币值对象。
 
/// 货币------金额的计价单位。
///
/// ## 为什么货币要独立成一个类型
/// 「金额」离开「币种」是没有意义的数字。把币种从金额里拆出来独立成类型,
/// 是为了让「不同币种不可相加」这条业务规则**在类型层面可表达**
/// (见 [`crate::domain::money::Money::add`] 的同币种断言)。
///
/// ## 为什么是开放型结构体而不是枚举
/// 本工程要求「完全可扩展」。若货币是枚举,`main.rs` 里想加一种新币种
/// 就必须修改本文件------那就不是扩展,而是改动。
/// 用 `const fn new` 之后,工程外可以写:
///
/// ```ignore
/// const JPY: Currency = Currency::new("JPY", "¥", 0);
/// ```
///
/// 无需触碰领域层。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Currency {
    /// ISO 4217 三字母货币代码,例如 `CNY` / `HKD`
    code: &'static str,
    /// 展示用货币符号,例如 `¥` / `HK$`
    symbol: &'static str,
    /// 小数位数:人民币与港币均为 2
    decimal_places: u8,
}
 
impl Currency {
    /// 构造一种货币。
    ///
    /// # 参数
    /// - `code`:ISO 4217 三字母代码
    /// - `symbol`:展示用符号
    /// - `decimal_places`:小数位数
    ///
    /// 声明为 `const fn` 是本类型可扩展的**唯一前提**:
    /// 只有 const 构造才能在工程外定义新的 `const` 币种常量。
    pub const fn new(code: &'static str, symbol: &'static str, decimal_places: u8) -> Self {
        Self {
            // 货币代码
            code,
            // 展示符号
            symbol,
            // 小数位数
            decimal_places,
        }
    }
 
    /// 读取 ISO 4217 货币代码。
    pub const fn code(&self) -> &'static str {
        // 直接暴露内部静态切片
        self.code
    }
 
    /// 读取展示用货币符号。
    pub const fn symbol(&self) -> &'static str {
        // 直接暴露内部静态切片
        self.symbol
    }
 
    /// 读取小数位数。
    pub const fn decimal_places(&self) -> u8 {
        // u8 是 Copy,直接返回
        self.decimal_places
    }
 
    /// 1 个主单位(元)对应的最小单位(分)数量。
    ///
    /// 例如人民币返回 100。金额在内部一律以最小单位整数存储,
    /// 需要与「元」互转时都走这里,避免各处重复手写 `100` 这类魔数。
    ///
    /// # 为什么用循环而不是 `pow`
    /// `pow` 在 `const fn` 上下文中的可用性随版本变动,
    /// 而 `while` 循环自 Rust 1.46 起已稳定支持常量求值,更稳妥。
    pub const fn minor_units_per_major_unit(&self) -> i64 {
        // 累积倍数,从 1 开始
        let mut scale: i64 = 1;
        // 剩余需要乘的位数
        let mut remaining_places: u8 = self.decimal_places;
        while remaining_places > 0 {
            // 每位乘 10
            scale *= 10;
            // 位数递减
            remaining_places -= 1;
        }
        scale
    }
 
    /// 预置:人民币。
    pub const CNY: Currency = Currency::new("CNY", "¥", 2);
}
 
impl std::fmt::Display for Currency {
    /// 对外展示货币代码。
    ///
    /// 刻意只输出代码而不输出符号:符号是「金额格式化」的职责
    /// (见 `Money::formatted`),两者混在一起会导致符号重复拼接。
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(formatter, "{}", self.code)
    }
}
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : document_body.rs
//! 单据正文值对象。
 
/// 单据正文------一件产品渲染后的结构化结果。
///
/// ## 为什么不让产品直接返回 `String`
/// 若 `render()` 返回裸字符串,上层(报表层、分析层)就只能把它当黑盒打印,
/// 无法回答「这份单据有几行」「抬头是什么」这类问题。
/// 返回结构化的「抬头 + 行集合」后:
/// - 报表层可以统一加边框、加编号;
/// - 分析层可以统计单据篇幅(本工程用它对比各渠道单据的信息量)。
///
/// ## 为什么不在这里放颜色、字号
/// 那是**排版渲染**的职责,不是**领域**的职责。
/// 本类型只描述「说了什么」,不描述「长什么样」------
/// 换成 HTML / PDF 渲染器时,本类型无需任何改动。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentBody {
    /// 单据抬头
    heading: String,
    /// 正文行(已含缩进与对齐,由各产品自行负责)
    lines: Vec<String>,
}
 
impl DocumentBody {
    /// 构造一份单据正文。
    ///
    /// # 参数
    /// - `heading`:单据抬头
    /// - `lines`:正文行集合
    pub fn new(heading: impl Into<String>, lines: Vec<String>) -> Self {
        Self {
            // 抬头
            heading: heading.into(),
            // 正文行
            lines,
        }
    }
 
    /// 读取单据抬头。
    pub fn heading(&self) -> &str {
        // 切片式暴露
        &self.heading
    }
 
    /// 读取正文行集合。
    pub fn lines(&self) -> &[String] {
        // 切片式暴露,上层可自由遍历
        &self.lines
    }
 
    /// 正文行数(不含抬头)。
    ///
    /// 分析层用它衡量各渠道单据的信息密度。
    pub fn line_count(&self) -> usize {
        self.lines.len()
    }
 
    /// 渲染为多行纯文本。
    pub fn to_text(&self) -> String {
        // 以抬头起始
        let mut text: String = self.heading.clone();
        // 逐行追加
        for line in &self.lines {
            text.push('\n');
            text.push_str(line);
        }
        text
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : document_spec.rs
//! 单据规格值对象------生成整套单据所需的全部输入数据。
 
use crate::domain::currency::Currency;
use crate::domain::item_line::ItemLine;
use crate::domain::money::Money;
use crate::domain::rate::Rate;
 
/// 单据规格------一次交易的原始输入。
///
/// ## 这个类型在抽象工厂里的位置
/// 它是**唯一**从客户端流向所有产品构造函数的参数:
///
/// ```text
/// 客户端 → DocumentSpec → 工厂 → 三件产品(各自持有规格副本)
/// ```
///
/// 三种产品(价格标签 / 销售小票 / 质保卡)都需要「这笔交易是什么」,
/// 但各自只取其中一部分:价格标签要商品与价格,小票要明细与折扣,
/// 质保卡要商品与下单日期。把共同输入收进一个值对象,
/// 是避免「每个工厂方法带一长串参数」的关键。
///
/// ## 为什么币种显式持有而不是从商品行推导
/// 空订单(明细为空)是合法状态,此时无法从商品行推出币种。
/// 显式持有币种后,`net_amount()` 在空订单下也能返回「人民币的零」
/// 而不是 panic。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentSpec {
    /// 订单号
    order_reference: String,
    /// 客户名称
    customer_name: String,
    /// 开单日期(文本形式,例如 `2026-10-04`)
    ///
    /// 刻意用文本而非日期类型:本工程零第三方依赖,而 Rust 标准库没有
    /// 日期类型。引入 `chrono` 会让这个演示工程被依赖细节淹没,
    /// 而本工程的税务逻辑并不需要真正的日期运算。
    issue_date: String,
    /// 结算币种(全渠道统一为人民币以便横向对照)
    currency: Currency,
    /// 商品明细行
    items: Vec<ItemLine>,
    /// 订单级折扣额(正数表示减免金额),在商品小计之上直接扣减
    order_discount: Money,
    /// 客户等级标签,例如「黄金 VIP」
    customer_tier_label: String,
}
 
impl DocumentSpec {
    /// 构造一份单据规格。
    ///
    /// # 参数
    /// - `order_reference`:订单号
    /// - `customer_name`:客户名称
    /// - `issue_date`:开单日期
    /// - `currency`:结算币种
    /// - `items`:商品明细行
    /// - `order_discount`:订单级折扣额
    /// - `customer_tier_label`:客户等级标签
    pub fn new(
        order_reference: impl Into<String>,
        customer_name: impl Into<String>,
        issue_date: impl Into<String>,
        currency: Currency,
        items: Vec<ItemLine>,
        order_discount: Money,
        customer_tier_label: impl Into<String>,
    ) -> Self {
        Self {
            // 订单号
            order_reference: order_reference.into(),
            // 客户名称
            customer_name: customer_name.into(),
            // 开单日期
            issue_date: issue_date.into(),
            // 结算币种
            currency,
            // 商品明细
            items,
            // 订单级折扣
            order_discount,
            // 客户等级
            customer_tier_label: customer_tier_label.into(),
        }
    }
 
    /// 读取订单号。
    pub fn order_reference(&self) -> &str {
        // 切片式暴露
        &self.order_reference
    }
 
    /// 读取客户名称。
    pub fn customer_name(&self) -> &str {
        // 切片式暴露
        &self.customer_name
    }
 
    /// 读取开单日期。
    pub fn issue_date(&self) -> &str {
        // 切片式暴露
        &self.issue_date
    }
 
    /// 读取结算币种。
    pub fn currency(&self) -> Currency {
        // Currency 是 Copy
        self.currency
    }
 
    /// 读取商品明细行集合。
    pub fn items(&self) -> &[ItemLine] {
        // 切片式暴露,上层可自由遍历
        &self.items
    }
 
    /// 读取订单级折扣额(正数)。
    pub fn order_discount(&self) -> Money {
        // Money 是 Copy
        self.order_discount
    }
 
    /// 读取客户等级标签。
    pub fn customer_tier_label(&self) -> &str {
        // 切片式暴露
        &self.customer_tier_label
    }
 
    /// 商品本金小计(所有明细行小计之和)。
    ///
    /// 从**币种的零金额**起累加,而不是从第一条明细起------
    /// 这样空订单也能正确返回零金额,无需额外分支。
    pub fn goods_subtotal(&self) -> Money {
        // 起点:本币种零金额
        let mut subtotal: Money = Money::zero(self.currency);
        // 逐行累加小计
        for item_line in &self.items {
            subtotal = subtotal.add(item_line.subtotal());
        }
        subtotal
    }
 
    /// 净额------扣除订单级折扣后的**结算基数**。
    ///
    /// # 为什么叫「结算基数」而不是「应付金额」
    /// 因为净额之后还要经过各渠道自己的税务/费用处理:
    /// - 门店渠道视为**含税价**,从中分离增值税;
    /// - 线上商城视为**不含税价**,在其上叠加增值税与平台服务费;
    /// - 免税店视为**免税价**,不做任何调整。
    ///
    /// 同一个净额,在三种渠道下演化出三种不同的客户实付。
    /// 这正是本工程要横向对照的核心现象。
    pub fn net_amount(&self) -> Money {
        // 商品本金减订单折扣
        self.goods_subtotal().subtract(self.order_discount)
    }
 
    /// 商品总件数。
    pub fn total_quantity(&self) -> u32 {
        // 逐行累加数量
        self.items.iter().map(|item_line| item_line.quantity()).sum()
    }
 
    /// 明细行数(区别于件数:一行可以有多个件)。
    pub fn item_count(&self) -> usize {
        self.items.len()
    }
 
    /// 是否发生了折扣。
    pub fn has_discount(&self) -> bool {
        // 折扣额非零即视为发生
        !self.order_discount.is_zero()
    }
 
    /// 折扣力度(折扣额占商品本金的比例)。
    pub fn discount_intensity(&self) -> Rate {
        // 委托给比率层的「部分 / 整体」换算
        Rate::from_money_ratio(self.order_discount, self.goods_subtotal())
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : item_line.rs
//! 商品明细行值对象。
 
use crate::domain::money::Money;
 
/// 商品明细行------订单中的一件商品。
///
/// ## 为什么单价与数量分开存,而不是只存小计
/// 价格标签要展示**单价**(顾客看的是单件价格),销售小票要展示
/// **数量 × 单价 = 小计**(顾客核对的是乘法是否算对)。
/// 若只存小计,标签就得反推单价,反推过程又要处理除不尽的情况
/// ------徒增一类边界问题。原始数据分开存,展示时按需计算。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ItemLine {
    /// 企业内货号
    product_code: String,
    /// 商品名称
    product_name: String,
    /// 购买数量
    quantity: u32,
    /// 单件价格
    unit_price: Money,
    /// 材质说明,例如「足金 999 / 净重 12.5g」
    material_note: String,
}
 
impl ItemLine {
    /// 构造一条商品明细行。
    ///
    /// # 参数
    /// - `product_code`:企业内货号
    /// - `product_name`:商品名称
    /// - `quantity`:购买数量
    /// - `unit_price`:单件价格
    /// - `material_note`:材质说明
    pub fn new(
        product_code: impl Into<String>,
        product_name: impl Into<String>,
        quantity: u32,
        unit_price: Money,
        material_note: impl Into<String>,
    ) -> Self {
        Self {
            // 货号
            product_code: product_code.into(),
            // 名称
            product_name: product_name.into(),
            // 数量
            quantity,
            // 单价
            unit_price,
            // 材质说明
            material_note: material_note.into(),
        }
    }
 
    /// 读取货号。
    pub fn product_code(&self) -> &str {
        // 切片式暴露,不交出所有权
        &self.product_code
    }
 
    /// 读取商品名称。
    pub fn product_name(&self) -> &str {
        // 切片式暴露
        &self.product_name
    }
 
    /// 读取数量。
    pub fn quantity(&self) -> u32 {
        // u32 是 Copy
        self.quantity
    }
 
    /// 读取单价。
    pub fn unit_price(&self) -> Money {
        // Money 是 Copy
        self.unit_price
    }
 
    /// 读取材质说明。
    pub fn material_note(&self) -> &str {
        // 切片式暴露
        &self.material_note
    }
 
    /// 读取本行小计(单价 × 数量)。
    pub fn subtotal(&self) -> Money {
        // 委托给金额的数量放大入口
        self.unit_price.multiply_by_quantity(self.quantity)
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : money.rs
//! 金额值对象(最小单位整数存储)。
 
use crate::domain::currency::Currency;
 
/// 金额------以**最小单位整数**存储(人民币的「分」、港币的「仙」)。
///
/// ## 为什么不存浮点
/// 本工程要做的事是「把同一批商品在 4 个渠道下的税负算清楚并横向对照」。
/// 这类计算包含「价内税分离」(× 13/113)这种**除不尽**的运算,
/// 用 `f64` 存储会产生 `0.30000000000000004` 式的残值,
/// 最终在「三个渠道合计是否等于应有金额」的对账步骤上暴露为 1 分的差异。
///
/// 因此:**存储用整数分,运算用 i128 中间量,结果显式四舍五入**。
///
/// ## 与 Currency 的关系
/// 金额自带币种。跨币种相加是非法的,[`Money::add`] 会当场断言失败------
/// 与其静默给出错误数字,不如让错误在最近的地方炸掉。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Money {
    /// 最小单位数量:1960000 表示 ¥19,600.00
    minor_units: i64,
    /// 计价货币
    currency: Currency,
}
 
impl Money {
    /// 以最小单位构造。
    ///
    /// # 参数
    /// - `minor_units`:最小单位数量(分)
    /// - `currency`:计价货币
    pub const fn from_minor_units(minor_units: i64, currency: Currency) -> Self {
        Self {
            // 最小单位数量
            minor_units,
            // 计价货币
            currency,
        }
    }
 
    /// 构造指定币种的零金额。
    ///
    /// # 参数
    /// - `currency`:计价货币
    ///
    /// # 为什么需要它
    /// 「零金额」在不同场景下币种不同(免税渠道的税额是零,但它必须是
    /// **人民币的零**而不是抽象零)。提供一个显式入口,可以避免上层
    /// 写 `Money::from_minor_units(0, ...)` 时到处重复传币种。
    pub const fn zero(currency: Currency) -> Self {
        Self::from_minor_units(0, currency)
    }
 
    /// 以主单位(元)构造金额。
    ///
    /// # 参数
    /// - `major_units`:主单位数量(元)
    /// - `currency`:计价货币
    ///
    /// 提供它是为了让演示数据可以直接写「9800 元」而不是「980000 分」,
    /// 降低阅读成本;内部仍立刻换算成最小单位存储。
    pub fn from_major_units(major_units: i64, currency: Currency) -> Self {
        Self::from_minor_units(
            // 元 × 每元的最小数
            major_units * currency.minor_units_per_major_unit(),
            // 计价货币
            currency,
        )
    }
 
    /// 读取最小单位数量。
    pub const fn minor_units(&self) -> i64 {
        // i64 是 Copy
        self.minor_units
    }
 
    /// 读取计价货币。
    pub const fn currency(&self) -> Currency {
        // Currency 是 Copy
        self.currency
    }
 
    /// 是否为零金额。
    pub const fn is_zero(&self) -> bool {
        // 免税渠道的税额会命中
        self.minor_units == 0
    }
 
    /// 是否为负金额。
    pub const fn is_negative(&self) -> bool {
        // 折扣、减免类明细会命中
        self.minor_units < 0
    }
 
    /// 取绝对值。
    pub const fn absolute(&self) -> Money {
        Money::from_minor_units(
            // 负数取反,非负数原样
            if self.minor_units < 0 {
                -self.minor_units
            } else {
                self.minor_units
            },
            // 币种不变
            self.currency,
        )
    }
 
    /// 同币种相加。
    ///
    /// # 参数
    /// - `other`:被加金额
    ///
    /// # Panics
    /// 币种不一致时 panic。跨币种相加是**业务错误**,
    /// 应当在使用汇率换算后才是合法操作。
    pub fn add(self, other: Money) -> Money {
        // 先校验币种
        self.assert_same_currency(other);
        Money::from_minor_units(
            // 最小单位直接相加
            self.minor_units + other.minor_units,
            // 币种保持一致
            self.currency,
        )
    }
 
    /// 同币种相减。
    ///
    /// # 参数
    /// - `other`:被减金额
    ///
    /// # Panics
    /// 币种不一致时 panic,理由同 [`Money::add`]。
    pub fn subtract(self, other: Money) -> Money {
        // 先校验币种
        self.assert_same_currency(other);
        Money::from_minor_units(
            // 最小单位直接相减
            self.minor_units - other.minor_units,
            // 币种保持一致
            self.currency,
        )
    }
 
    /// 取反(金额方向翻转)。
    ///
    /// # 为什么需要它
    /// 折扣在明细里记作**负数**是财务惯例(正负相加即得合计)。
    /// 但业务上「减免了多少」是一个正数概念,所以取反必须是显式操作,
    /// 而不是在构造时就写死负号------那样会让「减免额」本身失去正负语义。
    pub fn negate(self) -> Money {
        Money::from_minor_units(
            // 符号翻转
            -self.minor_units,
            // 币种不变
            self.currency,
        )
    }
 
    /// 按数量放大(单价 × 数量)。
    ///
    /// # 参数
    /// - `quantity`:数量
    pub fn multiply_by_quantity(self, quantity: u32) -> Money {
        Money::from_minor_units(
            // 用 i64 承接 u32,安全
            self.minor_units * i64::from(quantity),
            // 币种不变
            self.currency,
        )
    }
 
    /// 按 `numerator / denominator` 的比例缩放,结果四舍五入到最小单位。
    ///
    /// # 参数
    /// - `numerator`:比例分子
    /// - `denominator`:比例分母(必须为正)
    ///
    /// # 为什么中间量必须是 i128
    /// 「分」本身已经是放大 100 倍的量级,再乘一个万分量级的分子,
    /// i64 约 9.2×10^18 的上限在多渠道汇总后会触顶。
    /// i128 留出充分余量,且不引入任何浮点。
    ///
    /// # 为什么要有这个通用入口
    /// 价内税分离(税额 = 含税价 × 13/113)无法用「百分比」表达,
    /// 因为它的分母是 113 而不是 100。若只提供 `Rate::apply_to`,
    /// 这段最关键的税务逻辑就只能散落在各个产品实现里各写一遍。
    pub fn scale_by_ratio(self, numerator: i64, denominator: i64) -> Money {
        // 分母为零属于编码错误,直接暴露而不是静默返回零
        assert!(denominator > 0, "比例分母必须为正数,收到 {}", denominator);
        // 乘积用 i128 承接
        let product: i128 = i128::from(self.minor_units) * i128::from(numerator);
        // 四舍五入后再落回 i64
        let scaled: i128 = divide_rounded(product, i128::from(denominator));
        Money::from_minor_units(
            // 结果范围远小于 i64
            scaled as i64,
            // 币种不变
            self.currency,
        )
    }
 
    /// 格式化展示,例如 `¥19,600.00`。
    ///
    /// # 实现要点
    /// - 小数位宽度取自货币自身声明,而不是写死 2;
    /// - 千位分隔符由本地函数生成,不依赖 `format!` 的本地化能力;
    /// - 全程整数运算,不出现浮点。
    pub fn formatted(&self) -> String {
        // 每主单位的最小数(人民币为 100)
        let scale: i64 = self.currency.minor_units_per_major_unit();
        // 绝对值用于拆分整数与小数部分
        let absolute_minor: i64 = if self.minor_units < 0 {
            // 负数取反
            -self.minor_units
        } else {
            // 非负数原样
            self.minor_units
        };
        // 整数部分(元)
        let major_part: i64 = absolute_minor / scale;
        // 小数部分(分)
        let fraction_part: i64 = absolute_minor % scale;
        // 负号由符号位单独给出,避免 `-0.50` 被算成 `-0` 丢失符号
        let sign_text: &str = if self.minor_units < 0 { "-" } else { "" };
        format!(
            "{}{}{}.{:0width$}",
            // 符号
            sign_text,
            // 货币符号
            self.currency.symbol(),
            // 整数部分(含千位分隔)
            group_thousands(major_part),
            // 小数部分补零到货币声明的小数位
            fraction_part,
            width = self.currency.decimal_places() as usize
        )
    }
 
    /// 校验两个金额币种一致。
    ///
    /// # 参数
    /// - `other`:待校验的另一个金额
    fn assert_same_currency(&self, other: Money) {
        assert!(
            self.currency == other.currency,
            "币种不一致:{} 与 {} 不可直接运算",
            self.currency.code(),
            other.currency.code()
        );
    }
}
 
impl std::fmt::Display for Money {
    /// 以 [`Money::formatted`] 的格式输出,便于直接放进 `println!`。
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(formatter, "{}", self.formatted())
    }
}
 
/// 整数除法并按「四舍五入」取整(向远离零的方向进位)。
///
/// # 参数
/// - `numerator`:被除数
/// - `denominator`:除数(必须为正)
///
/// # 为什么要手写而不是用 `f64` 取整
/// `numerator as f64 / denominator as f64` 在 i128 量级上会丢失低位精度,
/// 恰好就是我们极力避免的那类误差。纯整数实现下,结果完全确定。
fn divide_rounded(numerator: i128, denominator: i128) -> i128 {
    // 半数阈值
    let half: i128 = denominator / 2;
    if numerator >= 0 {
        // 正数:加半数后向下取整
        (numerator + half) / denominator
    } else {
        // 负数:取反处理后再翻回负号,保证对称
        -((-numerator + half) / denominator)
    }
}
 
/// 给非负整数加千位分隔符(从右往左每 3 位插一个逗号)。
///
/// # 参数
/// - `value`:非负整数
fn group_thousands(value: i64) -> String {
    // 先转成十进制数字串
    let digits: String = value.to_string();
    // 总位数,用于判断当前位置右侧还剩几位
    let total_length: usize = digits.len();
    // 逐字符拼接结果
    let mut grouped: String = String::with_capacity(total_length + total_length / 3);
    for (index, character) in digits.chars().enumerate() {
        // 右侧剩余位数(含当前字符)
        let remaining: usize = total_length - index;
        // 不是首位、且右侧正好是 3 的整数倍时插入逗号
        if index > 0 && remaining % 3 == 0 {
            grouped.push(',');
        }
        // 写入当前数字
        grouped.push(character);
    }
    grouped
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : rate.rs
//! 比率值对象(万分比整数存储)。
 
use crate::domain::money::Money;
 
/// 比率------所有折扣率、税率、费率统一用**万分比整数**表示。
///
/// ## 为什么绝不用浮点
/// 本工程的业务核心是「同一批商品在不同渠道的税负差异」。
/// 这类计算要做「对中间结果再乘一个比率」,浮点误差会被逐层放大;
/// 更严重的是它**不可复现**------同一份数据两次运行的展示值可能差 1 分,
/// 对账时无从解释。
///
/// 用万分比整数 + 显式四舍五入,结果完全可复现。1300 即 13.00%。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Rate {
    /// 万分比基数:1300 表示 13.00%
    basis_points: i64,
}
 
impl Rate {
    /// 由万分比构造。
    ///
    /// # 参数
    /// - `basis_points`:万分比基数,例如 1300 表示 13.00%
    pub const fn from_basis_points(basis_points: i64) -> Self {
        Self {
            // 直接存万分比
            basis_points,
        }
    }
 
    /// 由「部分金额 / 整体金额」求出占比。
    ///
    /// # 参数
    /// - `part`:部分金额(如折扣额)
    /// - `whole`:整体金额(如商品本金)
    ///
    /// # 边界处理
    /// 整体为零时返回零比率而不是 panic------「分母为零」在报表场景里
    /// 是常态(空订单),不应让整个程序崩溃。
    pub fn from_money_ratio(part: Money, whole: Money) -> Self {
        // 分母为零直接返回零比率
        if whole.is_zero() {
            return Rate::ZERO;
        }
        // 用 i128 中间量,避免「金额 × 10000」溢出 i64
        let computed: i128 =
            i128::from(part.minor_units()) * 10_000 / i128::from(whole.minor_units());
        Rate::from_basis_points(computed as i64)
    }
 
    /// 读取万分比基数。
    pub const fn basis_points(&self) -> i64 {
        // i64 是 Copy,直接返回
        self.basis_points
    }
 
    /// 是否为零比率。
    pub const fn is_zero(&self) -> bool {
        // 免税渠道会命中这个分支
        self.basis_points == 0
    }
 
    /// 以百分比文本展示,固定保留两位小数,例如 `13.00%`。
    ///
    /// # 为什么不用 `{:.2}%` 直接格式化
    /// 因为那需要先做浮点除法 `basis_points as f64 / 100.0`,
    /// 等于把「整数存储」的努力在最后一步又交还给了浮点。
    /// 这里用整数除余直接拼字符串,全程不出现浮点。
    pub fn as_percent_text(&self) -> String {
        // 整数部分(可能为负)
        let whole_part: i64 = self.basis_points / 100;
        // 小数部分取绝对值,避免出现 `-0.50%` 之外的 `-98.-50%` 这类畸形输出
        let fraction_part: i64 = (self.basis_points % 100).abs();
        format!("{}.{:02}%", whole_part, fraction_part)
    }
 
    /// 把本比率作用到金额上(结果四舍五入到最小单位)。
    ///
    /// # 参数
    /// - `amount`:被缩放的金额
    pub fn apply_to(&self, amount: Money) -> Money {
        // 万分比天然就是「除以 10000」,委托给金额的统一缩放入口
        amount.scale_by_ratio(self.basis_points, 10_000)
    }
 
    /// 预置零比率,供免税渠道直接取用。
    pub const ZERO: Rate = Rate::from_basis_points(0);
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : document_suite_factory.rs
//! 抽象工厂:单据套件工厂。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_spec::DocumentSpec;
use crate::product::price_tag::PriceTag;
use crate::product::sales_receipt::SalesReceipt;
use crate::product::warranty_card::WarrantyCard;
 
/// 单据套件工厂------抽象工厂模式的**抽象工厂(AbstractFactory)**角色。
///
/// ## 它同时承担两件事
/// 1. **创建**:声明「本渠道能生产哪三件产品」,
///    返回的是抽象产品(`Box<dyn PriceTag>` 等)而非具体类型;
/// 2. **约束**:三件产品必然出自同一个实现。
///
/// ## 第 2 点才是抽象工厂相对「三个独立工厂」的核心增量
/// 假设不要本 trait,而是暴露三个独立的工厂函数:
/// `create_price_tag(channel, spec)`、`create_sales_receipt(channel, spec)`、
/// `create_warranty_card(channel, spec)`。那么客户端完全可能写出:
///
/// ```ignore
/// let price_tag = create_price_tag(Channel::Online, &spec);   // 线上口径
/// let warranty  = create_warranty_card(Channel::Retail, &spec); // 门店口径 ← 串味
/// ```
///
/// 编译器不会阻止它,运行期也没人会立刻发现------
/// 直到顾客拿着线上小票来主张门店的 24 个月质保。
///
/// 抽象工厂把「选渠道」这个决定**收敛为一次**:客户端拿到
/// `&dyn DocumentSuiteFactory` 之后,三件产品由同一个对象产出,
/// 「混搭」在类型层面就不再可表达。**约束是结构性的,不靠自觉。**
///
/// ## 参数为什么统一是 `&DocumentSpec`
/// 三种产品都需要「这笔交易是什么」。用同一个入参类型,
/// 让工厂实现能自然地做 `spec.clone()` 分发给三件产品,
/// 也让工程外新增工厂时不必再引入新的输入协议。
pub trait DocumentSuiteFactory {
    /// 本工厂对应的渠道编码。
    ///
    /// 客户端可用它核对「产品实际声明的渠道」是否与工厂一致
    /// (见 `analysis` 层的产品族一致性核查)。
    fn channel_code(&self) -> ChannelCode;
 
    /// 渠道展示名称(用于报表,如「内地线下门店」)。
    fn channel_label(&self) -> &'static str;
 
    /// 生产价格标签。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_price_tag(&self, specification: &DocumentSpec) -> Box<dyn PriceTag>;
 
    /// 生产销售小票。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_sales_receipt(&self, specification: &DocumentSpec) -> Box<dyn SalesReceipt>;
 
    /// 生产质保卡。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_warranty_card(&self, specification: &DocumentSpec) -> Box<dyn WarrantyCard>;
 
    /// 本工厂能生产的产品种类数。
    ///
    /// # 为什么给默认实现
    /// 「一个套件包含三件产品」是当前业务事实,但**不是模式要求**。
    /// 给它一个默认值 3,意味着将来某个渠道要额外出具一张「免税申报单」
    /// (变成四件)时,只需局部覆写,而不必让所有既有工厂跟着改。
    ///
    /// 这个默认实现只返回常量,不依赖任何 `Self` 假设,因此完全安全
    /// ------与装饰器工程里「默认方法不能充当父 trait 实现」的坑无关。
    fn product_kind_count(&self) -> usize {
        // 默认:价格标签 + 销售小票 + 质保卡
        3
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : duty_free_factory.rs
//! 具体工厂 C:免税店渠道单据套件工厂。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_spec::DocumentSpec;
use crate::factory::document_suite_factory::DocumentSuiteFactory;
use crate::product::duty_free_family::{
    DutyFreePriceTag, DutyFreeSalesReceipt, DutyFreeWarrantyCard, DUTY_FREE_STORE_CHANNEL,
};
use crate::product::price_tag::PriceTag;
use crate::product::sales_receipt::SalesReceipt;
use crate::product::warranty_card::WarrantyCard;
 
/// 免税店渠道套件工厂。
#[derive(Debug, Clone, Copy, Default)]
pub struct DutyFreeSuiteFactory;
 
impl DutyFreeSuiteFactory {
    /// 构造免税店渠道套件工厂。
    pub fn new() -> Self {
        // 零字段结构体
        Self
    }
}
 
impl DocumentSuiteFactory for DutyFreeSuiteFactory {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        DUTY_FREE_STORE_CHANNEL
    }
 
    /// 渠道展示名称。
    fn channel_label(&self) -> &'static str {
        "市内免税店"
    }
 
    /// 生产免税店价格标签。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_price_tag(&self, specification: &DocumentSpec) -> Box<dyn PriceTag> {
        // 复制规格
        Box::new(DutyFreePriceTag::new(specification.clone()))
    }
 
    /// 生产免税店销售小票。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_sales_receipt(&self, specification: &DocumentSpec) -> Box<dyn SalesReceipt> {
        // 复制规格
        Box::new(DutyFreeSalesReceipt::new(specification.clone()))
    }
 
    /// 生产免税店质保卡。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_warranty_card(&self, specification: &DocumentSpec) -> Box<dyn WarrantyCard> {
        // 复制规格
        Box::new(DutyFreeWarrantyCard::new(specification.clone()))
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : online_mall_factory.rs
//! 具体工厂 B:线上商城渠道单据套件工厂。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_spec::DocumentSpec;
use crate::factory::document_suite_factory::DocumentSuiteFactory;
use crate::product::online_mall_family::{
    OnlineMallPriceTag, OnlineMallSalesReceipt, OnlineMallWarrantyCard, ONLINE_MALL_CHANNEL,
};
use crate::product::price_tag::PriceTag;
use crate::product::sales_receipt::SalesReceipt;
use crate::product::warranty_card::WarrantyCard;
 
/// 线上商城渠道套件工厂。
#[derive(Debug, Clone, Copy, Default)]
pub struct OnlineMallSuiteFactory;
 
impl OnlineMallSuiteFactory {
    /// 构造线上商城渠道套件工厂。
    pub fn new() -> Self {
        // 零字段结构体
        Self
    }
}
 
impl DocumentSuiteFactory for OnlineMallSuiteFactory {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        ONLINE_MALL_CHANNEL
    }
 
    /// 渠道展示名称。
    fn channel_label(&self) -> &'static str {
        "线上商城"
    }
 
    /// 生产线上商城价格标签。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_price_tag(&self, specification: &DocumentSpec) -> Box<dyn PriceTag> {
        // 复制规格
        Box::new(OnlineMallPriceTag::new(specification.clone()))
    }
 
    /// 生产线上商城销售小票。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_sales_receipt(&self, specification: &DocumentSpec) -> Box<dyn SalesReceipt> {
        // 复制规格
        Box::new(OnlineMallSalesReceipt::new(specification.clone()))
    }
 
    /// 生产线上商城质保卡。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_warranty_card(&self, specification: &DocumentSpec) -> Box<dyn WarrantyCard> {
        // 复制规格
        Box::new(OnlineMallWarrantyCard::new(specification.clone()))
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : retail_store_factory.rs
//! 具体工厂 A:门店渠道单据套件工厂。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_spec::DocumentSpec;
use crate::factory::document_suite_factory::DocumentSuiteFactory;
use crate::product::price_tag::PriceTag;
use crate::product::retail_store_family::{
    RetailStorePriceTag, RetailStoreSalesReceipt, RetailStoreWarrantyCard, RETAIL_STORE_CHANNEL,
};
use crate::product::sales_receipt::SalesReceipt;
use crate::product::warranty_card::WarrantyCard;
 
/// 门店渠道套件工厂。
///
/// ## 为什么是零字段结构体
/// 工厂本身**不需要状态**------渠道的全部差异(税率、质保期、版式)
/// 都封装在它生产的具体产品里。给它加字段(如「当前门店编号」)
/// 会立刻引发一连串问题:这个编号要不要进小票?质保卡要不要带?
/// 加字段容易,想清楚它的传播后果很难。
///
/// 保持工厂无状态,等于让「渠道政策」这件事只有一个表达位置------
/// 产品族文件。这是**职责单一**在工厂层的具体体现。
#[derive(Debug, Clone, Copy, Default)]
pub struct RetailStoreSuiteFactory;
 
impl RetailStoreSuiteFactory {
    /// 构造门店渠道套件工厂。
    pub fn new() -> Self {
        // 零字段结构体,直接构造
        Self
    }
}
 
impl DocumentSuiteFactory for RetailStoreSuiteFactory {
    /// 渠道编码。
    ///
    /// 直接引用产品族文件里的常量,而不是在这里另写一遍字符串------
    /// 若两处各写一份,一旦拼写不一致,一致性核查就会永远报假警。
    fn channel_code(&self) -> ChannelCode {
        RETAIL_STORE_CHANNEL
    }
 
    /// 渠道展示名称。
    fn channel_label(&self) -> &'static str {
        "内地线下门店"
    }
 
    /// 生产门店价格标签。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_price_tag(&self, specification: &DocumentSpec) -> Box<dyn PriceTag> {
        // 复制规格:产品需要独立于工厂存活
        Box::new(RetailStorePriceTag::new(specification.clone()))
    }
 
    /// 生产门店销售小票。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_sales_receipt(&self, specification: &DocumentSpec) -> Box<dyn SalesReceipt> {
        // 复制规格
        Box::new(RetailStoreSalesReceipt::new(specification.clone()))
    }
 
    /// 生产门店质保卡。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_warranty_card(&self, specification: &DocumentSpec) -> Box<dyn WarrantyCard> {
        // 复制规格
        Box::new(RetailStoreWarrantyCard::new(specification.clone()))
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : suite_registry.rs
//! 渠道登记表------让「选工厂」从编译期决定变为运行期决定。
 
use crate::domain::channel_code::ChannelCode;
use crate::factory::document_suite_factory::DocumentSuiteFactory;
// 三个具体工厂走工厂层的统一出口引用:
// 这样「新增内置渠道」只需改本行与 with_builtin_channels,不必到处追文件路径。
use crate::factory::{DutyFreeSuiteFactory, OnlineMallSuiteFactory, RetailStoreSuiteFactory};
 
/// 登记表错误。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegistryError {
    /// 渠道编码重复登记。
    DuplicateChannelCode {
        /// 冲突的渠道编码
        channel_code: ChannelCode,
    },
}
 
impl std::fmt::Display for RegistryError {
    /// 输出可直接展示给运维人员的中文错误说明。
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            // 唯一一种错误:重复登记
            RegistryError::DuplicateChannelCode { channel_code } => write!(
                formatter,
                "渠道 {} 已在登记表中,重复登记会导致「选工厂」时结果不确定",
                channel_code.as_str()
            ),
        }
    }
}
 
impl std::error::Error for RegistryError {}
 
/// 渠道登记表------按渠道编码检索套件工厂。
///
/// ## 为什么需要它
/// 抽象工厂解决了「产品族内一致性」,但还有最后一个问题没解决:
/// **谁来选工厂**。若客户端写成
///
/// ```ignore
/// match channel_code {
///     ChannelCode::RETAIL_STORE => Box::new(RetailStoreSuiteFactory::new()),
///     ChannelCode::ONLINE_MALL  => Box::new(OnlineMallSuiteFactory::new()),
///     ChannelCode::DUTY_FREE_STORE => Box::new(DutyFreeSuiteFactory::new()),
///     _ => unreachable!(),
/// }
/// ```
///
/// 那 `main.rs` 里新增直播渠道时,这段 `match` 必须被修改,
/// 而且 `_ => unreachable!()` 会在新增渠道被真正用到时直接 panic。
/// 扩展性验证会当场失败。
///
/// 登记表把这段 `match` 提升为**数据**:每个工厂自己声明渠道编码,
/// 登记表按编码查找。新增渠道只需 `register`,不存在需要修改的分支。
///
/// ## 为什么不允许重复登记
/// 若允许,`find` 的返回值就取决于插入顺序,同一份配置在不同启动顺序下
/// 可能选出不同工厂------这类不确定性在单据系统里是不可接受的。
/// 因此构造期就拦下并给出明确错误。
pub struct SuiteRegistry {
    /// 已登记的工厂(拥有所有权)
    factories: Vec<Box<dyn DocumentSuiteFactory>>,
}
 
impl SuiteRegistry {
    /// 创建空登记表。
    pub fn new() -> Self {
        Self {
            // 空集合起步
            factories: Vec::new(),
        }
    }
 
    /// 登记一个工厂。
    ///
    /// # 参数
    /// - `factory`:套件工厂
    ///
    /// # 返回
    /// 登记成功返回 `Ok(())`;渠道编码重复返回
    /// [`RegistryError::DuplicateChannelCode`]。
    pub fn register(&mut self, factory: Box<dyn DocumentSuiteFactory>) -> Result<(), RegistryError> {
        // 先取出待登记工厂的渠道编码
        let candidate_channel_code: ChannelCode = factory.channel_code();
        // 检查是否已有同渠道工厂
        let already_registered: bool = self
            .factories
            .iter()
            .any(|existing| existing.channel_code() == candidate_channel_code);
        // 重复即拒绝
        if already_registered {
            return Err(RegistryError::DuplicateChannelCode {
                // 回传冲突的编码,便于调用方定位
                channel_code: candidate_channel_code,
            });
        }
        // 通过校验后入库
        self.factories.push(factory);
        Ok(())
    }
 
    /// 按渠道编码查找工厂。
    ///
    /// # 参数
    /// - `channel_code`:目标渠道编码
    ///
    /// # 返回
    /// 找到时返回 `&dyn DocumentSuiteFactory`,未登记时返回 `None`。
    ///
    /// # 为什么返回 `&dyn` 而不是 `&Box<dyn>`
    /// 让调用方只见抽象接口,看不到「背后是个 Box」这个实现细节。
    pub fn find(&self, channel_code: ChannelCode) -> Option<&dyn DocumentSuiteFactory> {
        self.factories
            .iter()
            // 按渠道编码匹配
            .find(|existing| existing.channel_code() == channel_code)
            // 从 &Box<dyn T> 取出 &dyn T
            .map(|existing| existing.as_ref())
    }
 
    /// 读取全部已登记工厂。
    pub fn all(&self) -> &[Box<dyn DocumentSuiteFactory>] {
        // 切片式暴露
        &self.factories
    }
 
    /// 列出全部已登记渠道编码。
    ///
    /// 分析层用它遍历渠道,从而**不需要**知道任何具体渠道的存在。
    pub fn channel_codes(&self) -> Vec<ChannelCode> {
        // 逐个取渠道编码
        self.factories
            .iter()
            .map(|existing| existing.channel_code())
            .collect()
    }
 
    /// 已登记的工厂数量。
    pub fn len(&self) -> usize {
        // 底层 Vec 的长度
        self.factories.len()
    }
 
    /// 登记表是否为空。
    pub fn is_empty(&self) -> bool {
        // 底层 Vec 是否为空
        self.factories.is_empty()
    }
 
    /// 构造一个已登记**工程内置三个渠道**的登记表。
    ///
    /// # 为什么这个方法可以放在工厂层
    /// 它引用的全是本层与 `product` 层的类型,依赖方向没有越界。
    /// 它只是把「内置渠道清单」这个事实集中到一处,
    /// 避免 `main.rs` 里手写三行 `register` 时漏掉一个渠道。
    pub fn with_builtin_channels() -> Self {
        // 空表起步
        let mut registry: SuiteRegistry = SuiteRegistry::new();
        // 登记门店渠道
        let _ = registry.register(Box::new(RetailStoreSuiteFactory::new()));
        // 登记线上商城渠道
        let _ = registry.register(Box::new(OnlineMallSuiteFactory::new()));
        // 登记免税店渠道
        let _ = registry.register(Box::new(DutyFreeSuiteFactory::new()));
        registry
    }
}
 
impl Default for SuiteRegistry {
    /// 默认构造空登记表。
    ///
    /// 刻意**不**默认登记内置渠道:默认值应当是「什么都没有」,
    /// 需要内置渠道的场景请显式调用 [`SuiteRegistry::with_builtin_channels`]。
    /// 让「登记了什么」永远是一个显式可见的决定。
    fn default() -> Self {
        Self::new()
    }
}
  
rust 复制代码
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : duty_free_family.rs
//! 市内免税店渠道产品族(ConcreteProduct 组 C)。
//!
//! ## 本渠道的定价惯例
//! - 标价即**免税价**,与顾客实付完全一致;
//! - 增值税率 **0%**(免税),小票上仍列示零税额行------
//!   这不是冗余,而是让分析层可以用统一口径遍历所有渠道,
//!   不必为「免税渠道没有税额字段」写特例分支;
//! - 无附加费用;
//! - 标准质保 **36 个月**(长于门店,是免税渠道的竞争力所在)。
//!
//! ## 这个渠道为什么必须存在
//! 抽象工厂的价值只有在「产品族之间存在实质差异」时才看得见。
//! 若三个渠道的税率、质保、费用结构完全相同,那用不用抽象工厂都一样。
//! 本渠道的 0% 税率与 36 个月质保,与线上渠道的 6% + 12 个月
//! 形成最强烈的对照,让「渠道政策差异」这一业务事实在数据上无法被忽略。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_body::DocumentBody;
use crate::domain::document_spec::DocumentSpec;
use crate::domain::money::Money;
use crate::domain::rate::Rate;
use crate::product::price_tag::PriceTag;
use crate::product::sales_receipt::SalesReceipt;
use crate::product::warranty_card::WarrantyCard;
use crate::support::date_text;
use crate::support::deterministic_code;
use crate::support::text_layout;
 
/// 免税店渠道编码。
pub const DUTY_FREE_STORE_CHANNEL: ChannelCode = ChannelCode::DUTY_FREE_STORE;
 
/// 免税店增值税率:0.00%。
///
/// 刻意保留这个「零比率」常量并让产品真的去调用它(`apply_to`),
/// 而不是直接写 `Money::zero(...)`。原因:若将来政策变化,
/// 只需把这一处改成非零值,三件产品的所有金额都会自动跟着变------
/// 而写死零的地方则不会。**零也要走统一的运算路径**。
pub const DUTY_FREE_STORE_VAT_RATE: Rate = Rate::ZERO;
 
/// 免税店标准质保期:36 个月。
pub const DUTY_FREE_STORE_WARRANTY_MONTHS: u32 = 36;
 
// ============================================================================
// 具体产品 C-1:免税店价格标签
// ============================================================================
 
/// 免税店价格标签。
pub struct DutyFreePriceTag {
    /// 单据规格副本
    specification: DocumentSpec,
}
 
impl DutyFreePriceTag {
    /// 构造免税店价格标签。
    ///
    /// # 参数
    /// - `specification`:单据规格
    pub fn new(specification: DocumentSpec) -> Self {
        Self {
            // 持有规格
            specification,
        }
    }
 
    /// 取标签主打商品的名称。
    fn primary_product_name(&self) -> String {
        self.specification
            .items()
            .first()
            .map(|item_line| item_line.product_name().to_string())
            .unwrap_or_else(|| "(本单无商品明细)".to_string())
    }
 
    /// 生成免税商品监管码。
    fn supervision_code_text(&self) -> String {
        deterministic_code::derive_digit_code(
            &format!("{}{}", self.specification.order_reference(), self.primary_product_name()),
            "76",
            16,
        )
    }
}
 
impl PriceTag for DutyFreePriceTag {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        DUTY_FREE_STORE_CHANNEL
    }
 
    /// 展示金额=免税价(等于净额)。
    fn display_amount(&self) -> Money {
        // 免税渠道不做任何税务调整
        self.specification.net_amount()
    }
 
    /// 版式名称。
    fn layout_name(&self) -> &'static str {
        // 免税价签
        "免税店价签(免税价)"
    }
 
    /// 渲染价签正文。
    fn render(&self) -> DocumentBody {
        // 键列显示宽度
        const KEY_WIDTH: usize = 16;
        // 免税价
        let duty_free_amount: Money = self.display_amount();
        // 税额恒为零(仍走统一运算路径)
        let vat_amount: Money = DUTY_FREE_STORE_VAT_RATE.apply_to(duty_free_amount);
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        // 渠道标识
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("渠道", KEY_WIDTH),
            "市内免税店(DUTY_FREE_STORE)"
        ));
        // 订单号
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("订单号", KEY_WIDTH),
            self.specification.order_reference()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 商品名称
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("商品", KEY_WIDTH),
            self.primary_product_name()
        ));
        // 监管码
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("免税监管码", KEY_WIDTH),
            self.supervision_code_text()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 免税价
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("免税价", KEY_WIDTH),
            format!("= {} =", duty_free_amount.formatted())
        ));
        // 税率说明
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("增值税", KEY_WIDTH),
            format!(
                "{}({},免征)",
                vat_amount.formatted(),
                DUTY_FREE_STORE_VAT_RATE.as_percent_text()
            )
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 购买资格提示
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("购买资格", KEY_WIDTH),
            "凭本人有效出入境证件与 60 日内离境行程单"
        ));
        // 限购提示
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("限购提示", KEY_WIDTH),
            "每人每次限购额度以海关现行规定为准"
        ));
        // 组装正文
        DocumentBody::new("价格标签 · 我的珠宝市内免税店", lines)
    }
}
 
// ============================================================================
// 具体产品 C-2:免税店销售小票
// ============================================================================
 
/// 免税店销售小票。
pub struct DutyFreeSalesReceipt {
    /// 单据规格副本
    specification: DocumentSpec,
}
 
impl DutyFreeSalesReceipt {
    /// 构造免税店销售小票。
    ///
    /// # 参数
    /// - `specification`:单据规格
    pub fn new(specification: DocumentSpec) -> Self {
        Self {
            // 持有规格
            specification,
        }
    }
}
 
impl SalesReceipt for DutyFreeSalesReceipt {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        DUTY_FREE_STORE_CHANNEL
    }
 
    /// 客户实付=免税净额(无税无费)。
    fn total_amount(&self) -> Money {
        // 免税渠道无需任何调整
        self.specification.net_amount()
    }
 
    /// 列示税额恒为零金额(本币种的零)。
    fn tax_amount(&self) -> Money {
        // 仍走统一运算路径,便于将来政策变化时一处生效
        DUTY_FREE_STORE_VAT_RATE.apply_to(self.specification.net_amount())
    }
 
    /// 是否价内税。
    ///
    /// # 为什么免税渠道返回 `true`
    /// 本方法在分析层的真实用途是回答「标价是否等于顾客实付」。
    /// 免税渠道虽然根本没有税,但其标价确实等于实付,
    /// 与门店渠道(价内税,标价即实付)在**用户感知上同类**。
    /// 若返回 `false`,对照表会把免税店归入「标价 ≠ 实付」一类,
    /// 与事实不符,反而产生误导。
    fn is_tax_inclusive(&self) -> bool {
        // 标价即实付
        true
    }
 
    /// 渲染小票正文。
    fn render(&self) -> DocumentBody {
        // 序号列宽
        const INDEX_WIDTH: usize = 6;
        // 商品名列宽
        const PRODUCT_WIDTH: usize = 30;
        // 数量列宽
        const QUANTITY_WIDTH: usize = 6;
        // 单价列宽
        const UNIT_PRICE_WIDTH: usize = 16;
        // 小计列宽
        const SUBTOTAL_WIDTH: usize = 16;
        // 合计区键列宽
        const KEY_WIDTH: usize = 26;
        // 合计区值列宽
        const VALUE_WIDTH: usize = 22;
        // 商品本金
        let goods_subtotal: Money = self.specification.goods_subtotal();
        // 订单折扣
        let order_discount: Money = self.specification.order_discount();
        // 免税净额
        let duty_free_amount: Money = self.specification.net_amount();
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        // 客户与开单信息
        lines.push(format!(
            "客户:{}({})    开单日期:{}",
            self.specification.customer_name(),
            self.specification.customer_tier_label(),
            self.specification.issue_date()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 78));
        // 表头
        lines.push(format!(
            "{}{}{}{}{}",
            text_layout::pad_right("序号", INDEX_WIDTH),
            text_layout::pad_right("商品", PRODUCT_WIDTH),
            text_layout::pad_left("数量", QUANTITY_WIDTH),
            text_layout::pad_left("单价", UNIT_PRICE_WIDTH),
            text_layout::pad_left("小计", SUBTOTAL_WIDTH)
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 78));
        // 逐条商品明细
        for (index, item_line) in self.specification.items().iter().enumerate() {
            // 序号从 1 开始
            let index_text: String = (index + 1).to_string();
            // 数量文本
            let quantity_text: String = item_line.quantity().to_string();
            // 一行明细
            lines.push(format!(
                "{}{}{}{}{}",
                text_layout::pad_right(&index_text, INDEX_WIDTH),
                text_layout::pad_right(item_line.product_name(), PRODUCT_WIDTH),
                text_layout::pad_left(&quantity_text, QUANTITY_WIDTH),
                text_layout::pad_left(&item_line.unit_price().formatted(), UNIT_PRICE_WIDTH),
                text_layout::pad_left(&item_line.subtotal().formatted(), SUBTOTAL_WIDTH)
            ));
            // 材质子行
            lines.push(format!(
                "{}{}",
                text_layout::pad_right("", INDEX_WIDTH),
                format!("↳ {}", item_line.material_note())
            ));
        }
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 78));
        // 商品本金
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("商品本金", KEY_WIDTH),
            text_layout::pad_left(&goods_subtotal.formatted(), VALUE_WIDTH)
        ));
        // 订单折扣
        if self.specification.has_discount() {
            lines.push(format!(
                "{}{}",
                text_layout::pad_right(
                    &format!(
                        "订单折扣(本单力度 {})",
                        self.specification.discount_intensity().as_percent_text()
                    ),
                    KEY_WIDTH
                ),
                text_layout::pad_left(&order_discount.negate().formatted(), VALUE_WIDTH)
            ));
        }
        // 免税净额
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("免税净额", KEY_WIDTH),
            text_layout::pad_left(&duty_free_amount.formatted(), VALUE_WIDTH)
        ));
        // 零税额行:明确列出,让顾客与稽查都看到「本单免征」
        lines.push(format!(
            "{}{}",
            text_layout::pad_right(
                &format!(
                    "增值税({})",
                    DUTY_FREE_STORE_VAT_RATE.as_percent_text()
                ),
                KEY_WIDTH
            ),
            text_layout::pad_left(&self.tax_amount().formatted(), VALUE_WIDTH)
        ));
        // 双线分隔
        lines.push(text_layout::horizontal_rule('=', 78));
        // 客户实付
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("客户实付", KEY_WIDTH),
            text_layout::pad_left(&self.total_amount().formatted(), VALUE_WIDTH)
        ));
        // 口径说明
        lines.push("免税:本单免征增值税,标价即实付,无任何附加费用。".to_string());
        // 组装正文
        DocumentBody::new("销售小票 · 我的珠宝市内免税店", lines)
    }
}
 
// ============================================================================
// 具体产品 C-3:免税店质保卡
// ============================================================================
 
/// 免税店质保卡。
pub struct DutyFreeWarrantyCard {
    /// 单据规格副本
    specification: DocumentSpec,
    /// 质保卡编号
    card_number: String,
}
 
impl DutyFreeWarrantyCard {
    /// 构造免税店质保卡。
    ///
    /// # 参数
    /// - `specification`:单据规格
    pub fn new(specification: DocumentSpec) -> Self {
        // 卡号 = 渠道前缀 + 订单号;免税渠道前缀为 WDF
        let card_number: String = format!("WDF-{}", specification.order_reference());
        Self {
            // 持有规格
            specification,
            // 缓存卡号
            card_number,
        }
    }
}
 
impl WarrantyCard for DutyFreeWarrantyCard {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        DUTY_FREE_STORE_CHANNEL
    }
 
    /// 质保月数。
    fn warranty_months(&self) -> u32 {
        // 免税渠道标准质保期(长于门店,是核心竞争力)
        DUTY_FREE_STORE_WARRANTY_MONTHS
    }
 
    /// 质保卡编号。
    fn card_number(&self) -> &str {
        // 切片式暴露
        &self.card_number
    }
 
    /// 渲染质保卡正文。
    fn render(&self) -> DocumentBody {
        // 键列显示宽度
        const KEY_WIDTH: usize = 14;
        // 商品清单
        let product_list: String = self
            .specification
            .items()
            .iter()
            .map(|item_line| {
                format!(
                    "{}(货号 {})× {}",
                    item_line.product_name(),
                    item_line.product_code(),
                    item_line.quantity()
                )
            })
            .collect::<Vec<String>>()
            .join(";");
        // 到期日
        let expiry_date: String =
            date_text::add_months(self.specification.issue_date(), self.warranty_months());
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        // 卡号
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("质保卡编号", KEY_WIDTH),
            self.card_number()
        ));
        // 持卡人
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("持卡人", KEY_WIDTH),
            self.specification.customer_name()
        ));
        // 承保商品
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("承保商品", KEY_WIDTH),
            product_list
        ));
        // 质保期
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("质保期", KEY_WIDTH),
            format!("{} 个月", self.warranty_months())
        ));
        // 起算日与到期日
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("起算日", KEY_WIDTH),
            self.specification.issue_date()
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("到期日", KEY_WIDTH),
            expiry_date
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 服务网络
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("服务网络", KEY_WIDTH),
            "免税店专柜 + 全球联保热线"
        ));
        // 条款
        lines.push(format!("{}条款:", text_layout::pad_right("", KEY_WIDTH)));
        lines.push("  · 质保范围涵盖制造工艺缺陷,不含人为损坏与正常磨损。".to_string());
        lines.push("  · 免税渠道质保期较长,凭卡可在免税店专柜享受优先服务。".to_string());
        lines.push("  · 境外维修需提前联系联保热线,凭卡号建立服务档案。".to_string());
        // 组装正文
        DocumentBody::new("质保卡 · 我的珠宝市内免税店", lines)
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : online_mall_family.rs
//! 线上商城渠道产品族(ConcreteProduct 组 B)。
//!
//! ## 本渠道的定价惯例(与门店刻意形成对照)
//! - 标价**不含税**(价外税):顾客看到的价格**不等于**最终付款额;
//! - 增值税率 6%,直接在不含税净额上叠加;
//! - 另收平台服务费 2%(同样按不含税净额计);
//! - 标准质保 12 个月(短于门店)。
//!
//! ## 为什么这个渠道是本工程最有价值的对照样本
//! 它与门店卖**同一批商品**、用**同一份净额**,但因为
//! 「计税方式(价内 vs 价外)」「费用结构(有/无附加费)」「质保期」
//! 三项渠道政策不同,最终演化出完全不同的客户实付与售后权益。
//!
//! 而抽象工厂的作用就是保证:这三个差异**必然同进同退**。
//! 客户端拿到的永远是一整套线上口径的单据,
//! 不可能出现「用了线上 6% 的税率,却配了门店 24 个月的质保」。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_body::DocumentBody;
use crate::domain::document_spec::DocumentSpec;
use crate::domain::money::Money;
use crate::domain::rate::Rate;
use crate::product::price_tag::PriceTag;
use crate::product::sales_receipt::SalesReceipt;
use crate::product::warranty_card::WarrantyCard;
use crate::support::date_text;
use crate::support::deterministic_code;
use crate::support::text_layout;
 
/// 线上商城渠道编码。
pub const ONLINE_MALL_CHANNEL: ChannelCode = ChannelCode::ONLINE_MALL;
 
/// 线上商城增值税率:6.00%(价外税)。
pub const ONLINE_MALL_VAT_RATE: Rate = Rate::from_basis_points(600);
 
/// 线上商城平台服务费率:2.00%。
pub const ONLINE_MALL_PLATFORM_FEE_RATE: Rate = Rate::from_basis_points(200);
 
/// 线上商城标准质保期:12 个月。
pub const ONLINE_MALL_WARRANTY_MONTHS: u32 = 12;
 
// ============================================================================
// 具体产品 B-1:线上商城价格标签
// ============================================================================
 
/// 线上商城价格标签(商品详情页与促销海报上的价格展示)。
pub struct OnlineMallPriceTag {
    /// 单据规格副本
    specification: DocumentSpec,
}
 
impl OnlineMallPriceTag {
    /// 构造线上商城价格标签。
    ///
    /// # 参数
    /// - `specification`:单据规格
    pub fn new(specification: DocumentSpec) -> Self {
        Self {
            // 持有规格
            specification,
        }
    }
 
    /// 取标签主打商品的名称。
    fn primary_product_name(&self) -> String {
        self.specification
            .items()
            .first()
            .map(|item_line| item_line.product_name().to_string())
            .unwrap_or_else(|| "(本单无商品明细)".to_string())
    }
 
    /// 取标签主打商品的货号。
    fn primary_product_code(&self) -> String {
        self.specification
            .items()
            .first()
            .map(|item_line| item_line.product_code().to_string())
            .unwrap_or_else(|| "-".to_string())
    }
 
    /// 生成页面 SKU 编码。
    ///
    /// 复用了条码派生的同一套确定性算法,但换了前缀------
    /// 线上 SKU 与线下条码必须**可区分**,否则库存系统会误判为同一实体。
    fn sku_text(&self) -> String {
        deterministic_code::derive_digit_code(
            &format!("{}{}", self.primary_product_code(), self.specification.order_reference()),
            "88",
            12,
        )
    }
}
 
impl PriceTag for OnlineMallPriceTag {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        ONLINE_MALL_CHANNEL
    }
 
    /// 展示金额=**不含税价**(线上惯例:详情页标净价,税费另计)。
    fn display_amount(&self) -> Money {
        // 线上净额视为不含税价
        self.specification.net_amount()
    }
 
    /// 版式名称。
    fn layout_name(&self) -> &'static str {
        // 明确标注口径,避免与门店价签混淆
        "线上详情页价签(不含税价)"
    }
 
    /// 渲染价签正文。
    fn render(&self) -> DocumentBody {
        // 键列显示宽度
        const KEY_WIDTH: usize = 16;
        // 不含税标价
        let excluding_tax_amount: Money = self.display_amount();
        // 预估增值税
        let estimated_vat_amount: Money = ONLINE_MALL_VAT_RATE.apply_to(excluding_tax_amount);
        // 预估平台服务费
        let estimated_platform_fee: Money = ONLINE_MALL_PLATFORM_FEE_RATE.apply_to(excluding_tax_amount);
        // 预估实付
        let estimated_total: Money = excluding_tax_amount
            .add(estimated_vat_amount)
            .add(estimated_platform_fee);
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        // 渠道标识
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("渠道", KEY_WIDTH),
            "线上商城(ONLINE_MALL)"
        ));
        // 订单号
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("订单号", KEY_WIDTH),
            self.specification.order_reference()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 商品名称
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("商品", KEY_WIDTH),
            self.primary_product_name()
        ));
        // 页面 SKU
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("页面 SKU", KEY_WIDTH),
            self.sku_text()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 不含税标价
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("页面标价", KEY_WIDTH),
            format!("= {} =", excluding_tax_amount.formatted())
        ));
        // 口径说明
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("标价口径", KEY_WIDTH),
            "不含税价,税费与服务费于结算时另计"
        ));
        // 预估税额
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("预估增值税", KEY_WIDTH),
            format!(
                "{}({})",
                estimated_vat_amount.formatted(),
                ONLINE_MALL_VAT_RATE.as_percent_text()
            )
        ));
        // 预估平台服务费
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("预估服务费", KEY_WIDTH),
            format!(
                "{}({})",
                estimated_platform_fee.formatted(),
                ONLINE_MALL_PLATFORM_FEE_RATE.as_percent_text()
            )
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 预估实付
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("结算预估实付", KEY_WIDTH),
            format!("= {} =", estimated_total.formatted())
        ));
        // 组装正文
        DocumentBody::new("价格标签 · 我的珠宝线上商城", lines)
    }
}
 
// ============================================================================
// 具体产品 B-2:线上商城销售小票
// ============================================================================
 
/// 线上商城销售小票(电子发票附属的结算明细)。
pub struct OnlineMallSalesReceipt {
    /// 单据规格副本
    specification: DocumentSpec,
}
 
impl OnlineMallSalesReceipt {
    /// 构造线上商城销售小票。
    ///
    /// # 参数
    /// - `specification`:单据规格
    pub fn new(specification: DocumentSpec) -> Self {
        Self {
            // 持有规格
            specification,
        }
    }
}
 
impl SalesReceipt for OnlineMallSalesReceipt {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        ONLINE_MALL_CHANNEL
    }
 
    /// 客户实付合计=不含税净额 + 增值税 + 平台服务费。
    fn total_amount(&self) -> Money {
        // 不含税净额
        let excluding_tax_amount: Money = self.specification.net_amount();
        // 含税合计 = 净额 + 税 + 费
        excluding_tax_amount
            .add(self.tax_amount())
            .add(self.surcharge_amount())
    }
 
    /// 列示的增值税额(在不含税净额上按 6% 计)。
    fn tax_amount(&self) -> Money {
        // 价外税:直接对净额乘税率
        ONLINE_MALL_VAT_RATE.apply_to(self.specification.net_amount())
    }
 
    /// 列示的平台服务费(在不含税净额上按 2% 计)。
    ///
    /// 覆写了 trait 的默认实现------这是抽象产品允许的行为:
    /// 默认值表达「大多数渠道无附加费」这一常规,
    /// 个别渠道按自己的政策覆盖即可,无需强迫所有实现者都写一遍。
    fn surcharge_amount(&self) -> Money {
        // 按不含税净额计费
        ONLINE_MALL_PLATFORM_FEE_RATE.apply_to(self.specification.net_amount())
    }
 
    /// 线上渠道为价外税。
    fn is_tax_inclusive(&self) -> bool {
        // 标价不含税
        false
    }
 
    /// 渲染小票正文。
    fn render(&self) -> DocumentBody {
        // 序号列宽
        const INDEX_WIDTH: usize = 6;
        // 商品名列宽
        const PRODUCT_WIDTH: usize = 30;
        // 数量列宽
        const QUANTITY_WIDTH: usize = 6;
        // 单价列宽
        const UNIT_PRICE_WIDTH: usize = 16;
        // 小计列宽
        const SUBTOTAL_WIDTH: usize = 16;
        // 合计区键列宽
        const KEY_WIDTH: usize = 26;
        // 合计区值列宽
        const VALUE_WIDTH: usize = 22;
        // 商品本金(不含税)
        let goods_subtotal: Money = self.specification.goods_subtotal();
        // 订单折扣
        let order_discount: Money = self.specification.order_discount();
        // 不含税净额
        let excluding_tax_amount: Money = self.specification.net_amount();
        // 增值税
        let vat_amount: Money = self.tax_amount();
        // 平台服务费
        let platform_fee: Money = self.surcharge_amount();
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        // 客户与开单信息
        lines.push(format!(
            "客户:{}({})    开单日期:{}",
            self.specification.customer_name(),
            self.specification.customer_tier_label(),
            self.specification.issue_date()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 78));
        // 表头
        lines.push(format!(
            "{}{}{}{}{}",
            text_layout::pad_right("序号", INDEX_WIDTH),
            text_layout::pad_right("商品", PRODUCT_WIDTH),
            text_layout::pad_left("数量", QUANTITY_WIDTH),
            text_layout::pad_left("单价", UNIT_PRICE_WIDTH),
            text_layout::pad_left("小计", SUBTOTAL_WIDTH)
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 78));
        // 逐条商品明细
        for (index, item_line) in self.specification.items().iter().enumerate() {
            // 序号从 1 开始
            let index_text: String = (index + 1).to_string();
            // 数量文本
            let quantity_text: String = item_line.quantity().to_string();
            // 一行明细
            lines.push(format!(
                "{}{}{}{}{}",
                text_layout::pad_right(&index_text, INDEX_WIDTH),
                text_layout::pad_right(item_line.product_name(), PRODUCT_WIDTH),
                text_layout::pad_left(&quantity_text, QUANTITY_WIDTH),
                text_layout::pad_left(&item_line.unit_price().formatted(), UNIT_PRICE_WIDTH),
                text_layout::pad_left(&item_line.subtotal().formatted(), SUBTOTAL_WIDTH)
            ));
            // 材质子行
            lines.push(format!(
                "{}{}",
                text_layout::pad_right("", INDEX_WIDTH),
                format!("↳ {}", item_line.material_note())
            ));
        }
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 78));
        // 商品本金(不含税)
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("商品本金(不含税)", KEY_WIDTH),
            text_layout::pad_left(&goods_subtotal.formatted(), VALUE_WIDTH)
        ));
        // 订单折扣
        if self.specification.has_discount() {
            lines.push(format!(
                "{}{}",
                text_layout::pad_right(
                    &format!(
                        "订单折扣(本单力度 {})",
                        self.specification.discount_intensity().as_percent_text()
                    ),
                    KEY_WIDTH
                ),
                text_layout::pad_left(&order_discount.negate().formatted(), VALUE_WIDTH)
            ));
        }
        // 不含税净额
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("不含税净额", KEY_WIDTH),
            text_layout::pad_left(&excluding_tax_amount.formatted(), VALUE_WIDTH)
        ));
        // 增值税(价外)
        lines.push(format!(
            "{}{}",
            text_layout::pad_right(
                &format!("加:增值税 {}", ONLINE_MALL_VAT_RATE.as_percent_text()),
                KEY_WIDTH
            ),
            text_layout::pad_left(&vat_amount.formatted(), VALUE_WIDTH)
        ));
        // 平台服务费(价外)
        lines.push(format!(
            "{}{}",
            text_layout::pad_right(
                &format!(
                    "加:平台服务费 {}",
                    ONLINE_MALL_PLATFORM_FEE_RATE.as_percent_text()
                ),
                KEY_WIDTH
            ),
            text_layout::pad_left(&platform_fee.formatted(), VALUE_WIDTH)
        ));
        // 双线分隔
        lines.push(text_layout::horizontal_rule('=', 78));
        // 客户实付
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("客户实付", KEY_WIDTH),
            text_layout::pad_left(&self.total_amount().formatted(), VALUE_WIDTH)
        ));
        // 口径说明:价外税的核心特征
        lines.push(format!(
            "价外税:页面标价 {},实付 {},差额 {} 全部来自税费与平台服务费。",
            excluding_tax_amount.formatted(),
            self.total_amount().formatted(),
            self.total_amount().subtract(excluding_tax_amount).formatted()
        ));
        // 组装正文
        DocumentBody::new("销售小票 · 我的珠宝线上商城", lines)
    }
}
 
// ============================================================================
// 具体产品 B-3:线上商城质保卡
// ============================================================================
 
/// 线上商城质保卡(电子质保卡)。
pub struct OnlineMallWarrantyCard {
    /// 单据规格副本
    specification: DocumentSpec,
    /// 质保卡编号
    card_number: String,
}
 
impl OnlineMallWarrantyCard {
    /// 构造线上商城质保卡。
    ///
    /// # 参数
    /// - `specification`:单据规格
    pub fn new(specification: DocumentSpec) -> Self {
        // 卡号 = 渠道前缀 + 订单号;线上渠道前缀为 WON
        let card_number: String = format!("WON-{}", specification.order_reference());
        Self {
            // 持有规格
            specification,
            // 缓存卡号
            card_number,
        }
    }
}
 
impl WarrantyCard for OnlineMallWarrantyCard {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        ONLINE_MALL_CHANNEL
    }
 
    /// 质保月数。
    fn warranty_months(&self) -> u32 {
        // 线上标准质保期(短于门店)
        ONLINE_MALL_WARRANTY_MONTHS
    }
 
    /// 质保卡编号。
    fn card_number(&self) -> &str {
        // 切片式暴露
        &self.card_number
    }
 
    /// 渲染质保卡正文。
    fn render(&self) -> DocumentBody {
        // 键列显示宽度
        const KEY_WIDTH: usize = 14;
        // 商品清单
        let product_list: String = self
            .specification
            .items()
            .iter()
            .map(|item_line| {
                format!(
                    "{}(货号 {})× {}",
                    item_line.product_name(),
                    item_line.product_code(),
                    item_line.quantity()
                )
            })
            .collect::<Vec<String>>()
            .join(";");
        // 到期日
        let expiry_date: String =
            date_text::add_months(self.specification.issue_date(), self.warranty_months());
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        // 卡号
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("质保卡编号", KEY_WIDTH),
            self.card_number()
        ));
        // 持卡人
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("持卡人", KEY_WIDTH),
            self.specification.customer_name()
        ));
        // 承保商品
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("承保商品", KEY_WIDTH),
            product_list
        ));
        // 质保期
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("质保期", KEY_WIDTH),
            format!("{} 个月", self.warranty_months())
        ));
        // 起算日与到期日
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("起算日", KEY_WIDTH),
            self.specification.issue_date()
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("到期日", KEY_WIDTH),
            expiry_date
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 服务网络
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("服务网络", KEY_WIDTH),
            "线上客服寄修通道(不支持门店通兑)"
        ));
        // 条款
        lines.push(format!("{}条款:", text_layout::pad_right("", KEY_WIDTH)));
        lines.push("  · 质保范围涵盖制造工艺缺陷,不含人为损坏与正常磨损。".to_string());
        lines.push("  · 寄修需先在线上客服登记,凭电子质保卡号发起工单。".to_string());
        lines.push("  · 保质期短于门店渠道,如需门店通兑请选择线下购买。".to_string());
        // 组装正文
        DocumentBody::new("质保卡 · 我的珠宝线上商城", lines)
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : price_tag.rs
//! 抽象产品 A:价格标签。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_body::DocumentBody;
use crate::domain::money::Money;
 
/// 价格标签------贴在商品上的价签。
///
/// ## 抽象产品角色的职责边界
/// 本 trait 只回答三个问题:
/// 1. 你属于哪个渠道?([`PriceTag::channel_code`])
/// 2. 你正面展示多少钱?([`PriceTag::display_amount`])
/// 3. 把你渲染成文本长什么样?([`PriceTag::render`])
///
/// **不回答**「这钱是含税还是不含税」------那是渠道的定价惯例,
/// 已经体现在 `display_amount` 的口径里,并由各实现自行在渲染时说明。
/// 若把它提升为 trait 方法,就等于要求所有渠道都必须往「含税/不含税」
/// 这个二元划分上靠,反而限制了未来的渠道形态(例如「按日金价浮动」)。
///
/// ## 客户端如何使用
/// 客户端只持有 `Box<dyn PriceTag>`。它无法调用任何渠道特有方法,
/// 因此**不可能**写出「在门店标签上调用线上商城的限时价逻辑」这种串味代码。
/// 这就是抽象产品带来的编译期保护。
pub trait PriceTag {
    /// 本标签所属的渠道编码。
    ///
    /// 存在的意义是**一致性自检**:客户端可以核对三件产品的渠道码是否一致,
    /// 从而在运行期发现「产品族被拼错了」这类问题(见 `analysis` 层的核查器)。
    fn channel_code(&self) -> ChannelCode;
 
    /// 标签正面展示的金额。
    ///
    /// 口径由渠道决定:
    /// - 门店:含税零售价
    /// - 线上商城:不含税价(税费结算时另计)
    /// - 免税店:免税价
    fn display_amount(&self) -> Money;
 
    /// 渲染为单据正文。
    fn render(&self) -> DocumentBody;
 
    /// 标签版式名称。
    ///
    /// # 为什么这里可以用默认方法,而装饰器模式里不行
    /// 上一版装饰器工程踩过的坑是:**trait 默认方法不能充当父 trait 的实现**
    /// (子 trait 实现者仍被迫逐一重写转发方法)。
    /// 但这里是**独立的普通方法**,不存在「充当别的 trait 的实现」这回事;
    /// 而且它的返回值是 `&'static str`,不涉及 `Self: Sized` 假设,
    /// 因此默认实现完全安全且确实省掉了三个渠道各写一遍。
    ///
    /// 判断标准很简单:默认方法是否只依赖 trait 已声明的方法或常量?
    /// 这里它连已声明方法都不依赖,纯粹是常量,属最安全的一类。
    fn layout_name(&self) -> &'static str {
        // 通用默认版式名
        "标准价格标签"
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : retail_store_family.rs
//! 门店渠道产品族(ConcreteProduct 组 A)。
//!
//! ## 为什么三件产品写在同一个文件里,而不是拆成三个文件
//! 它们共享同一组渠道参数:渠道编码、增值税率、价内税分母、质保月数。
//! 若拆成三个文件,「门店增值税是 13%」这个事实就会被复制到三个地方,
//! 一旦税务政策调整(13% → 12%)就必须同步修改三处,
//! 漏改一处就会产出「价签按 13% 分离、小票按 12% 分离」的自相矛盾单据。
//!
//! **一个文件 = 一个产品族 = 一个内聚单元。** 这是抽象工厂模式在
//! 代码组织上最自然的落地方式:族的边界与文件的边界重合。
//!
//! ## 本渠道的定价惯例
//! - 标价**含税**(价内税),顾客看到的价格就是最终付款额;
//! - 增值税率 13%,从含税价中反向分离(税额 = 含税价 × 13/113);
//! - 无附加费用;
//! - 标准质保 24 个月。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_body::DocumentBody;
use crate::domain::document_spec::DocumentSpec;
use crate::domain::money::Money;
use crate::domain::rate::Rate;
use crate::product::price_tag::PriceTag;
use crate::product::sales_receipt::SalesReceipt;
use crate::product::warranty_card::WarrantyCard;
use crate::support::date_text;
use crate::support::deterministic_code;
use crate::support::text_layout;
 
/// 门店渠道编码。
pub const RETAIL_STORE_CHANNEL: ChannelCode = ChannelCode::RETAIL_STORE;
 
/// 门店渠道增值税率:13.00%。
pub const RETAIL_STORE_VAT_RATE: Rate = Rate::from_basis_points(1_300);
 
/// 价内税分离时使用的分子(13%)。
const RETAIL_STORE_VAT_NUMERATOR_BASIS_POINTS: i64 = 1_300;
 
/// 价内税分离时使用的分母(100% + 13% = 113%)。
///
/// 价内税的关键在于:含税价 = 不含税价 × (1 + 税率)。
/// 因此反推税额时,分母是 **113** 而不是 100------
/// 用 `Rate::apply_to`(分母固定 10000)算出来的会是「含税价的 13%」,
/// 比真实税额多出约 13%,这是最容易写错的一处。
const RETAIL_STORE_VAT_DENOMINATOR_BASIS_POINTS: i64 = 11_300;
 
/// 门店渠道标准质保期:24 个月。
pub const RETAIL_STORE_WARRANTY_MONTHS: u32 = 24;
 
/// 从门店含税净额中分离出增值税额。
///
/// # 参数
/// - `tax_inclusive_amount`:含税金额(门店净额即含税价)
fn separate_retail_vat(tax_inclusive_amount: Money) -> Money {
    // 税额 = 含税价 × 13 / 113
    tax_inclusive_amount.scale_by_ratio(
        // 分子:税率万分比
        RETAIL_STORE_VAT_NUMERATOR_BASIS_POINTS,
        // 分母:1 + 税率
        RETAIL_STORE_VAT_DENOMINATOR_BASIS_POINTS,
    )
}
 
// ============================================================================
// 具体产品 A-1:门店柜台价格标签
// ============================================================================
 
/// 门店柜台价格标签。
///
/// 持有单据规格的**副本**,而不是引用------因为产品需要独立于工厂存活
/// (客户端拿到套件后,工厂可能已被释放)。规格本身很小(若干商品行),
/// 复制的成本远低于引入生命周期参数带来的复杂度。
pub struct RetailStorePriceTag {
    /// 单据规格副本
    specification: DocumentSpec,
}
 
impl RetailStorePriceTag {
    /// 构造门店价格标签。
    ///
    /// # 参数
    /// - `specification`:单据规格
    pub fn new(specification: DocumentSpec) -> Self {
        Self {
            // 持有规格
            specification,
        }
    }
 
    /// 取标签主打商品(第一件)的名称。
    ///
    /// 门店价签一张贴一件商品,因此只取首件;
    /// 空订单是合法状态,返回占位文本而不是 panic。
    fn primary_product_name(&self) -> String {
        self.specification
            .items()
            .first()
            .map(|item_line| item_line.product_name().to_string())
            .unwrap_or_else(|| "(本单无商品明细)".to_string())
    }
 
    /// 取标签主打商品的货号。
    fn primary_product_code(&self) -> String {
        self.specification
            .items()
            .first()
            .map(|item_line| item_line.product_code().to_string())
            .unwrap_or_else(|| "-".to_string())
    }
 
    /// 取标签主打商品的材质说明。
    fn primary_material_note(&self) -> String {
        self.specification
            .items()
            .first()
            .map(|item_line| item_line.material_note().to_string())
            .unwrap_or_else(|| "-".to_string())
    }
 
    /// 生成价签条码(13 位,中国商品条码前缀 690)。
    fn barcode_text(&self) -> String {
        // 以货号 + 订单号为派生源,保证同一笔交易条码稳定
        deterministic_code::derive_digit_code(
            &format!("{}{}", self.primary_product_code(), self.specification.order_reference()),
            "690",
            13,
        )
    }
}
 
impl PriceTag for RetailStorePriceTag {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        RETAIL_STORE_CHANNEL
    }
 
    /// 展示金额=含税零售价。
    fn display_amount(&self) -> Money {
        // 门店净额即含税价
        self.specification.net_amount()
    }
 
    /// 版式名称。
    fn layout_name(&self) -> &'static str {
        // 覆盖默认版式名,说明本渠道的价格口径
        "门店柜台价签(含税价)"
    }
 
    /// 渲染价签正文。
    fn render(&self) -> DocumentBody {
        // 键列显示宽度(中文键最长 6 字 = 12 列,留余量)
        const KEY_WIDTH: usize = 16;
        // 含税零售价
        let tax_inclusive_amount: Money = self.display_amount();
        // 分离出的增值税额
        let vat_amount: Money = separate_retail_vat(tax_inclusive_amount);
        // 不含税价
        let excluding_tax_amount: Money = tax_inclusive_amount.subtract(vat_amount);
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        // 渠道与订单标识
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("渠道", KEY_WIDTH),
            "内地线下门店(RETAIL_STORE)"
        ));
        // 订单号
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("订单号", KEY_WIDTH),
            self.specification.order_reference()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 商品名称
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("商品", KEY_WIDTH),
            self.primary_product_name()
        ));
        // 货号
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("货号", KEY_WIDTH),
            self.primary_product_code()
        ));
        // 材质
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("材质", KEY_WIDTH),
            self.primary_material_note()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 含税零售价(价签主角,用 = 强调)
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("含税零售价", KEY_WIDTH),
            format!("= {} =", tax_inclusive_amount.formatted())
        ));
        // 其中增值税
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("其中增值税", KEY_WIDTH),
            format!(
                "{}({} 价内税)",
                vat_amount.formatted(),
                RETAIL_STORE_VAT_RATE.as_percent_text()
            )
        ));
        // 不含税价
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("不含税价", KEY_WIDTH),
            excluding_tax_amount.formatted()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 条码
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("商品条码", KEY_WIDTH),
            self.barcode_text()
        ));
        // 口径提示:让顾客一眼知道看到的就是实付价
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("口径提示", KEY_WIDTH),
            "本价签为含税价,结算时无需另行加税。"
        ));
        // 组装正文
        DocumentBody::new("价格标签 · 我的珠宝内地门店", lines)
    }
}
 
// ============================================================================
// 具体产品 A-2:门店销售小票
// ============================================================================
 
/// 门店销售小票。
pub struct RetailStoreSalesReceipt {
    /// 单据规格副本
    specification: DocumentSpec,
}
 
impl RetailStoreSalesReceipt {
    /// 构造门店销售小票。
    ///
    /// # 参数
    /// - `specification`:单据规格
    pub fn new(specification: DocumentSpec) -> Self {
        Self {
            // 持有规格
            specification,
        }
    }
}
 
impl SalesReceipt for RetailStoreSalesReceipt {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        RETAIL_STORE_CHANNEL
    }
 
    /// 客户实付合计=含税净额(价内税,标价即实付)。
    fn total_amount(&self) -> Money {
        // 门店无附加费,实付即净额
        self.specification.net_amount()
    }
 
    /// 小票列示的增值税额。
    fn tax_amount(&self) -> Money {
        // 从含税净额中分离
        separate_retail_vat(self.specification.net_amount())
    }
 
    /// 门店渠道为价内税。
    fn is_tax_inclusive(&self) -> bool {
        // 标价已含税
        true
    }
 
    /// 渲染小票正文。
    fn render(&self) -> DocumentBody {
        // 序号列宽
        const INDEX_WIDTH: usize = 6;
        // 商品名列宽(中文,按显示列计)
        const PRODUCT_WIDTH: usize = 30;
        // 数量列宽
        const QUANTITY_WIDTH: usize = 6;
        // 单价列宽
        const UNIT_PRICE_WIDTH: usize = 16;
        // 小计列宽
        const SUBTOTAL_WIDTH: usize = 16;
        // 键列宽(用于合计区)
        const KEY_WIDTH: usize = 26;
        // 值列宽
        const VALUE_WIDTH: usize = 22;
        // 商品本金
        let goods_subtotal: Money = self.specification.goods_subtotal();
        // 订单折扣
        let order_discount: Money = self.specification.order_discount();
        // 含税净额
        let tax_inclusive_amount: Money = self.specification.net_amount();
        // 税额
        let vat_amount: Money = self.tax_amount();
        // 不含税价
        let excluding_tax_amount: Money = tax_inclusive_amount.subtract(vat_amount);
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        // 客户与开单信息
        lines.push(format!(
            "客户:{}({})    开单日期:{}",
            self.specification.customer_name(),
            self.specification.customer_tier_label(),
            self.specification.issue_date()
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 78));
        // 表头
        lines.push(format!(
            "{}{}{}{}{}",
            text_layout::pad_right("序号", INDEX_WIDTH),
            text_layout::pad_right("商品", PRODUCT_WIDTH),
            text_layout::pad_left("数量", QUANTITY_WIDTH),
            text_layout::pad_left("单价", UNIT_PRICE_WIDTH),
            text_layout::pad_left("小计", SUBTOTAL_WIDTH)
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 78));
        // 逐条商品明细
        for (index, item_line) in self.specification.items().iter().enumerate() {
            // 序号从 1 开始
            let index_text: String = (index + 1).to_string();
            // 数量文本
            let quantity_text: String = item_line.quantity().to_string();
            // 一行明细
            lines.push(format!(
                "{}{}{}{}{}",
                text_layout::pad_right(&index_text, INDEX_WIDTH),
                text_layout::pad_right(item_line.product_name(), PRODUCT_WIDTH),
                text_layout::pad_left(&quantity_text, QUANTITY_WIDTH),
                text_layout::pad_left(&item_line.unit_price().formatted(), UNIT_PRICE_WIDTH),
                text_layout::pad_left(&item_line.subtotal().formatted(), SUBTOTAL_WIDTH)
            ));
            // 材质说明作为子行,缩进对齐到商品列,便于顾客核对成色
            lines.push(format!(
                "{}{}",
                text_layout::pad_right("", INDEX_WIDTH),
                format!("↳ {}", item_line.material_note())
            ));
        }
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 78));
        // 商品本金
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("商品本金", KEY_WIDTH),
            text_layout::pad_left(&goods_subtotal.formatted(), VALUE_WIDTH)
        ));
        // 订单折扣(仅在有折扣时展示,避免出现 -¥0.00 这种噪音行)
        if self.specification.has_discount() {
            lines.push(format!(
                "{}{}",
                text_layout::pad_right(
                    &format!(
                        "订单折扣(本单力度 {})",
                        self.specification.discount_intensity().as_percent_text()
                    ),
                    KEY_WIDTH
                ),
                text_layout::pad_left(&order_discount.negate().formatted(), VALUE_WIDTH)
            ));
        }
        // 价税合计
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("价税合计(含税)", KEY_WIDTH),
            text_layout::pad_left(&tax_inclusive_amount.formatted(), VALUE_WIDTH)
        ));
        // 其中增值税
        lines.push(format!(
            "{}{}",
            text_layout::pad_right(
                &format!(" 其中 增值税 {}", RETAIL_STORE_VAT_RATE.as_percent_text()),
                KEY_WIDTH
            ),
            text_layout::pad_left(&vat_amount.formatted(), VALUE_WIDTH)
        ));
        // 不含税价
        lines.push(format!(
            "{}{}",
            text_layout::pad_right(" 不含税价", KEY_WIDTH),
            text_layout::pad_left(&excluding_tax_amount.formatted(), VALUE_WIDTH)
        ));
        // 双线分隔,标记合计区结束
        lines.push(text_layout::horizontal_rule('=', 78));
        // 客户实付
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("客户实付", KEY_WIDTH),
            text_layout::pad_left(&self.total_amount().formatted(), VALUE_WIDTH)
        ));
        // 口径说明:价内税的核心特征
        lines.push("价内税:标价即实付,顾客无需另行支付税款。".to_string());
        // 组装正文
        DocumentBody::new("销售小票 · 我的珠宝内地门店", lines)
    }
}
 
// ============================================================================
// 具体产品 A-3:门店质保卡
// ============================================================================
 
/// 门店质保卡。
pub struct RetailStoreWarrantyCard {
    /// 单据规格副本
    specification: DocumentSpec,
    /// 质保卡编号(构造时一次性生成)
    card_number: String,
}
 
impl RetailStoreWarrantyCard {
    /// 构造门店质保卡。
    ///
    /// # 参数
    /// - `specification`:单据规格
    ///
    /// 卡号在构造时确定并缓存,而不是每次读取时重算------
    /// 卡号会同时出现在正文与服务系统里,重算意味着两处可能因
    /// 上游数据变化而分叉。
    pub fn new(specification: DocumentSpec) -> Self {
        // 卡号 = 渠道前缀 + 订单号;门店渠道前缀为 WRT
        let card_number: String = format!("WRT-{}", specification.order_reference());
        Self {
            // 持有规格
            specification,
            // 缓存卡号
            card_number,
        }
    }
}
 
impl WarrantyCard for RetailStoreWarrantyCard {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        RETAIL_STORE_CHANNEL
    }
 
    /// 质保月数。
    fn warranty_months(&self) -> u32 {
        // 门店标准质保期
        RETAIL_STORE_WARRANTY_MONTHS
    }
 
    /// 质保卡编号。
    fn card_number(&self) -> &str {
        // 切片式暴露
        &self.card_number
    }
 
    /// 渲染质保卡正文。
    fn render(&self) -> DocumentBody {
        // 键列显示宽度
        const KEY_WIDTH: usize = 14;
        // 商品清单(多件商品全部列出,避免只保首件引发争议)
        let product_list: String = self
            .specification
            .items()
            .iter()
            .map(|item_line| {
                format!(
                    "{}(货号 {})× {}",
                    item_line.product_name(),
                    item_line.product_code(),
                    item_line.quantity()
                )
            })
            .collect::<Vec<String>>()
            .join(";");
        // 到期日 = 开单日期 + 质保月数
        let expiry_date: String =
            date_text::add_months(self.specification.issue_date(), self.warranty_months());
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        // 卡号
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("质保卡编号", KEY_WIDTH),
            self.card_number()
        ));
        // 持卡人
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("持卡人", KEY_WIDTH),
            self.specification.customer_name()
        ));
        // 承保商品
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("承保商品", KEY_WIDTH),
            product_list
        ));
        // 质保期
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("质保期", KEY_WIDTH),
            format!("{} 个月", self.warranty_months())
        ));
        // 起算日与到期日
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("起算日", KEY_WIDTH),
            self.specification.issue_date()
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("到期日", KEY_WIDTH),
            expiry_date
        ));
        // 分隔线
        lines.push(text_layout::horizontal_rule('-', 58));
        // 服务网络
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("服务网络", KEY_WIDTH),
            "我的珠宝内地门店全网络通兑"
        ));
        // 条款
        lines.push(format!("{}条款:", text_layout::pad_right("", KEY_WIDTH)));
        lines.push("  · 质保范围涵盖制造工艺缺陷,不含人为损坏与正常磨损。".to_string());
        lines.push("  · 凭本卡与原始销售小票,可在任一内地门店享受免费清洗与保养。".to_string());
        lines.push("  · 质保期内非人为损坏,享免费维修;超出范围按门店报价处理。".to_string());
        // 组装正文
        DocumentBody::new("质保卡 · 我的珠宝内地门店", lines)
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : sales_receipt.rs
//! 抽象产品 B:销售小票。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_body::DocumentBody;
use crate::domain::money::Money;
 
/// 销售小票------顾客离店时拿到的那张凭据。
///
/// ## 为什么把「税额」「附加费」提升为 trait 方法
/// 本工程的分析层要做**跨渠道税负对照**(核心业务问题:
/// 「同一件商品,为什么线上比门店贵」)。要回答它,
/// 分析层必须能从任意渠道的小票上取到税额与附加费。
///
/// 如果这些信息只藏在渲染出来的文本里,分析层就只能去解析字符串------
/// 这是典型的「把结构信息降维成文本再逆向还原」,脆弱且不可维护。
/// 把口径提升为方法,等于把「可分析性」写进了契约。
///
/// ## 关于默认方法
/// [`SalesReceipt::surcharge_amount`] 给了默认实现(返回零金额)。
/// 门店与免税店没有附加费,直接用默认值即可;
/// 线上商城与直播渠道会覆写它。这与装饰器工程里的陷阱无关------
/// 这里默认实现只依赖**已声明的方法** `total_amount()`,是合法且安全的。
pub trait SalesReceipt {
    /// 本小票所属的渠道编码。
    fn channel_code(&self) -> ChannelCode;
 
    /// 客户实付合计。
    ///
    /// 注意这是**客户实际付出**的金额,已包含渠道施加的全部税费与附加费。
    /// 门店渠道下它等于含税零售价;线上渠道下等于不含税价 + 增值税 + 平台服务费。
    fn total_amount(&self) -> Money;
 
    /// 小票中列示的税额。
    ///
    /// 免税渠道返回**本币种的零金额**,而不是「没有税额」这种模糊状态------
    /// 分析层可以直接参与求和与比较,无需处理 `Option`。
    fn tax_amount(&self) -> Money;
 
    /// 小票中列示的附加费用(平台服务费、直播佣金等),无则为本币种零金额。
    fn surcharge_amount(&self) -> Money {
        // 默认无附加费;币种必须取自实付金额,否则会造出「零元人民币」
        // 与「零元港币」相减的隐性错误
        Money::zero(self.total_amount().currency())
    }
 
    /// 是否为价内税(标价已含税)。
    ///
    /// 价内税与价外税在**用户感知**上完全不同:
    /// - 价内税(门店):顾客看到的价格就是最终付款额;
    /// - 价外税(线上):顾客看到的价格不等于最终付款额。
    ///
    /// 这个布尔值让报表层能在对照表中明确标注口径差异,
    /// 避免把两种口径的金额并排放进同一列做「谁更贵」的错误结论。
    fn is_tax_inclusive(&self) -> bool;
 
    /// 渲染为单据正文。
    fn render(&self) -> DocumentBody;
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : warranty_card.rs
//! 抽象产品 C:质保卡。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_body::DocumentBody;
 
/// 质保卡------随商品交付、凭以享受售后服务的凭证。
///
/// ## 为什么质保卡也必须进产品族
/// 抽象工厂的价值在于保证**产品族内的一致性**。
/// 若质保卡不受工厂约束,就完全可能出现
/// 「门店买的商品配着线上渠道的 12 个月质保卡」------
/// 顾客拿门店小票来主张 24 个月质保,系统却按 12 个月处理,
/// 这类纠纷在售后环节成本极高。
///
/// 把质保卡纳入同一个工厂的产出,等价于在**类型层面**保证:
/// 只要三份单据来自同一个工厂,质保期就必然与该渠道的售后政策一致。
///
/// ## 关于 `card_number`
/// 卡号必须由实现自行生成且**渠道内可追溯**(包含渠道前缀)。
/// 本工程用「渠道前缀 + 订单号 + 顺序号」构造,因此它是
/// 渲染结果的**确定性函数**,而不是随机数------
/// 同一个规格两次运行必然得到同一个卡号,便于对账复现。
pub trait WarrantyCard {
    /// 本质保卡所属的渠道编码。
    fn channel_code(&self) -> ChannelCode;
 
    /// 质保月数。
    ///
    /// 分析层用它做跨渠道对照:门店 24 个月、线上 12 个月、免税 36 个月。
    /// 这个差值本身就是一个值得向业务方呈现的发现。
    fn warranty_months(&self) -> u32;
 
    /// 质保卡编号。
    fn card_number(&self) -> &str;
 
    /// 渲染为单据正文。
    fn render(&self) -> DocumentBody;
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : date_text.rs
//! 日期文本工具(年月加减,零第三方依赖)。
//!
//! ## 为什么不用 `chrono`
//! 本工程只需「在开单日期上加 N 个月得到质保到期日」这一件事,
//! 为此引入一个完整的日期时间库会让演示工程被依赖细节淹没。
//! 而本工程的税务与单据逻辑**不需要**真正的日期运算能力。
//!
//! ## 为什么放在 support 而不是各产品里
//! 四个渠道都要算到期日(24 / 12 / 36 / 6 个月)。
//! 若各自实现,迟早会出现「有的做了月末截断、有的没有」的不一致,
//! 而同一天到期日在不同渠道给出不同结果,是最难排查的一类报表缺陷。
 
/// 在 `YYYY-MM-DD` 文本上增加若干月,返回新的 `YYYY-MM-DD` 文本。
///
/// # 参数
/// - `issue_date`:起始日期,格式 `YYYY-MM-DD`
/// - `months`:增加的月数
///
/// # 边界处理:月末截断
/// 「1 月 31 日加一个月」在业务上应得「2 月 28 日」(闰年为 29 日),
/// 而不是溢出到 3 月。这里做了月末截断,因为质保到期日若凭空多出几天,
/// 售后系统会据此判定「仍在保」,进而产生本不该发生的免费维修。
///
/// # 解析失败时的行为
/// 输入不符合 `YYYY-MM-DD` 时**原样返回**。宁可让报表显示一个
/// 明显未被修改的日期(人眼可察觉异常),也不要 panic 掉整条单据生成流程
/// ------ 一份日期可疑的单据仍然可用,一份生成失败的单据则完全不可用。
pub fn add_months(issue_date: &str, months: u32) -> String {
    // 按连字符拆三段
    let mut segments = issue_date.split('-');
    // 依次取出年 / 月 / 日
    let year_segment: Option<&str> = segments.next();
    let month_segment: Option<&str> = segments.next();
    let day_segment: Option<&str> = segments.next();
    // 三段必须齐全
    let (year_text, month_text, day_text) = match (year_segment, month_segment, day_segment) {
        // 齐全
        (Some(year), Some(month), Some(day)) => (year, month, day),
        // 缺段:原样返回
        _ => return issue_date.to_string(),
    };
    // 解析年份
    let year: i32 = match year_text.parse::<i32>() {
        // 解析成功
        Ok(parsed_year) => parsed_year,
        // 解析失败:原样返回
        Err(_) => return issue_date.to_string(),
    };
    // 解析月份
    let month: u32 = match month_text.parse::<u32>() {
        // 解析成功
        Ok(parsed_month) => parsed_month,
        // 解析失败:原样返回
        Err(_) => return issue_date.to_string(),
    };
    // 解析日
    let day: u32 = match day_text.parse::<u32>() {
        // 解析成功
        Ok(parsed_day) => parsed_day,
        // 解析失败:原样返回
        Err(_) => return issue_date.to_string(),
    };
    // 年份必须非负、月份必须在 1..=12
    if year < 0 || !(1..=12).contains(&month) {
        return issue_date.to_string();
    }
    // 换算为「从公元 0 年 1 月起的绝对月序号」,跨年进位就自然成立了
    let absolute_month: u32 = (year as u32) * 12 + (month - 1);
    // 加上要增加的月数
    let target_absolute_month: u32 = absolute_month + months;
    // 换算回目标年 / 月
    let target_year: u32 = target_absolute_month / 12;
    let target_month: u32 = target_absolute_month % 12 + 1;
    // 目标日的上限是该月实际天数,超出则截断到月末
    let target_day: u32 = day.min(days_in_month(target_year, target_month));
    format!("{:04}-{:02}-{:02}", target_year, target_month, target_day)
}
 
/// 返回指定年月的天数。
///
/// # 参数
/// - `year`:年份
/// - `month`:月份(1..=12)
fn days_in_month(year: u32, month: u32) -> u32 {
    match month {
        // 大月
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        // 小月
        4 | 6 | 9 | 11 => 30,
        // 二月按闰年判断
        2 => {
            if is_leap_year(year) {
                29
            } else {
                28
            }
        }
        // 非法月份:返回 30 只为不 panic,调用方已在上游校验过范围
        _ => 30,
    }
}
 
/// 判断是否为闰年(公历规则:四年一闰,百年不闰,四百年再闰)。
///
/// # 参数
/// - `year`:年份
fn is_leap_year(year: u32) -> bool {
    // 能被 4 整除且不能被 100 整除,或能被 400 整除
    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : deterministic_code.rs
//! 确定性编码派生(零依赖、可复现)。
 
/// 由任意文本派生一个确定性数字串(用于条码、凭证号等机器可读标识)。
///
/// # 参数
/// - `source`:派生源文本(如货号)
/// - `prefix`:固定前缀(如中国商品条码前缀 `690`)
/// - `total_length`:最终数字串的总长度
///
/// # 为什么必须是确定性的
/// 条码、券码这类标识若用随机数生成,就会出现「同一份单据两次渲染结果不同」。
/// 这破坏可复现性------对账时无法确认「哪一份才是当时开出的单据」,
/// 也无法用同一份输入重跑出一致的结果去验证差异究竟来自哪一层。
///
/// 这里用 FNV 风格的乘加散列:**输入相同则输出必然相同**,
/// 且分布足够均匀,不会出现连续订单号撞码。
///
/// # 输出长度不足时的处理
/// 用前导零补齐到 `available` 位;散列值超过可用位数时取低位。
pub fn derive_digit_code(source: &str, prefix: &str, total_length: usize) -> String {
    // 64 位散列累加器,初值取 FNV-1a 偏移基准
    let mut accumulator: u64 = 0xcbf2_9ce4_8422_2325;
    for character in source.chars() {
        // 先异或字符码位
        accumulator ^= character as u64;
        // 再乘以 FNV-1a 质数(wrapping 避免溢出 panic)
        accumulator = accumulator.wrapping_mul(0x0000_0100_0000_01b3);
    }
    // 前缀已占用的字符数
    let prefix_length: usize = prefix.chars().count();
    // 剩余可用位数
    let available: usize = total_length.saturating_sub(prefix_length);
    // 前缀已占满:只返回前缀的前 total_length 位
    if available == 0 {
        return prefix.chars().take(total_length).collect();
    }
    // 可用位数的十进制量级;位数过大时 saturating_pow 会饱和,
    // 此时直接用散列原值(实际业务中不会出现这么长的编码)
    let modulus: u64 = 10u64.saturating_pow(available as u32);
    // 取低位
    let digits: u64 = if modulus == u64::MAX {
        // 量级饱和,原值即结果
        accumulator
    } else {
        // 正常取模
        accumulator % modulus
    };
    // 拼接前缀与前导零补齐后的数字
    format!(
        "{}{:0width$}",
        prefix,
        digits,
        width = available
    )
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : text_layout.rs
//! 宽字符感知的文本排版工具。
//!
//! ## 这一层为什么必须存在
//! 中文(CJK)字符在等宽终端里占 **2 个显示列**,而 Rust 标准格式化的宽度参数
//! (如 `{:<20}`)是按 **字符个数** 补空格的。只要表格里出现中文,
//! 用标准格式化输出就会整列错位------这是前几个工程反复踩到的坑,
//! 因此在这里沉淀成**全工程唯一**的一份实现。
//!
//! ## 依赖约束
//! 本模块**零依赖**(连 `domain` 都不引用),只处理 `&str` 与 `usize`,
//! 因此可以被任意层安全使用而不产生依赖环。
 
/// 判断单个字符是否占 2 个显示列。
///
/// # 参数
/// - `character`:待判断的字符
///
/// # 为什么用区间近似而不是引入 `unicode-width`
/// 本工程坚持零第三方依赖;而下列区间已覆盖中日韩文字、全角标点、
/// 假名与常见符号,对本工程的报表排版完全够用。
/// 区间之间刻意**不重叠**,否则 `matches!` 会触发「不可达模式」告警。
pub fn is_wide_character(character: char) -> bool {
    // 逐区间比对码位;命中任一区间即视为全角(宽 2 列)
    matches!(
        character as u32,
        0x1100..=0x115F      // 朝鲜文字母
            | 0x2E80..=0x303E // 康熙部首 + 中日韩符号与标点
            | 0x3041..=0x33FF // 平假名 / 片假名 / 中日韩兼容字符
            | 0x3400..=0x4DBF // 中日韩扩展 A
            | 0x4E00..=0x9FFF // 中日韩统一表意文字
            | 0xA000..=0xA4CF // 彝文
            | 0xAC00..=0xD7A3 // 谚文音节
            | 0xF900..=0xFAFF // 中日韩兼容表意文字
            | 0xFE30..=0xFE6F // 中日韩竖排标点与兼容形式
            | 0xFF00..=0xFF60 // 全角 ASCII 变体
            | 0xFFE0..=0xFFE6 // 全角货币与符号
            | 0x20000..=0x3FFFD // 中日韩扩展 B 及以后
    )
}
 
/// 计算字符串的显示宽度(CJK 字符计 2 列,其余计 1 列)。
///
/// # 参数
/// - `text`:待测量的文本
pub fn display_width(text: &str) -> usize {
    // 逐字符累加宽度,不做任何分配
    text.chars()
        .map(|character| if is_wide_character(character) { 2 } else { 1 })
        .sum()
}
 
/// 在右侧补空格,使整串达到目标显示宽度(用于左对齐列)。
///
/// # 参数
/// - `text`:原始文本
/// - `target_width`:目标显示宽度(列)
///
/// # 为什么超出时不截断
/// 截断是**破坏性**操作,会静默丢字。这里选择「超宽就原样返回」,
/// 把「要不要丢字」的决定权交给调用方(需要时显式调用 [`truncate_to_width`]),
/// 避免排版工具悄悄吞掉业务数据。
pub fn pad_right(text: &str, target_width: usize) -> String {
    // 当前显示宽度
    let current_width: usize = display_width(text);
    // 已达到或超过目标宽度:原样返回,不截断
    if current_width >= target_width {
        return text.to_string();
    }
    // 预分配足够容量,避免补空格时反复扩容
    let mut result: String = String::with_capacity(text.len() + (target_width - current_width));
    // 先写原文
    result.push_str(text);
    // 再补足差值个空格
    result.push_str(&" ".repeat(target_width - current_width));
    result
}
 
/// 在左侧补空格,使整串达到目标显示宽度(用于金额等右对齐列)。
///
/// # 参数
/// - `text`:原始文本
/// - `target_width`:目标显示宽度(列)
pub fn pad_left(text: &str, target_width: usize) -> String {
    // 当前显示宽度
    let current_width: usize = display_width(text);
    // 已达到或超过目标宽度:原样返回
    if current_width >= target_width {
        return text.to_string();
    }
    // 先补空格
    let mut result: String = " ".repeat(target_width - current_width);
    // 再写原文
    result.push_str(text);
    result
}
 
/// 居中补齐:左右均分空格;差值为奇数时右侧多一个(视觉上更自然)。
///
/// # 参数
/// - `text`:原始文本
/// - `target_width`:目标显示宽度(列)
pub fn pad_center(text: &str, target_width: usize) -> String {
    // 当前显示宽度
    let current_width: usize = display_width(text);
    // 已达到或超过目标宽度:原样返回
    if current_width >= target_width {
        return text.to_string();
    }
    // 需要补的总空格数
    let total_padding: usize = target_width - current_width;
    // 左半(向下取整)
    let left_padding: usize = total_padding / 2;
    // 右半(剩余的都给右边)
    let right_padding: usize = total_padding - left_padding;
    // 拼接三段
    format!(
        "{}{}{}",
        " ".repeat(left_padding),
        text,
        " ".repeat(right_padding)
    )
}
 
/// 按显示宽度截断文本,超出部分以省略号「...」标记。
///
/// # 参数
/// - `text`:原始文本
/// - `target_width`:目标显示宽度(列)
///
/// # 实现要点
/// 必须**按字符边界**逐个累加,不能按字节切片------中文是 3 字节 UTF-8,
/// 按字节切会切出非法字符串并 panic。
pub fn truncate_to_width(text: &str, target_width: usize) -> String {
    // 未超宽则原样返回,省掉一次遍历
    if display_width(text) <= target_width {
        return text.to_string();
    }
    // 省略号「...」自身占 1 列,因此留给正文的预算要减 1
    let content_budget: usize = target_width.saturating_sub(1);
    // 已累计的显示宽度
    let mut accumulated_width: usize = 0;
    // 逐字符累积结果
    let mut result: String = String::new();
    for character in text.chars() {
        // 当前字符宽度
        let character_width: usize = if is_wide_character(character) { 2 } else { 1 };
        // 放不下就停(宁可留一列空白,也不切断字符)
        if accumulated_width + character_width > content_budget {
            break;
        }
        // 累加宽度并写入
        accumulated_width += character_width;
        result.push(character);
    }
    // 补上省略号
    result.push('...');
    result
}
 
/// 生成一条水平分隔线。
///
/// # 参数
/// - `fill_character`:填充字符,例如 `-` 或 `=`
/// - `width`:分隔线的显示宽度(列)
pub fn horizontal_rule(fill_character: char, width: usize) -> String {
    // 重复填充字符到指定宽度;这些字符均为半角,宽度即字符数
    std::iter::repeat(fill_character).take(width).collect()
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : channel_comparison.rs
//! 跨渠道横向对照。
 
use crate::client::suite_bundle::SuiteBundle;
use crate::domain::channel_code::ChannelCode;
use crate::domain::money::Money;
 
/// 一个渠道的对照行。
///
/// ## 为什么用公开字段而不是一堆 getter
/// 这是纯粹的**数据传输对象(DTO)**:由 `compare_channels` 一次性产出、
/// 由 `app` 层一次性消费,中间没有任何行为。
/// 给它加十个 getter 只会增加阅读成本,而不会增加任何封装价值------
/// 它本来就没有需要保护的内部不变量。
#[derive(Debug, Clone)]
pub struct ChannelComparisonRow {
    /// 渠道编码
    pub channel_code: ChannelCode,
    /// 渠道展示名称
    pub channel_label: String,
    /// 价格标签正面展示的金额(口径随渠道而异)
    pub price_display_amount: Money,
    /// 小票列示的税额
    pub tax_amount: Money,
    /// 小票列示的附加费
    pub surcharge_amount: Money,
    /// 客户实付合计
    pub total_amount: Money,
    /// 质保月数
    pub warranty_months: u32,
    /// 是否价内税(标价即实付)
    pub tax_inclusive: bool,
    /// 三件单据正文行数总和(信息密度)
    pub document_line_total: usize,
}
 
impl ChannelComparisonRow {
    /// 标价与实付之间的差额。
    ///
    /// 价内税渠道该项为零(顾客看到多少就付多少);
    /// 价外税渠道该项为正(差额即税与费)。
    /// 这是本工程要揭示的核心现象之一。
    pub fn display_to_payment_gap(&self) -> Money {
        // 实付减标价
        self.total_amount.subtract(self.price_display_amount)
    }
}
 
/// 对多个渠道的套件做横向对照。
///
/// # 参数
/// - `bundles`:套件包切片(各渠道的套件应基于**同一份单据规格**)
///
/// # 返回
/// 与入参顺序一致的对照行列表。
///
/// # 一个必须由调用方承担的前提
/// 本函数无法验证「这些套件是否基于同一份规格」------
/// 那需要把规格也传进来。本工程通过约定保证:
/// 所有套件都由 `client::assemble_suites` 用同一个 `&DocumentSpec` 产出。
/// 在签名层面强调这一点,比在文档里写一句「请保证」更可靠。
pub fn compare_channels(bundles: &[SuiteBundle]) -> Vec<ChannelComparisonRow> {
    // 逐个套件产出对照行
    bundles
        .iter()
        .map(|bundle| {
            // 价格标签
            let price_tag = bundle.price_tag();
            // 销售小票
            let sales_receipt = bundle.sales_receipt();
            // 质保卡
            let warranty_card = bundle.warranty_card();
            ChannelComparisonRow {
                // 渠道编码取自工厂声明
                channel_code: bundle.factory_channel_code(),
                // 渠道名称取自工厂声明
                channel_label: bundle.factory_channel_label().to_string(),
                // 标签展示金额
                price_display_amount: price_tag.display_amount(),
                // 税额
                tax_amount: sales_receipt.tax_amount(),
                // 附加费
                surcharge_amount: sales_receipt.surcharge_amount(),
                // 客户实付
                total_amount: sales_receipt.total_amount(),
                // 质保月数
                warranty_months: warranty_card.warranty_months(),
                // 税务口径
                tax_inclusive: sales_receipt.is_tax_inclusive(),
                // 单据行数合计
                document_line_total: bundle.document_line_total(),
            }
        })
        .collect()
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : family_consistency.rs
//! 产品族一致性核查------验证抽象工厂的核心承诺。
 
use crate::client::suite_bundle::SuiteBundle;
use crate::domain::channel_code::ChannelCode;
use crate::domain::money::Money;
use crate::domain::rate::Rate;
 
/// 产品族一致性核查报告。
///
/// ## 这个核查器存在的意义
/// 抽象工厂的核心承诺是「同一工厂产出的产品必然同族」。
/// 但**承诺需要被验证**:
/// - 若工程外有人实现了一个有缺陷的工厂(例如把门店质保卡误写进线上工厂),
///   编译期不会报错------它确实是合法的 `Box<dyn WarrantyCard>`;
/// - 若将来有人手工拼装 `SuiteBundle`(绕过 `assemble_suite`),
///   也完全可能把两个渠道的产品混在一起。
///
/// 本核查器在运行期比对「工厂声明的渠道」与「三件产品自报的渠道」,
/// 让这类问题在数据显示出来之前就被发现。
///
/// ## 为什么不用 `Result`
/// 「不一致」是**被检查出来的事实**(发现),不是「流程失败」(异常)。
/// 用报告对象承载,报表层可以选择「报警但继续出具单据」,
/// 这比直接抛错更符合单据系统的现实(单据本身仍然有效,只是需要人工复核)。
#[derive(Debug, Clone)]
pub struct FamilyConsistencyCheck {
    /// 工厂声明的渠道编码
    factory_channel_code: ChannelCode,
    /// 工厂声明的渠道名称
    factory_channel_label: String,
    /// 已核查的单据件数
    inspected_document_count: usize,
    /// 渠道自报与工厂声明不一致的单据名称
    mismatched_document_names: Vec<String>,
    /// 逐项核查的口径说明(供人工复核)
    notes: Vec<String>,
}
 
impl FamilyConsistencyCheck {
    /// 一致性是否通过。
    pub fn is_consistent(&self) -> bool {
        // 没有任何不一致项即通过
        self.mismatched_document_names.is_empty()
    }
 
    /// 读取工厂声明的渠道编码。
    pub fn factory_channel_code(&self) -> ChannelCode {
        // ChannelCode 是 Copy
        self.factory_channel_code
    }
 
    /// 读取工厂声明的渠道名称。
    pub fn factory_channel_label(&self) -> &str {
        // 切片式暴露
        &self.factory_channel_label
    }
 
    /// 读取已核查的单据件数。
    pub fn inspected_document_count(&self) -> usize {
        // usize 是 Copy
        self.inspected_document_count
    }
 
    /// 读取不一致单据的名称列表。
    pub fn mismatched_document_names(&self) -> &[String] {
        // 切片式暴露
        &self.mismatched_document_names
    }
 
    /// 读取逐项核查说明。
    pub fn notes(&self) -> &[String] {
        // 切片式暴露
        &self.notes
    }
}
 
/// 核查一个套件包的产品族一致性。
///
/// # 参数
/// - `bundle`:待核查的套件包
///
/// # 核查项
/// 1. **渠道归属**:三件产品自报的渠道编码是否都等于工厂声明的编码;
/// 2. **税务口径自洽**:价内税下税额不得超过实付;
/// 3. **质保期有效**:质保月数必须大于零。
pub fn inspect_family_consistency(bundle: &SuiteBundle) -> FamilyConsistencyCheck {
    // 工厂声明的渠道编码
    let factory_channel_code: ChannelCode = bundle.factory_channel_code();
    // 三件单据的名称(用于把不一致项定位到具体单据)
    let document_names: Vec<&'static str> = bundle
        .documents()
        .iter()
        .map(|(name, _)| *name)
        .collect();
    // 三件产品自报的渠道编码
    let reported_channel_codes: Vec<ChannelCode> = bundle.product_channel_codes();
    // 收集不一致的单据名
    let mut mismatched_document_names: Vec<String> = Vec::new();
    // 逐件比对(两列表长度一致:都是三件)
    for (index, reported_channel_code) in reported_channel_codes.iter().enumerate() {
        // 与工厂声明比对
        if *reported_channel_code != factory_channel_code {
            // 记录不一致的单据名(越界时退化为占位名,不会 panic)
            let document_name: &str = document_names.get(index).copied().unwrap_or("未知单据");
            mismatched_document_names.push(format!(
                "{}(自报 {},工厂声明 {})",
                document_name,
                reported_channel_code.as_str(),
                factory_channel_code.as_str()
            ));
        }
    }
    // 收集口径说明
    let mut notes: Vec<String> = Vec::new();
    // 销售小票
    let sales_receipt = bundle.sales_receipt();
    // 实付金额
    let total_amount: Money = sales_receipt.total_amount();
    // 税额
    let tax_amount: Money = sales_receipt.tax_amount();
    // 附加费
    let surcharge_amount: Money = sales_receipt.surcharge_amount();
    // 核查项 2:税务口径自洽
    if sales_receipt.is_tax_inclusive() {
        // 价内税:税额应已包含在实付中,因此不得超过实付
        if tax_amount.minor_units() > total_amount.minor_units() {
            notes.push(format!(
                "销售小票:价内税口径下税额 {} 超过了实付 {},口径不自洽",
                tax_amount.formatted(),
                total_amount.formatted()
            ));
        } else if tax_amount.is_zero() {
            // 零税额:这是免税口径,不能说成「价内税」------
            // 「税额为零」在业务上是「免征」,措辞必须区分,
            // 否则税务同事看到「价内税 0.00」会以为是税率配置漏了值
            notes.push(format!(
                "销售小票:免税口径,本单税额为零,标价即实付 {}",
                total_amount.formatted()
            ));
        } else {
            // 计算税额占实付的比例,让核查结果可量化
            let tax_share: Rate = Rate::from_money_ratio(tax_amount, total_amount);
            notes.push(format!(
                "销售小票:价内税,税额 {} 已包含在实付 {} 中(占 {})",
                tax_amount.formatted(),
                total_amount.formatted(),
                tax_share.as_percent_text()
            ));
        }
    } else {
        // 价外税:实付应当严格大于不含税净额
        let excluding_tax_amount: Money = total_amount.subtract(tax_amount).subtract(surcharge_amount);
        if total_amount.minor_units() <= excluding_tax_amount.minor_units() {
            notes.push(format!(
                "销售小票:价外税口径下实付 {} 未超过不含税净额 {},口径不自洽",
                total_amount.formatted(),
                excluding_tax_amount.formatted()
            ));
        } else {
            notes.push(format!(
                "销售小票:价外税,不含税净额 {} + 税额 {} + 附加费 {} = 实付 {}",
                excluding_tax_amount.formatted(),
                tax_amount.formatted(),
                surcharge_amount.formatted(),
                total_amount.formatted()
            ));
        }
    }
    // 核查项 3:质保期有效
    let warranty_card = bundle.warranty_card();
    // 质保月数
    let warranty_months: u32 = warranty_card.warranty_months();
    if warranty_months == 0 {
        // 零质保期视为异常
        notes.push(format!(
            "质保卡:{} 的质保月数为 0,质保卡形同虚设",
            warranty_card.card_number()
        ));
    } else {
        // 正常情况
        notes.push(format!(
            "质保卡:{},质保期 {} 个月",
            warranty_card.card_number(),
            warranty_months
        ));
    }
    FamilyConsistencyCheck {
        // 工厂声明的渠道编码
        factory_channel_code,
        // 工厂声明的渠道名称
        factory_channel_label: bundle.factory_channel_label().to_string(),
        // 已核查件数
        inspected_document_count: reported_channel_codes.len(),
        // 不一致清单
        mismatched_document_names,
        // 口径说明
        notes,
    }
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : tax_study.rs
//! 渠道税负研究------回答「同一个商品,哪个渠道的税务成本更高」。
 
use crate::client::suite_bundle::SuiteBundle;
use crate::domain::channel_code::ChannelCode;
use crate::domain::money::Money;
use crate::domain::rate::Rate;
 
/// 单个渠道的税负条目。
#[derive(Debug, Clone)]
pub struct TaxStudyEntry {
    /// 渠道编码
    pub channel_code: ChannelCode,
    /// 渠道展示名称
    pub channel_label: String,
    /// 税额
    pub tax_amount: Money,
    /// 客户实付
    pub total_amount: Money,
    /// 有效税率=税额 ÷ 客户实付。
    ///
    /// # 为什么用「实付」而不是「不含税净额」作分母
    /// 因为本工程要回答的问题是「顾客每付 100 元,有多少是税」。
    /// 这个口径跨渠道可比------无论价内税还是价外税,
    /// 实付都是顾客真实掏出的钱,分母一致。
    /// 若用不含税净额作分母,价内税渠道需要先反向分离(做一次除法),
    /// 每次除法都会引入一次舍入,多渠道路径不同则误差不可控。
    pub effective_rate: Rate,
}
 
/// 渠道税负研究结果。
#[derive(Debug, Clone)]
pub struct TaxStudy {
    /// 各渠道税负条目
    entries: Vec<TaxStudyEntry>,
}
 
impl TaxStudy {
    /// 读取全部税负条目。
    pub fn entries(&self) -> &[TaxStudyEntry] {
        // 切片式暴露
        &self.entries
    }
 
    /// 查找**实付最低**的渠道。
    ///
    /// 空输入返回 `None`------「没有渠道」与「有渠道但都为零」是两回事,
    /// 用 `Option` 表达才能让报表层区分这两种情况。
    pub fn lowest_total_entry(&self) -> Option<&TaxStudyEntry> {
        // 按实付金额取最小
        self.entries
            .iter()
            .min_by_key(|entry| entry.total_amount.minor_units())
    }
 
    /// 查找**实付最高**的渠道。
    pub fn highest_total_entry(&self) -> Option<&TaxStudyEntry> {
        // 按实付金额取最大
        self.entries
            .iter()
            .max_by_key(|entry| entry.total_amount.minor_units())
    }
 
    /// 最高与最低实付之间的差额。
    ///
    /// 渠道少于 2 个时返回 `None`(没有差额可言)。
    pub fn total_spread(&self) -> Option<Money> {
        // 取出两端
        let lowest: &TaxStudyEntry = self.lowest_total_entry()?;
        let highest: &TaxStudyEntry = self.highest_total_entry()?;
        // 差额
        Some(highest.total_amount.subtract(lowest.total_amount))
    }
 
    /// 查找**税额最低**的渠道。
    pub fn lowest_tax_entry(&self) -> Option<&TaxStudyEntry> {
        // 按税额取最小
        self.entries
            .iter()
            .min_by_key(|entry| entry.tax_amount.minor_units())
    }
 
    /// 查找**税额最高**的渠道。
    pub fn highest_tax_entry(&self) -> Option<&TaxStudyEntry> {
        // 按税额取最大
        self.entries
            .iter()
            .max_by_key(|entry| entry.tax_amount.minor_units())
    }
}
 
/// 对多个渠道的套件做税负研究。
///
/// # 参数
/// - `bundles`:套件包切片
pub fn study_channel_tax(bundles: &[SuiteBundle]) -> TaxStudy {
    // 逐套件产出税负条目
    let entries: Vec<TaxStudyEntry> = bundles
        .iter()
        .map(|bundle| {
            // 销售小票
            let sales_receipt = bundle.sales_receipt();
            // 实付
            let total_amount: Money = sales_receipt.total_amount();
            // 税额
            let tax_amount: Money = sales_receipt.tax_amount();
            TaxStudyEntry {
                // 渠道编码
                channel_code: bundle.factory_channel_code(),
                // 渠道名称
                channel_label: bundle.factory_channel_label().to_string(),
                // 税额
                tax_amount,
                // 实付
                total_amount,
                // 有效税率 = 税额 ÷ 实付
                effective_rate: Rate::from_money_ratio(tax_amount, total_amount),
            }
        })
        .collect();
    TaxStudy {
        // 汇总条目
        entries,
    }
}
rust 复制代码
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : comparison_report.rs
//! 跨渠道对照报表、一致性核查报表、税负研究报表。
 
use crate::analysis::{ChannelComparisonRow, FamilyConsistencyCheck, TaxStudy, TaxStudyEntry};
use crate::domain::money::Money;
use crate::support::text_layout;
 
/// 渲染跨渠道对照表。
///
/// # 参数
/// - `rows`:对照行切片
///
/// # 本表最想回答的问题
/// 「同一批商品,在四个渠道下的客户实付差多少、差在哪。」
/// 因此除了金额列,还刻意保留「标价口径」与「单据行数」两列:
/// - 前者让人一眼看出「线上实付高」不是因为商品贵,而是因为口径不同;
/// - 后者让人看到合规成本------免税店要多交代购买资格与监管码,
///   它的单据行数必然最长。
pub fn render_channel_comparison(rows: &[ChannelComparisonRow]) -> String {
    // 各列显示宽度
    const CHANNEL_WIDTH: usize = 20;
    const BASIS_WIDTH: usize = 12;
    const DISPLAY_AMOUNT_WIDTH: usize = 16;
    const TAX_WIDTH: usize = 14;
    const SURCHARGE_WIDTH: usize = 13;
    const TOTAL_WIDTH: usize = 16;
    const WARRANTY_WIDTH: usize = 12;
    const LINE_COUNT_WIDTH: usize = 12;
    // 逐行收集
    let mut lines: Vec<String> = Vec::new();
    // 表头
    lines.push(format!(
        "  {}{}{}{}{}{}{}{}",
        text_layout::pad_right("渠道", CHANNEL_WIDTH),
        text_layout::pad_right("标价口径", BASIS_WIDTH),
        text_layout::pad_left("标签标价", DISPLAY_AMOUNT_WIDTH),
        text_layout::pad_left("税额", TAX_WIDTH),
        text_layout::pad_left("附加费", SURCHARGE_WIDTH),
        text_layout::pad_left("客户实付", TOTAL_WIDTH),
        text_layout::pad_left("质保(月)", WARRANTY_WIDTH),
        text_layout::pad_left("单据行数", LINE_COUNT_WIDTH)
    ));
    // 分隔线
    lines.push(text_layout::horizontal_rule(
        '-',
        2 + CHANNEL_WIDTH
            + BASIS_WIDTH
            + DISPLAY_AMOUNT_WIDTH
            + TAX_WIDTH
            + SURCHARGE_WIDTH
            + TOTAL_WIDTH
            + WARRANTY_WIDTH
            + LINE_COUNT_WIDTH,
    ));
    // 逐渠道一行
    for row in rows {
        // 质保月数文本
        let warranty_text: String = row.warranty_months.to_string();
        // 单据行数文本
        let line_count_text: String = row.document_line_total.to_string();
        lines.push(format!(
            "  {}{}{}{}{}{}{}{}",
            text_layout::pad_right(
                // 渠道名过长时按显示宽度截断,避免整行错位;
                // 截断只在真的超宽时发生(本工程的渠道名都不会触发)
                &text_layout::truncate_to_width(&row.channel_label, CHANNEL_WIDTH),
                CHANNEL_WIDTH
            ),
            text_layout::pad_right(price_basis_text(row), BASIS_WIDTH),
            text_layout::pad_left(&row.price_display_amount.formatted(), DISPLAY_AMOUNT_WIDTH),
            text_layout::pad_left(&row.tax_amount.formatted(), TAX_WIDTH),
            text_layout::pad_left(&row.surcharge_amount.formatted(), SURCHARGE_WIDTH),
            text_layout::pad_left(&row.total_amount.formatted(), TOTAL_WIDTH),
            text_layout::pad_left(&warranty_text, WARRANTY_WIDTH),
            text_layout::pad_left(&line_count_text, LINE_COUNT_WIDTH)
        ));
    }
    // 分隔线
    lines.push(text_layout::horizontal_rule(
        '-',
        2 + CHANNEL_WIDTH
            + BASIS_WIDTH
            + DISPLAY_AMOUNT_WIDTH
            + TAX_WIDTH
            + SURCHARGE_WIDTH
            + TOTAL_WIDTH
            + WARRANTY_WIDTH
            + LINE_COUNT_WIDTH,
    ));
    // 逐渠道给出「标价 → 实付」的差额
    lines.push("  【标价与实付的差额】".to_string());
    for row in rows {
        // 差额
        let gap: Money = row.display_to_payment_gap();
        if gap.is_zero() {
            // 差额为零:标价即实付
            lines.push(format!(
                "    · {}({}):标价 {}",
                row.channel_label,
                row.channel_code.as_str(),
                row.price_display_amount.formatted()
            ));
            lines.push(format!(
                "      ⟶ 实付 {},差额 {}(标价即实付,顾客所见即所付)",
                row.total_amount.formatted(),
                gap.formatted()
            ));
        } else {
            // 差额不为零:标价不是最终付款额
            lines.push(format!(
                "    · {}({}):标价 {}",
                row.channel_label,
                row.channel_code.as_str(),
                row.price_display_amount.formatted()
            ));
            lines.push(format!(
                "      ⟶ 实付 {},差额 {}(税与费在结算时另计)",
                row.total_amount.formatted(),
                gap.formatted()
            ));
        }
    }
    // 合并
    lines.join("\n")
}
 
/// 从对照行推导「标价口径」文本。
///
/// # 参数
/// - `row`:对照行
///
/// # 为什么是推导而不是读字段
/// 「免税价」并不是一个布尔开关------它是「税额为零」这一事实的**表述**。
/// 若专门加一个字段,就必须有人在每个渠道里记得去设它,
/// 而漏设会导致报表错误标注。从数据推导,则永远自洽。
fn price_basis_text(row: &ChannelComparisonRow) -> &'static str {
    if row.tax_amount.is_zero() {
        // 税额为零即免税口径
        "免税价"
    } else if row.tax_inclusive {
        // 价内税:标价即实付
        "含税价"
    } else {
        // 价外税:标价不含税
        "不含税价"
    }
}
 
/// 渲染产品族一致性核查报表。
///
/// # 参数
/// - `check`:核查报告
pub fn render_consistency_check(check: &FamilyConsistencyCheck) -> String {
    // 逐行收集
    let mut lines: Vec<String> = Vec::new();
    // 抬头:渠道 + 结论
    lines.push(format!(
        "  渠道 {}({}) 核查 {}/3 件单据 结论:{}",
        check.factory_channel_label(),
        check.factory_channel_code().as_str(),
        check.inspected_document_count(),
        if check.is_consistent() {
            "一致"
        } else {
            "不一致"
        }
    ));
    // 不一致清单
    if check.is_consistent() {
        // 一致时也明确说出来,避免「没输出」被误读为「没检查」
        lines.push("    · 渠道归属:三件产品自报渠道与工厂声明完全一致。".to_string());
    } else {
        // 逐条列出
        for mismatch in check.mismatched_document_names() {
            lines.push(format!("    ! 渠道归属不一致:{}", mismatch));
        }
    }
    // 口径说明
    for note in check.notes() {
        lines.push(format!("    · {}", note));
    }
    // 合并
    lines.join("\n")
}
 
/// 渲染渠道税负研究报表。
///
/// # 参数
/// - `study`:税负研究结果
pub fn render_tax_study(study: &TaxStudy) -> String {
    // 渠道列宽(容纳「名称(编码)」形式)
    const CHANNEL_WIDTH: usize = 28;
    // 税额列宽
    const TAX_WIDTH: usize = 16;
    // 实付列宽
    const TOTAL_WIDTH: usize = 16;
    // 有效税率列宽
    const RATE_WIDTH: usize = 12;
    // 逐行收集
    let mut lines: Vec<String> = Vec::new();
    // 表头
    lines.push(format!(
        "  {}{}{}{}",
        text_layout::pad_right("渠道", CHANNEL_WIDTH),
        text_layout::pad_left("税额", TAX_WIDTH),
        text_layout::pad_left("客户实付", TOTAL_WIDTH),
        text_layout::pad_left("有效税率", RATE_WIDTH)
    ));
    // 分隔线
    lines.push(text_layout::horizontal_rule(
        '-',
        2 + CHANNEL_WIDTH + TAX_WIDTH + TOTAL_WIDTH + RATE_WIDTH,
    ));
    // 取税负条目(显式标注类型,让「本报表消费的是哪一层的数据结构」一目了然)
    let entries: &[TaxStudyEntry] = study.entries();
    // 逐渠道一行
    for entry in entries {
        lines.push(format!(
            "  {}{}{}{}",
            text_layout::pad_right(
                // 「渠道名(编码)」便于与领域层的渠道编码对照
                &format!("{}({})", entry.channel_label, entry.channel_code.as_str()),
                CHANNEL_WIDTH
            ),
            text_layout::pad_left(&entry.tax_amount.formatted(), TAX_WIDTH),
            text_layout::pad_left(&entry.total_amount.formatted(), TOTAL_WIDTH),
            text_layout::pad_left(&entry.effective_rate.as_percent_text(), RATE_WIDTH)
        ));
    }
    // 分隔线
    lines.push(text_layout::horizontal_rule(
        '-',
        2 + CHANNEL_WIDTH + TAX_WIDTH + TOTAL_WIDTH + RATE_WIDTH,
    ));
    // 结论区
    lines.push("  【结论】".to_string());
    // 实付极差
    match study.total_spread() {
        Some(spread) => {
            // 取两端渠道名
            let lowest_label: &str = study
                .lowest_total_entry()
                .map(|entry| entry.channel_label.as_str())
                .unwrap_or("未知");
            let highest_label: &str = study
                .highest_total_entry()
                .map(|entry| entry.channel_label.as_str())
                .unwrap_or("未知");
            lines.push(format!(
                "    · 实付极差:{}({}最低,{}最高)",
                spread.formatted(),
                lowest_label,
                highest_label
            ));
        }
        None => {
            // 渠道不足两个
            lines.push("    · 渠道少于两个,无法给出极差。".to_string());
        }
    }
    // 税负极值
    if let (Some(lowest_tax_entry), Some(highest_tax_entry)) =
        (study.lowest_tax_entry(), study.highest_tax_entry())
    {
        lines.push(format!(
            "    · 税负极值:{} {} / {} {}",
            lowest_tax_entry.channel_label,
            lowest_tax_entry.tax_amount.formatted(),
            highest_tax_entry.channel_label,
            highest_tax_entry.tax_amount.formatted()
        ));
    }
    // 合并
    lines.join("\n")
}
 
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : suite_report.rs
//! 套件正文渲染。
 
use crate::client::suite_bundle::SuiteBundle;
use crate::domain::document_body::DocumentBody;
use crate::support::text_layout;
 
/// 渲染一整套单据(三件依次输出)。
///
/// # 参数
/// - `bundle`:套件包
///
/// # 输出结构
/// ```text
/// ══ 套件抬头(渠道 + 件数)══
///   ▸ 1/3 价格标签
///     <正文内容,缩进 4 空格>
///   ▸ 2/3 销售小票
///     ...
/// ```
///
/// # 正文为什么逐行加缩进
/// 产品渲染出的正文是**独立文档**(行首无缩进,可直接打印在纸面上)。
/// 报表把它们嵌进更大的版面时,若不缩进,单据内的分隔线就会与报表的
/// 分隔线混在一起,读者无法分辨「哪条线属于哪一层」。
/// 缩进是**渲染时**加的,产品本身不需要知道自己在被谁展示------
/// 这正是 `DocumentBody` 只描述内容、不描述外观的价值。
pub fn render_suite(bundle: &SuiteBundle) -> String {
    // 报表宽度(显示列)
    const REPORT_WIDTH: usize = 96;
    // 逐行收集
    let mut lines: Vec<String> = Vec::new();
    // 顶部双线
    lines.push(text_layout::horizontal_rule('=', REPORT_WIDTH));
    // 套件抬头:渠道名称 + 渠道编码 + 件数
    lines.push(format!(
        "  套件:{}({}) 共 {} 件单据",
        bundle.factory_channel_label(),
        bundle.factory_channel_code().as_str(),
        bundle.document_count()
    ));
    // 底部分隔线
    lines.push(text_layout::horizontal_rule('=', REPORT_WIDTH));
    // 逐件渲染
    for (index, (document_name, document_body)) in bundle.documents().iter().enumerate() {
        // 小节标题:序号 / 总数 名称
        lines.push(format!(
            "  ▸ {}/{} {}",
            index + 1,
            bundle.document_count(),
            document_name
        ));
        // 正文逐行缩进
        push_indented_body(&mut lines, document_body);
        // 小节之间空一行
        lines.push(String::new());
    }
    // 合并
    lines.join("\n")
}
 
/// 渲染多套件的目录(只列抬头与关键指标,不展开正文)。
///
/// # 参数
/// - `bundles`:套件包切片
///
/// # 用途
/// 六个渠道逐个展开整套单据会非常长,目录让读者先看清
/// 「这次一共生成了哪些渠道的套件、各有多少件」,
/// 需要细节时再单独展开某一个。
pub fn render_suite_directory(bundles: &[SuiteBundle]) -> String {
    // 渠道名列宽
    const CHANNEL_WIDTH: usize = 20;
    // 渠道编码列宽
    const CODE_WIDTH: usize = 20;
    // 件数列宽
    const COUNT_WIDTH: usize = 8;
    // 行数列宽
    const LINE_COUNT_WIDTH: usize = 10;
    // 逐行收集
    let mut lines: Vec<String> = Vec::new();
    // 抬头
    lines.push(format!(
        "  {}{}{}{}",
        text_layout::pad_right("渠道", CHANNEL_WIDTH),
        text_layout::pad_right("渠道编码", CODE_WIDTH),
        text_layout::pad_left("件数", COUNT_WIDTH),
        text_layout::pad_left("正文行数", LINE_COUNT_WIDTH)
    ));
    // 分隔线(长度 = 各列宽之和)
    lines.push(text_layout::horizontal_rule(
        '-',
        CHANNEL_WIDTH + CODE_WIDTH + COUNT_WIDTH + LINE_COUNT_WIDTH,
    ));
    // 逐套件一行
    for bundle in bundles {
        // 件数文本
        let document_count_text: String = bundle.document_count().to_string();
        // 行数文本
        let line_total_text: String = bundle.document_line_total().to_string();
        lines.push(format!(
            "  {}{}{}{}",
            text_layout::pad_right(bundle.factory_channel_label(), CHANNEL_WIDTH),
            text_layout::pad_right(bundle.factory_channel_code().as_str(), CODE_WIDTH),
            text_layout::pad_left(&document_count_text, COUNT_WIDTH),
            text_layout::pad_left(&line_total_text, LINE_COUNT_WIDTH)
        ));
    }
    // 合并
    lines.join("\n")
}
 
/// 把一份单据正文按固定缩进追加到输出行集合中。
///
/// # 参数
/// - `lines`:输出行集合(原地追加)
/// - `document_body`:单据正文
fn push_indented_body(lines: &mut Vec<String>, document_body: &DocumentBody) {
    // 缩进宽度(空格数)
    const INDENT: &str = "    ";
    // 抬头单独成行,便于阅读(用方括号包住,与正文行区分开)
    lines.push(format!("{}【{}】", INDENT, document_body.heading()));
    // 正文逐行缩进
    for body_line in document_body.lines() {
        lines.push(format!("{}{}", INDENT, body_line));
    }
}
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : suite_assembler.rs
//! 套件组装------抽象工厂模式里「Client」角色的唯一动作。
 
use crate::client::suite_bundle::SuiteBundle;
use crate::domain::document_spec::DocumentSpec;
use crate::factory::document_suite_factory::DocumentSuiteFactory;
 
/// 从抽象工厂组装一整套单据。
///
/// # 参数
/// - `factory`:任何实现了 [`DocumentSuiteFactory`] 的工厂(含工程外新增的)
/// - `specification`:单据规格
///
/// # 这个函数为什么值得单独存在
/// 它是**全工程唯一**需要「同时触碰三件抽象产品」的地方。
/// 把它抽成一个自由函数而不是散落在调用点,好处是:
///
/// 1. **扩展性验证的靶心**:`main.rs` 里新增直播渠道时,
///    新增的工厂直接传进来即可,本函数一个字都不用改。
///    如果这段逻辑被复制到 `main.rs` 的每个调用点,扩展时就要改 N 处。
/// 2. **测试/分析可复用**:`analysis` 层批量对照各渠道时调用 `assemble_suites`,
///    不需要自己再拼一遍,从而不存在「组装口径不一致」的可能。
///
/// # 注意它只依赖抽象
/// 函数体内**没有出现任何具体产品类型**(没有 `RetailStorePriceTag` 等)。
/// 这就是「客户端只依赖抽象」在代码上的样子------
/// 换句话说,这个文件根本不知道世界上存在「门店」这个渠道。
pub fn assemble_suite(
    factory: &dyn DocumentSuiteFactory,
    specification: &DocumentSpec,
) -> SuiteBundle {
    SuiteBundle::new(
        // 工厂声明自己属于哪个渠道
        factory.channel_code(),
        // 工厂声明的渠道名称
        factory.channel_label().to_string(),
        // 三件产品全部由同一个工厂产出------「不可能串味」的结构性来源
        factory.create_price_tag(specification),
        factory.create_sales_receipt(specification),
        factory.create_warranty_card(specification),
    )
}
 
/// 批量组装多个渠道的套件。
///
/// # 参数
/// - `factories`:工厂切片(`&[&dyn DocumentSuiteFactory]`)
/// - `specification`:**同一份**单据规格
///
/// # 为什么强调「同一份」
/// 本工程的核心业务问题是「同一批商品在不同渠道的客户实付差多少」。
/// 若每个渠道各用一份规格,差异就可能来自输入而非渠道政策,
/// 结论当场失效。共用同一份 `&DocumentSpec` 在签名上就排除了这种可能。
pub fn assemble_suites(
    factories: &[&dyn DocumentSuiteFactory],
    specification: &DocumentSpec,
) -> Vec<SuiteBundle> {
    // 逐个工厂组装,顺序与入参一致
    factories
        .iter()
        .map(|factory| assemble_suite(*factory, specification))
        .collect()
}
 
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : suite_bundle.rs
//! 套件包------一个渠道产出的整套单据的聚合容器。
 
use crate::domain::channel_code::ChannelCode;
use crate::domain::document_body::DocumentBody;
use crate::product::price_tag::PriceTag;
use crate::product::sales_receipt::SalesReceipt;
use crate::product::warranty_card::WarrantyCard;
 
/// 套件包------某渠道一次交易产出的三件单据。
///
/// ## 它为什么叫「包」而不是「套件」
/// 因为它装的是**已经生产出来的产品实例**(盒子里的东西),
/// 而不是「产品族的配方」。配方在工厂里,成品在这个包里。
///
/// ## 为什么同时记录「工厂声明的渠道」和「产品自报的渠道」
/// 这两个信息在**正常情况下必然一致**------`client` 从同一个工厂
/// 索取三件产品,不可能串味。
///
/// 但一致性是**抽象工厂提供的保证**,值得被验证而不是被假定。
/// 把两边都留下,`analysis` 层的核查器就能在运行期真的去比对:
/// 若将来有人在工程外实现了一个「工厂方法返回了别家产品」的错误工厂,
/// 核查会立刻发现,而不是等到顾客拿错质保卡来投诉。
///
/// **一个可被验证的保证,才配叫保证。**
pub struct SuiteBundle {
    /// 工厂声明自己所属的渠道编码
    factory_channel_code: ChannelCode,
    /// 工厂声明的渠道展示名称
    factory_channel_label: String,
    /// 价格标签(抽象产品)
    price_tag: Box<dyn PriceTag>,
    /// 销售小票(抽象产品)
    sales_receipt: Box<dyn SalesReceipt>,
    /// 质保卡(抽象产品)
    warranty_card: Box<dyn WarrantyCard>,
}
 
impl SuiteBundle {
    /// 组装一个套件包。
    ///
    /// # 参数
    /// - `factory_channel_code`:工厂声明的渠道编码
    /// - `factory_channel_label`:工厂声明的渠道名称
    /// - `price_tag`:价格标签
    /// - `sales_receipt`:销售小票
    /// - `warranty_card`:质保卡
    pub fn new(
        factory_channel_code: ChannelCode,
        factory_channel_label: String,
        price_tag: Box<dyn PriceTag>,
        sales_receipt: Box<dyn SalesReceipt>,
        warranty_card: Box<dyn WarrantyCard>,
    ) -> Self {
        Self {
            // 工厂声明的渠道编码
            factory_channel_code,
            // 工厂声明的渠道名称
            factory_channel_label,
            // 三件产品
            price_tag,
            sales_receipt,
            warranty_card,
        }
    }
 
    /// 读取工厂声明的渠道编码。
    pub fn factory_channel_code(&self) -> ChannelCode {
        // ChannelCode 是 Copy
        self.factory_channel_code
    }
 
    /// 读取工厂声明的渠道名称。
    pub fn factory_channel_label(&self) -> &str {
        // 切片式暴露
        &self.factory_channel_label
    }
 
    /// 读取价格标签。
    pub fn price_tag(&self) -> &dyn PriceTag {
        // 从 Box<dyn T> 取出 &dyn T
        self.price_tag.as_ref()
    }
 
    /// 读取销售小票。
    pub fn sales_receipt(&self) -> &dyn SalesReceipt {
        // 从 Box<dyn T> 取出 &dyn T
        self.sales_receipt.as_ref()
    }
 
    /// 读取质保卡。
    pub fn warranty_card(&self) -> &dyn WarrantyCard {
        // 从 Box<dyn T> 取出 &dyn T
        self.warranty_card.as_ref()
    }
 
    /// 套件内的单据件数。
    ///
    /// 由于类型是固定的三个字段,这个值恒为 3。
    /// 保留方法而不是直接写常量,是为了让报表层不必知道内部结构。
    pub fn document_count(&self) -> usize {
        // 价格标签 + 销售小票 + 质保卡
        3
    }
 
    /// 三件产品各自声明的渠道编码。
    ///
    /// 顺序与 [`SuiteBundle::documents`] 一致,便于核查器逐项比对。
    pub fn product_channel_codes(&self) -> Vec<ChannelCode> {
        vec![
            // 价格标签自报的渠道
            self.price_tag.channel_code(),
            // 销售小票自报的渠道
            self.sales_receipt.channel_code(),
            // 质保卡自报的渠道
            self.warranty_card.channel_code(),
        ]
    }
 
    /// 渲染并返回三件单据的(名称, 正文)。
    ///
    /// # 为什么返回 `Vec` 而不是固定长度的数组
    /// 套件当前固定三件,但「几件」是业务事实而非模式约束
    /// (见 `DocumentSuiteFactory::product_kind_count` 的说明)。
    /// 用 `Vec` 可以让将来「四件套件」的渠道不必改动本方法的签名。
    pub fn documents(&self) -> Vec<(&'static str, DocumentBody)> {
        vec![
            // 价格标签
            ("价格标签", self.price_tag.render()),
            // 销售小票
            ("销售小票", self.sales_receipt.render()),
            // 质保卡
            ("质保卡", self.warranty_card.render()),
        ]
    }
 
    /// 三件单据的正文行数总和。
    ///
    /// 分析层用它衡量各渠道单据的**信息密度**:
    /// 同样是卖一件首饰,免税店要交代购买资格与监管码,
    /// 线上要交代税费构成,门店要交代柜台与条码------
    /// 行数差异直接反映了各渠道的合规负担。
    pub fn document_line_total(&self) -> usize {
        // 逐个渲染并累加行数
        self.documents()
            .iter()
            .map(|(_, body)| body.line_count())
            .sum()
    }
}
  

调用:

rust 复制代码
//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:抽象工厂模式Abstract Factory 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/1 19:24
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : AbstractFactorypattern
//!# File      : main.rs
//! # 抽象工厂模式(Abstract Factory Pattern)· Rust 严格分层实现
//!
//! 业务域:**珠宝多销售渠道的单据套件生成**。
//! 每个渠道必须成套产出三件口径一致的单据:
//! **价格标签 + 销售小票 + 质保卡**。
//!
//! ## 为什么这个业务天然需要抽象工厂
//! 三件产品之间存在**强一致性约束**:
//! - 价格标签写「含税价 18,620」,小票就必须能分离出与之对应的增值税;
//! - 小票写着 6% 增值税,质保卡就不该承诺门店渠道的 24 个月质保;
//! - 免税店价签写「免征」,小票就不该出现非零税额行。
//!
//! 若三件产品各自独立创建,这些约束只能靠「记得」来维持。
//! 抽象工厂把「选渠道」收敛为**一次**决策,三件产品由同一个对象产出------
//! 一致性从「自觉」变成「结构性必然」。
//!
//! ## 严格分层结构
//!
//! ```text
//! main.rs                  入口:六幕编排 + 工程外扩展区(直播渠道 / 错误工厂 / 新客户端逻辑)
//!   │
//!   ├── app                应用层:只排版,不算口径
//!   │     ├── comparison_report.rs   跨渠道对照表 / 一致性核查表 / 税负研究表
//!   │     └── suite_report.rs        套件正文 + 套件目录
//!   │
//!   ├── analysis           分析层:所有业务口径的唯一来源
//!   │     ├── family_consistency.rs  产品族一致性核查(把抽象工厂的承诺变成可验证项)
//!   │     ├── channel_comparison.rs  跨渠道横向对照
//!   │     └── tax_study.rs           渠道税负研究(有效税率 / 实付极差)
//!   │
//!   ├── client             客户端层:只依赖抽象,组装产品族
//!   │     ├── suite_bundle.rs        套件包(承载三件抽象产品)
//!   │     └── suite_assembler.rs     assemble_suite / assemble_suites
//!   │
//!   ├── factory            工厂层:抽象工厂 + 3 个具体工厂 + 渠道登记表
//!   │     ├── document_suite_factory.rs  【抽象工厂 AbstractFactory】
//!   │     ├── retail_store_factory.rs    具体工厂 A(内地线下门店)
//!   │     ├── online_mall_factory.rs     具体工厂 B(线上商城)
//!   │     ├── duty_free_factory.rs       具体工厂 C(市内免税店)
//!   │     └── suite_registry.rs          渠道登记表(把「选工厂」推迟到运行期)
//!   │
//!   ├── product            产品层:抽象产品 + 3 个渠道产品族
//!   │     ├── price_tag.rs               【抽象产品 A】价格标签
//!   │     ├── sales_receipt.rs           【抽象产品 B】销售小票
//!   │     ├── warranty_card.rs           【抽象产品 C】质保卡
//!   │     ├── retail_store_family.rs     ConcreteProduct 组 A
//!   │     ├── online_mall_family.rs      ConcreteProduct 组 B
//!   │     └── duty_free_family.rs        ConcreteProduct 组 C
//!   │
//!   ├── domain             领域层:Money(分) / Currency / Rate(万分比) / ChannelCode
//!   │     └── ItemLine / DocumentSpec / DocumentBody
//!   │
//!   └── support            支持层:纯工具,零业务语义、零依赖
//!         ├── text_layout.rs        CJK 显示宽度 / 对齐 / 截断
//!         ├── date_text.rs          质保到期日(含月末截断与闰年)
//!         └── deterministic_code.rs 条码 / 监管码(确定性派生,非随机)
//! ```
//!
//! ### 依赖方向(严格单向,已用 grep 逐层实测)
//!
//! ```text
//! support   → (无依赖)
//! domain    → (无依赖)
//! product   → domain, support
//! factory   → product, domain
//! client    → factory, product, domain
//! analysis  → client, domain
//! app       → analysis, client, domain, support
//! main      → 以上全部
//! ```
//!
//! 实测结论要点:
//! - `support` 与 `domain` 是**两个零依赖的叶子层**;
//! - `product` 与 `factory` 对 `client` / `analysis` / `app` 的引用数为 **0**
//!   ------这是本模式的架构生命线;
//! - `analysis` 甚至不引用 `factory`:它从 `client` 拿到的套件包已经
//!   足以支撑全部口径计算,不需要认识「工厂」这个概念。
//!
//! **架构生命线**:`product` 与 `factory` 一旦反向引用上层,
//! 「工程外新增一个工厂」就必须回头修改产品层才能被认识,
//! 本文件的扩展性验证会当场失败。
//!
//! ## 抽象工厂的角色落点
//!
//! | 模式角色 | 本工程实现 |
//! |---|---|
//! | AbstractFactory | `factory::DocumentSuiteFactory` |
//! | ConcreteFactory | `RetailStoreSuiteFactory` / `OnlineMallSuiteFactory` / `DutyFreeSuiteFactory` |
//! | AbstractProduct | `product::PriceTag` / `product::SalesReceipt` / `product::WarrantyCard` |
//! | ConcreteProduct | 三个 `*_family.rs` 中的 9 个结构体 |
//! | Client | `client::assemble_suite`(函数体内不出现任何具体产品类型) |
//!
//! ## 扩展性验证
//! 见本文件末尾的「工程外扩展区」:那里定义了一个全新渠道(直播间)
//! 及其完整产品族,并注册进渠道登记表参与全流程对照。
//! **全程未改动任何既有分层文件。** 第四幕、第五幕即为实证。
 
mod analysis;
mod app;
mod client;
mod domain;
mod factory;
mod product;
mod support;
 
// ---------- 领域层 ----------
use domain::{ChannelCode, Currency, DocumentBody, DocumentSpec, ItemLine, Money, Rate};
// ---------- 产品层:抽象产品 trait ----------
use product::{PriceTag, SalesReceipt, WarrantyCard};
// ---------- 产品层:线上商城产品族(仅供「错误工厂」反例使用) ----------
use product::online_mall_family::{OnlineMallPriceTag, OnlineMallSalesReceipt, OnlineMallWarrantyCard};
// ---------- 工厂层 ----------
use factory::{DocumentSuiteFactory, RegistryError, SuiteRegistry};
// ---------- 客户端层 ----------
use client::{assemble_suite, assemble_suites, SuiteBundle};
// ---------- 分析层 ----------
use analysis::{compare_channels, inspect_family_consistency, study_channel_tax};
// ---------- 应用层 ----------
use app::{
    render_channel_comparison, render_consistency_check, render_suite, render_suite_directory,
    render_tax_study,
};
// ---------- 支持层 ----------
use support::{date_text, text_layout};
 
fn main() {
    // ========================================================================
    // 第一幕:抽象工厂装配单套单据(客户端只认抽象)
    // ========================================================================
    print_section_banner("抽象工厂模式 · 珠宝多销售渠道单据套件");
    print_section_banner("第一幕 抽象工厂装配单套单据");
 
    // 构造演示规格:同一批商品、同一笔折扣,后续所有渠道共用这一份
    let specification: DocumentSpec = build_demo_specification();
    // 打印规格概要
    print_specification_summary(&specification);
 
    // 渠道登记表:登记工程内置的三个渠道
    let registry: SuiteRegistry = SuiteRegistry::with_builtin_channels();
    // 登记表为空的兜底分支。正常不会命中,保留它是为了说明
    // 「选工厂失败」时调用方该走哪条路------比静默 unwrap 更负责。
    if registry.is_empty() {
        println!("登记表中没有任何渠道工厂,无法出具单据。");
        return;
    }
 
    // 从登记表按渠道编码取出门店工厂(运行期决定,而非编译期 match)
    match registry.find(ChannelCode::RETAIL_STORE) {
        // 找得到
        Some(retail_store_factory) => {
            // 客户端只做一件事:把抽象工厂交进去,拿回一整套单据
            let retail_store_bundle: SuiteBundle =
                assemble_suite(retail_store_factory, &specification);
            // 输出套件正文
            println!("{}", render_suite(&retail_store_bundle));
            // 价签也能脱离套件单独打印------DocumentBody 是独立可用的值对象
            println!(
                "── 价签可脱离套件单独打印(版式:{})──",
                retail_store_bundle.price_tag().layout_name()
            );
            println!("{}", retail_store_bundle.price_tag().render().to_text());
            println!();
        }
        // 找不到:明确报出而不是静默跳过
        None => println!("未找到门店渠道工厂。"),
    }
 
    // ========================================================================
    // 第二幕:同一份规格 × 三个渠道 = 三套口径各异的单据
    // ========================================================================
    print_section_banner("第二幕 同一份规格 × 三渠道 = 三套口径各异的单据");
 
    // 收集全部已登记工厂(&Box<dyn T> → &dyn T)
    let builtin_factories: Vec<&dyn DocumentSuiteFactory> =
        registry.all().iter().map(|boxed| boxed.as_ref()).collect();
    // 批量装配。共用同一份 specification,从签名上排除「差异来自输入」的可能
    let builtin_bundles: Vec<SuiteBundle> = assemble_suites(&builtin_factories, &specification);
 
    println!("【套件目录】");
    println!("{}", render_suite_directory(&builtin_bundles));
    println!();
    println!("【跨渠道对照】");
    println!("{}", render_channel_comparison(&compare_channels(&builtin_bundles)));
    println!();
 
    // ========================================================================
    // 第三幕:产品族一致性核查------抽象工厂的承诺是可验证的
    // ========================================================================
    print_section_banner("第三幕 产品族一致性核查");
 
    // 逐个渠道核查
    for bundle in &builtin_bundles {
        println!("{}", render_consistency_check(&inspect_family_consistency(bundle)));
        println!();
    }
 
    // 反例:一个「声明自己是门店、却产出线上商城产品」的错误工厂。
    // 注意它能**编译通过**------三件产品都是合法的抽象产品实现。
    // 类型系统挡不住「语义串味」,所以核查必须在运行期真的跑一遍。
    println!("【反例】一个「自称门店、实为线上」的错误工厂:");
    let mismatched_bundle: SuiteBundle =
        assemble_suite(&MismatchedSuiteFactory, &specification);
    println!(
        "{}",
        render_consistency_check(&inspect_family_consistency(&mismatched_bundle))
    );
    println!();
 
    // ========================================================================
    // 第四幕:工程外扩展------登记一个全新渠道(直播渠道)
    // ========================================================================
    print_section_banner("第四幕 工程外扩展:新增直播渠道(未改动任何既有文件)");
 
    // 先取一份内置三渠道的登记表
    let mut extended_registry: SuiteRegistry = SuiteRegistry::with_builtin_channels();
    // 登记工程外新增的直播渠道工厂
    let registration_outcome: Result<(), RegistryError> =
        extended_registry.register(Box::new(LiveStreamSuiteFactory::new()));
    println!(
        "登记直播渠道:{}",
        describe_registration(registration_outcome)
    );
    println!(
        " 渠道名称 {}({})",
        LiveStreamSuiteFactory::new().channel_label(),
        LiveStreamSuiteFactory::new().channel_code()
    );
    // 再次登记同一渠道,验证登记表拒绝重复
    let duplicate_outcome: Result<(), RegistryError> =
        extended_registry.register(Box::new(LiveStreamSuiteFactory::new()));
    println!(
        "重复登记同一渠道:{}",
        describe_registration(duplicate_outcome)
    );
    // 列出登记表全部渠道(渠道编码走领域层的 Display)
    let all_channel_codes: Vec<String> = extended_registry
        .channel_codes()
        .iter()
        .map(|channel_code| channel_code.to_string())
        .collect();
    println!(
        "登记表现有 {} 个渠道:{}",
        extended_registry.len(),
        all_channel_codes.join("、")
    );
    // 产品种类数由抽象工厂的默认实现给出,「三件产品」不是硬编码在报表里的
    println!(
        "每渠道产品种类数:{} 件(取自抽象工厂的默认实现)",
        LiveStreamSuiteFactory::new().product_kind_count()
    );
    println!();
 
    // 用扩展后的登记表重新装配并对照
    let extended_factories: Vec<&dyn DocumentSuiteFactory> = extended_registry
        .all()
        .iter()
        .map(|boxed| boxed.as_ref())
        .collect();
    let extended_bundles: Vec<SuiteBundle> = assemble_suites(&extended_factories, &specification);
    println!("【加入直播渠道后的跨渠道对照】");
    println!("{}", render_channel_comparison(&compare_channels(&extended_bundles)));
    println!();
 
    // 另一条开放轴:货币同样是开放型标签,工程外可直接定义
    const MACAO_PATACA: Currency = Currency::new("MOP", "MOP$", 2);
    // 以扩展货币构造一笔金额(整数最小单位运算,与人民币完全同构)
    let macao_sample_amount: Money = Money::from_major_units(18_620, MACAO_PATACA);
    println!(
        "【另一条开放轴:货币】工程外新增 {} 后可直接参与全工程运算:18,620 元 ⇒ {}",
        MACAO_PATACA.code(),
        macao_sample_amount
    );
    println!();
 
    // ========================================================================
    // 第五幕:工程外扩展------只依赖抽象的新客户端逻辑
    // ========================================================================
    print_section_banner("第五幕 工程外扩展:新写两个只依赖抽象的业务函数");
 
    // 新增逻辑(一):汇总跨渠道质保卡索引
    println!("【跨渠道质保卡索引】");
    for (channel_label, card_number, warranty_months) in collect_warranty_index(&extended_bundles) {
        println!(
            "  · {} ⇒ {}({} 个月,到期 {})",
            channel_label,
            card_number,
            warranty_months,
            // 到期日同样由支持层计算,新增逻辑不必自己处理月份进位
            date_text::add_months(specification.issue_date(), warranty_months)
        );
    }
    println!();
 
    // 新增逻辑(二):稽核异常金额
    let anomaly_findings: Vec<String> = collect_anomaly_findings(&extended_bundles);
    if anomaly_findings.is_empty() {
        // 无异常也要明确说出来,避免「没输出」被误读成「没检查」
        println!("【金额稽核】未发现负金额、零质保期等异常。");
    } else {
        println!("【金额稽核】发现 {} 项异常:", anomaly_findings.len());
        for finding in &anomaly_findings {
            println!("  ! {}", finding);
        }
    }
    println!();
 
    // ========================================================================
    // 第六幕:税负研究------同一批商品,哪个渠道的税务成本更高
    // ========================================================================
    print_section_banner("第六幕 渠道税负研究");
 
    // 汇总税负研究结果
    let tax_study = study_channel_tax(&extended_bundles);
    println!("{}", render_tax_study(&tax_study));
    // 统计有效税率为零的渠道数(免税渠道会命中)
    let zero_effective_rate_count: usize = tax_study
        .entries()
        .iter()
        .filter(|entry| entry.effective_rate.is_zero())
        .count();
    println!(
        "    · 有效税率为零的渠道:{} 个(免税口径)",
        zero_effective_rate_count
    );
    println!();
 
    // ========================================================================
    // 收尾:组合规模
    // ========================================================================
    print_section_banner("收尾 组合规模");
 
    // 渠道数与产品种类数
    let channel_count: usize = extended_registry.len();
    let product_kind_count: usize = LiveStreamSuiteFactory::new().product_kind_count();
    println!(
        "渠道 {} 个 × 产品种类 {} 件 = {} 份单据,全部由同一套抽象装配逻辑产出。",
        channel_count,
        product_kind_count,
        channel_count * product_kind_count
    );
    println!(
        "新增第 {} 个渠道时,product / factory / client / analysis / app 中的既有代码改动行数:0",
        channel_count
    );
    println!(
        "可组合的「渠道 × 产品」形态总数:{} 种,且不存在任何需要维护的分支判断。",
        channel_count * product_kind_count
    );
    println!();
}
 
// ============================================================================
// 演示数据与打印辅助
// ============================================================================
 
/// 构造演示用单据规格。
///
/// # 返回
/// 一份人民币结算、两行明细、一笔订单折扣的规格。
///
/// 数值刻意取整(9,800 / 4,900 / 980),使各渠道的税额可以手工验算:
/// 商品本金 9,800 + 4,900×2 = 19,600;减折扣 980 ⇒ 净额 18,620。
fn build_demo_specification() -> DocumentSpec {
    DocumentSpec::new(
        // 订单号
        "LF20261004-0001",
        // 客户名称
        "陈嘉明",
        // 开单日期
        "2026-10-04",
        // 结算币种
        Currency::CNY,
        // 商品明细(两行三件)
        vec![
            ItemLine::new(
                // 货号
                "AU2201",
                // 名称
                "足金999项链",
                // 数量
                1,
                // 单价
                Money::from_major_units(9_800, Currency::CNY),
                // 材质说明
                "足金 999 / 净重 12.5g",
            ),
            ItemLine::new(
                // 货号
                "DM3315",
                // 名称
                "18K金钻石戒指",
                // 数量
                2,
                // 单价
                Money::from_major_units(4_900, Currency::CNY),
                // 材质说明
                "18K 金 / 钻石 0.35ct",
            ),
        ],
        // 订单级折扣
        Money::from_major_units(980, Currency::CNY),
        // 客户等级
        "黄金 VIP",
    )
}
 
/// 打印单据规格概要(顺带覆盖领域层的若干只读接口)。
///
/// # 参数
/// - `specification`:单据规格
fn print_specification_summary(specification: &DocumentSpec) {
    // 折扣力度(万分比整数),用于展示「比率也是整数存储」
    let discount_basis_points: i64 = specification.discount_intensity().basis_points();
    println!("【单据规格】");
    println!(
        "  订单号 {} 客户 {}({}) 开单日 {}",
        specification.order_reference(),
        specification.customer_name(),
        specification.customer_tier_label(),
        specification.issue_date()
    );
    println!(
        "  币种 {} 明细 {} 行 总件数 {}",
        specification.currency(),
        specification.item_count(),
        specification.total_quantity()
    );
    println!(
        "  商品本金 {} 订单折扣 {} 净额(结算基数) {}",
        specification.goods_subtotal().formatted(),
        specification.order_discount().formatted(),
        specification.net_amount().formatted()
    );
    println!(
        "  折扣力度 {}(万分比整数 {})",
        specification.discount_intensity().as_percent_text(),
        discount_basis_points
    );
    println!();
}
 
/// 打印一条居中的小节横幅。
///
/// # 参数
/// - `title`:横幅标题
///
/// # 为什么要自己算宽度
/// 标题含中文,`{:^96}` 会按字符数居中而错位。这里走支持层的
/// `pad_center`(内部用 `display_width` 按显示列计算)。
fn print_section_banner(title: &str) {
    // 横幅宽度(显示列)
    const BANNER_WIDTH: usize = 96;
    // 标题显示宽度
    let title_width: usize = text_layout::display_width(title);
    // 标题过宽时给出提示但**不截断**------宁可横幅变长,也不丢信息
    if title_width > BANNER_WIDTH {
        println!(
            "(标题显示宽度 {} 超过横幅 {},按原样输出)",
            title_width, BANNER_WIDTH
        );
    }
    // 上分隔线
    println!("{}", text_layout::horizontal_rule('=', BANNER_WIDTH));
    // 居中标题
    println!("{}", text_layout::pad_center(title, BANNER_WIDTH));
    // 下分隔线
    println!("{}", text_layout::horizontal_rule('=', BANNER_WIDTH));
}
 
/// 把渠道登记结果描述为一行中文文本。
///
/// # 参数
/// - `registration_outcome`:登记结果
///
/// # 为什么单独抽一个函数
/// 登记成功与失败要给出**不同措辞**的可读结论。把这段映射
/// 从 `main` 里抽出来,可以让主流程保持「一句话一件事」的节奏;
/// 同时它显式地按 `RegistryError` 的变体消费错误,
/// 而不是简单 `unwrap` 或吞掉。
fn describe_registration(registration_outcome: Result<(), RegistryError>) -> String {
    match registration_outcome {
        // 登记通过
        Ok(()) => "通过".to_string(),
        // 登记被拒,附上错误类型给出的可执行说明
        Err(registry_error) => format!("被拒绝 ------ {}", registry_error),
    }
}
 
/// 工程外新增的客户端逻辑(一):汇总各渠道质保卡索引。
///
/// # 参数
/// - `bundles`:套件包切片
///
/// # 返回
/// 每项为(渠道名称, 质保卡编号, 质保月数)。
///
/// # 关键点
/// 本函数**只用抽象产品 trait**(`WarrantyCard::card_number` /
/// `warranty_months`),因此它对「世界上存在几个渠道、分别叫什么」
/// 完全无知。新增渠道后它自动覆盖到新渠道,无需修改。
fn collect_warranty_index(bundles: &[SuiteBundle]) -> Vec<(String, String, u32)> {
    bundles
        .iter()
        .map(|bundle| {
            // 取质保卡(抽象产品)
            let warranty_card = bundle.warranty_card();
            (
                // 渠道名称
                bundle.factory_channel_label().to_string(),
                // 卡号
                warranty_card.card_number().to_string(),
                // 质保月数
                warranty_card.warranty_months(),
            )
        })
        .collect()
}
 
/// 工程外新增的客户端逻辑(二):稽核异常金额与无效质保期。
///
/// # 参数
/// - `bundles`:套件包切片
///
/// # 返回
/// 异常描述清单;无异常时为空向量。
fn collect_anomaly_findings(bundles: &[SuiteBundle]) -> Vec<String> {
    // 收集异常描述
    let mut findings: Vec<String> = Vec::new();
    // 逐个套件检查
    for bundle in bundles {
        // 销售小票(抽象产品)
        let sales_receipt = bundle.sales_receipt();
        // 负的客户实付:记价错误
        if sales_receipt.total_amount().is_negative() {
            findings.push(format!(
                "{}:客户实付为负({}),疑似记价错误",
                bundle.factory_channel_label(),
                // 展示绝对值,避免读者被负号干扰
                sales_receipt.total_amount().absolute().formatted()
            ));
        }
        // 负的税额:税务口径异常
        if sales_receipt.tax_amount().is_negative() {
            findings.push(format!(
                "{}:税额为负({})",
                bundle.factory_channel_label(),
                sales_receipt.tax_amount().absolute().formatted()
            ));
        }
        // 零质保期:质保卡形同虚设
        if bundle.warranty_card().warranty_months() == 0 {
            findings.push(format!("{}:质保期为 0", bundle.factory_channel_label()));
        }
    }
    findings
}
 
// ============================================================================
// 工程外扩展区
// ============================================================================
//
// ⚠️ 本节以下所有代码都写在 main.rs 中,**不属于任何既有分层文件**。
//
// 它的存在只为一件事:实证「完全可扩展」。
// 新增一个完整渠道(1 个渠道编码 + 3 件产品 + 1 个工厂),
// 不需要修改 product / factory / client / analysis / app 中的任何一行。
 
/// 工程外新增渠道:直播间渠道编码。
const LIVE_STREAM_CHANNEL: ChannelCode = ChannelCode::new("LIVE_STREAM");
 
/// 工程外新增渠道的展示名称。
const LIVE_STREAM_CHANNEL_LABEL: &str = "直播间渠道";
 
/// 直播渠道增值税率:13%(价内税)。
const LIVE_STREAM_VAT_RATE: Rate = Rate::from_basis_points(1_300);
 
/// 直播渠道佣金率:5%(按不含税净额计,价外)。
const LIVE_STREAM_COMMISSION_RATE: Rate = Rate::from_basis_points(500);
 
/// 直播渠道质保期:6 个月(最短------冲动型消费的售后政策)。
const LIVE_STREAM_WARRANTY_MONTHS: u32 = 6;
 
/// 从直播渠道含税净额中分离增值税(13 / 113)。
///
/// # 参数
/// - `tax_inclusive_amount`:含税金额
fn separate_live_stream_vat(tax_inclusive_amount: Money) -> Money {
    // 与门店渠道同构:分子 13,分母 113
    tax_inclusive_amount.scale_by_ratio(1_300, 11_300)
}
 
// ---------------------------------------------------------------------------
// 工程外产品 A-1:直播渠道价格标签
// ---------------------------------------------------------------------------
 
/// 直播渠道价格标签。
struct LiveStreamPriceTag {
    /// 单据规格
    specification: DocumentSpec,
}
 
impl LiveStreamPriceTag {
    /// 构造。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn new(specification: DocumentSpec) -> Self {
        Self {
            // 持有规格
            specification,
        }
    }
}
 
impl PriceTag for LiveStreamPriceTag {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        LIVE_STREAM_CHANNEL
    }
 
    /// 展示金额=直播专享价(含税)。
    fn display_amount(&self) -> Money {
        self.specification.net_amount()
    }
 
    /// 版式名称。
    fn layout_name(&self) -> &'static str {
        // 覆盖默认版式名
        "直播间专享价签(含税价)"
    }
 
    /// 渲染价签正文。
    fn render(&self) -> DocumentBody {
        // 键列显示宽度
        const KEY_WIDTH: usize = 18;
        // 含税专享价
        let tax_inclusive_amount: Money = self.display_amount();
        // 分离增值税
        let vat_amount: Money = separate_live_stream_vat(tax_inclusive_amount);
        // 不含税价
        let excluding_tax_amount: Money = tax_inclusive_amount.subtract(vat_amount);
        // 主播佣金(按不含税价计)
        let commission_amount: Money = LIVE_STREAM_COMMISSION_RATE.apply_to(excluding_tax_amount);
        // 主打商品名
        let primary_product_name: String = self
            .specification
            .items()
            .first()
            .map(|item_line| item_line.product_name().to_string())
            .unwrap_or_else(|| "(本单无商品明细)".to_string());
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("渠道", KEY_WIDTH),
            "直播间渠道(LIVE_STREAM)"
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("订单号", KEY_WIDTH),
            self.specification.order_reference()
        ));
        lines.push(text_layout::horizontal_rule('-', 62));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("主打商品", KEY_WIDTH),
            primary_product_name
        ));
        lines.push(text_layout::horizontal_rule('-', 62));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("直播专享价", KEY_WIDTH),
            format!("= {} =", tax_inclusive_amount.formatted())
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("其中增值税", KEY_WIDTH),
            format!(
                "{}({} 价内税)",
                vat_amount.formatted(),
                LIVE_STREAM_VAT_RATE.as_percent_text()
            )
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("不含税价", KEY_WIDTH),
            excluding_tax_amount.formatted()
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("主播佣金", KEY_WIDTH),
            format!(
                "{}({},结算时另计)",
                commission_amount.formatted(),
                LIVE_STREAM_COMMISSION_RATE.as_percent_text()
            )
        ));
        lines.push(text_layout::horizontal_rule('-', 62));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("提示", KEY_WIDTH),
            "直播间价格已含税,佣金由平台向商家收取,不计入顾客实付。"
        ));
        // 组装正文
        DocumentBody::new("价格标签 · 我的珠宝直播间", lines)
    }
}
 
// ---------------------------------------------------------------------------
// 工程外产品 A-2:直播渠道销售小票
// ---------------------------------------------------------------------------
 
/// 直播渠道销售小票。
struct LiveStreamSalesReceipt {
    /// 单据规格
    specification: DocumentSpec,
}
 
impl LiveStreamSalesReceipt {
    /// 构造。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn new(specification: DocumentSpec) -> Self {
        Self {
            // 持有规格
            specification,
        }
    }
}
 
impl SalesReceipt for LiveStreamSalesReceipt {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        LIVE_STREAM_CHANNEL
    }
 
    /// 客户实付=含税净额 + 平台佣金中由顾客承担的部分。
    fn total_amount(&self) -> Money {
        // 价内税净额 + 附加费
        self.specification.net_amount().add(self.surcharge_amount())
    }
 
    /// 列示税额(价内税分离)。
    fn tax_amount(&self) -> Money {
        // 从含税净额中分离
        separate_live_stream_vat(self.specification.net_amount())
    }
 
    /// 列示的附加费用=按不含税净额计的 5% 佣金。
    fn surcharge_amount(&self) -> Money {
        // 不含税净额
        let excluding_tax_amount: Money = self
            .specification
            .net_amount()
            .subtract(self.tax_amount());
        // 按不含税净额计佣
        LIVE_STREAM_COMMISSION_RATE.apply_to(excluding_tax_amount)
    }
 
    /// 直播渠道为价内税。
    fn is_tax_inclusive(&self) -> bool {
        // 专享价已含税
        true
    }
 
    /// 渲染小票正文。
    fn render(&self) -> DocumentBody {
        // 商品名列宽
        const PRODUCT_WIDTH: usize = 30;
        // 数量列宽
        const QUANTITY_WIDTH: usize = 6;
        // 单价列宽
        const UNIT_PRICE_WIDTH: usize = 16;
        // 小计列宽
        const SUBTOTAL_WIDTH: usize = 16;
        // 合计区键列宽
        const KEY_WIDTH: usize = 28;
        // 合计区值列宽
        const VALUE_WIDTH: usize = 22;
        // 商品本金
        let goods_subtotal: Money = self.specification.goods_subtotal();
        // 含税净额
        let tax_inclusive_amount: Money = self.specification.net_amount();
        // 增值税
        let vat_amount: Money = self.tax_amount();
        // 佣金
        let commission_amount: Money = self.surcharge_amount();
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        lines.push(format!(
            "客户:{}({})    开单日期:{}",
            self.specification.customer_name(),
            self.specification.customer_tier_label(),
            self.specification.issue_date()
        ));
        lines.push(text_layout::horizontal_rule('-', 78));
        // 商品明细(直播渠道行数较少,省去材质子行,体现「信息密度」差异)
        for (index, item_line) in self.specification.items().iter().enumerate() {
            lines.push(format!(
                "  {}{}{}{}",
                text_layout::pad_right(&format!("{}.", index + 1), 6),
                text_layout::pad_right(item_line.product_name(), PRODUCT_WIDTH),
                text_layout::pad_left(&item_line.quantity().to_string(), QUANTITY_WIDTH),
                text_layout::pad_left(&item_line.subtotal().formatted(), SUBTOTAL_WIDTH)
            ));
            lines.push(format!(
                "     {}{}",
                text_layout::pad_left(&item_line.unit_price().formatted(), UNIT_PRICE_WIDTH),
                "(单价)"
            ));
        }
        lines.push(text_layout::horizontal_rule('-', 78));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("商品本金", KEY_WIDTH),
            text_layout::pad_left(&goods_subtotal.formatted(), VALUE_WIDTH)
        ));
        if self.specification.has_discount() {
            lines.push(format!(
                "{}{}",
                text_layout::pad_right(
                    &format!(
                        "订单折扣(力度 {})",
                        self.specification.discount_intensity().as_percent_text()
                    ),
                    KEY_WIDTH
                ),
                text_layout::pad_left(
                    &self.specification.order_discount().negate().formatted(),
                    VALUE_WIDTH
                )
            ));
        }
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("含税净额", KEY_WIDTH),
            text_layout::pad_left(&tax_inclusive_amount.formatted(), VALUE_WIDTH)
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right(
                &format!(" 其中 增值税 {}", LIVE_STREAM_VAT_RATE.as_percent_text()),
                KEY_WIDTH
            ),
            text_layout::pad_left(&vat_amount.formatted(), VALUE_WIDTH)
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right(
                &format!("加:平台佣金 {}", LIVE_STREAM_COMMISSION_RATE.as_percent_text()),
                KEY_WIDTH
            ),
            text_layout::pad_left(&commission_amount.formatted(), VALUE_WIDTH)
        ));
        lines.push(text_layout::horizontal_rule('=', 78));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("客户实付", KEY_WIDTH),
            text_layout::pad_left(&self.total_amount().formatted(), VALUE_WIDTH)
        ));
        lines.push("价内税 + 价外佣金:专享价已含税,佣金按不含税净额另计。".to_string());
        // 组装正文
        DocumentBody::new("销售小票 · 我的珠宝直播间", lines)
    }
}
 
// ---------------------------------------------------------------------------
// 工程外产品 A-3:直播渠道质保卡
// ---------------------------------------------------------------------------
 
/// 直播渠道质保卡。
struct LiveStreamWarrantyCard {
    /// 单据规格
    specification: DocumentSpec,
    /// 质保卡编号
    card_number: String,
}
 
impl LiveStreamWarrantyCard {
    /// 构造。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn new(specification: DocumentSpec) -> Self {
        // 卡号 = 渠道前缀 + 订单号;直播渠道前缀为 WLV
        let card_number: String = format!("WLV-{}", specification.order_reference());
        Self {
            // 持有规格
            specification,
            // 缓存卡号
            card_number,
        }
    }
}
 
impl WarrantyCard for LiveStreamWarrantyCard {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        LIVE_STREAM_CHANNEL
    }
 
    /// 质保月数。
    fn warranty_months(&self) -> u32 {
        // 直播渠道标准质保期
        LIVE_STREAM_WARRANTY_MONTHS
    }
 
    /// 质保卡编号。
    fn card_number(&self) -> &str {
        // 切片式暴露
        &self.card_number
    }
 
    /// 渲染质保卡正文。
    fn render(&self) -> DocumentBody {
        // 键列显示宽度
        const KEY_WIDTH: usize = 14;
        // 到期日
        let expiry_date: String =
            date_text::add_months(self.specification.issue_date(), self.warranty_months());
        // 逐行收集
        let mut lines: Vec<String> = Vec::new();
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("质保卡编号", KEY_WIDTH),
            self.card_number()
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("持卡人", KEY_WIDTH),
            self.specification.customer_name()
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("质保期", KEY_WIDTH),
            format!("{} 个月", self.warranty_months())
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("起算日", KEY_WIDTH),
            self.specification.issue_date()
        ));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("到期日", KEY_WIDTH),
            expiry_date
        ));
        lines.push(text_layout::horizontal_rule('-', 58));
        lines.push(format!(
            "{}{}",
            text_layout::pad_right("服务网络", KEY_WIDTH),
            "直播间客服专属通道"
        ));
        lines.push(format!("{}条款:", text_layout::pad_right("", KEY_WIDTH)));
        lines.push("  · 质保范围涵盖制造工艺缺陷,不含人为损坏与正常磨损。".to_string());
        lines.push("  · 直播渠道质保期为 6 个月,短于门店与免税渠道。".to_string());
        lines.push("  · 需通过直播间客服发起售后工单,凭卡号建档。".to_string());
        // 组装正文
        DocumentBody::new("质保卡 · 我的珠宝直播间", lines)
    }
}
 
// ---------------------------------------------------------------------------
// 工程外工厂:直播渠道套件工厂
// ---------------------------------------------------------------------------
 
/// 直播渠道套件工厂(ConcreteFactory D,工程外新增)。
struct LiveStreamSuiteFactory;
 
impl LiveStreamSuiteFactory {
    /// 构造。
    fn new() -> Self {
        // 零字段结构体
        Self
    }
}
 
impl DocumentSuiteFactory for LiveStreamSuiteFactory {
    /// 渠道编码。
    fn channel_code(&self) -> ChannelCode {
        LIVE_STREAM_CHANNEL
    }
 
    /// 渠道展示名称。
    fn channel_label(&self) -> &'static str {
        LIVE_STREAM_CHANNEL_LABEL
    }
 
    /// 生产直播渠道价格标签。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_price_tag(&self, specification: &DocumentSpec) -> Box<dyn PriceTag> {
        // 复制规格
        Box::new(LiveStreamPriceTag::new(specification.clone()))
    }
 
    /// 生产直播渠道销售小票。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_sales_receipt(&self, specification: &DocumentSpec) -> Box<dyn SalesReceipt> {
        // 复制规格
        Box::new(LiveStreamSalesReceipt::new(specification.clone()))
    }
 
    /// 生产直播渠道质保卡。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_warranty_card(&self, specification: &DocumentSpec) -> Box<dyn WarrantyCard> {
        // 复制规格
        Box::new(LiveStreamWarrantyCard::new(specification.clone()))
    }
}
 
// ---------------------------------------------------------------------------
// 工程外反例:一个语义串味的错误工厂
// ---------------------------------------------------------------------------
 
/// 一个**故意写错**的工厂:声明自己是门店渠道,却产出线上商城的产品。
///
/// # 为什么保留这个反例
/// 它能**编译通过**------三件产品都是合法的抽象产品实现,类型完全正确。
/// 这说明类型系统挡不住「语义串味」:只要有人手工拼装产品,
/// 抽象工厂的结构性保证就被绕过了。
///
/// 因此本工程的 `analysis::family_consistency` 核查器不是装饰性代码,
/// 而是真正兜住这类问题的那一层。第三幕会展示它成功地抓出这个工厂。
struct MismatchedSuiteFactory;
 
impl DocumentSuiteFactory for MismatchedSuiteFactory {
    /// 声明自己是门店渠道。
    fn channel_code(&self) -> ChannelCode {
        // 声明:门店
        ChannelCode::RETAIL_STORE
    }
 
    /// 渠道展示名称(主动标出这是错误工厂)。
    fn channel_label(&self) -> &'static str {
        "(错误工厂)自称门店"
    }
 
    /// 出错点:门店工厂却产出线上商城的价格标签。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_price_tag(&self, specification: &DocumentSpec) -> Box<dyn PriceTag> {
        // 串味:线上产品
        Box::new(OnlineMallPriceTag::new(specification.clone()))
    }
 
    /// 出错点:门店工厂却产出线上商城的销售小票。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_sales_receipt(&self, specification: &DocumentSpec) -> Box<dyn SalesReceipt> {
        // 串味:线上产品
        Box::new(OnlineMallSalesReceipt::new(specification.clone()))
    }
 
    /// 出错点:门店工厂却产出线上商城的质保卡。
    ///
    /// # 参数
    /// - `specification`:单据规格
    fn create_warranty_card(&self, specification: &DocumentSpec) -> Box<dyn WarrantyCard> {
        // 串味:线上产品
        Box::new(OnlineMallWarrantyCard::new(specification.clone()))
    }
}

输出:

相关推荐
pe7er1 小时前
Spring Boot 日志最佳实践:接入、分级、异常记录与滚动拆分
后端
“AI国潮设计-小江”1 小时前
《Python+SDXL实战:用ControlNet批量生成“英歌舞麻将糕”IP,附自动化脚本与商用思路》
开发语言·人工智能·python·prompt·aigc
Cc.Y1 小时前
Java零基础入门:封装与继承 —— 从“裸奔“到“穿衣服“,从“重复造轮子“到“站在巨人肩膀上“
java·开发语言
Escalating_xu1 小时前
【C 语言】深入理解指针(4):回调函数、qsort、void * 与泛型排序的模拟实现
java·c语言·开发语言
吠品2 小时前
GitHub 周榜拆解:决策小模型扎堆上位
c语言·开发语言·算法
见叶之秋2 小时前
C++ 泛型世界的两块拼图:容器适配器与仿函数
java·开发语言
程序员老赵2 小时前
Docker 部署 Dolibarr:轻松搭建开源 ERP/CRM 平台
运维·前端·后端
逗脑IDE3 小时前
零基础学ESP32:RFID无线射频卡——让ESP32拥有“刷卡“能力!
开发语言·网络·人工智能·python·esp32·硬件开发