项目结构:

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