rust: pool Pattern

项目结构:

rust 复制代码
//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : arithmetic.rs
//! 整数算术:全工程**唯一**的舍入与比率口径。
//!
//! ## 为什么单独一个文件
//!
//! 「算账的除法」在工程里只能有一处实现。散在各处写 `a / b` 会带来两个后果:
//! ① 整数除法向零截断,与手工口径差 1 分,而这种差 1 分在报表上**看不出来**;
//! ② 以后想统一改成「银行家舍入」要改十几个地方。
//! 因此本文件收口,其余各层一律调用 [`divide_rounded`]。
//!
//! ## 为什么用 `i128` 做中间量
//!
//! 金额用「分」存储,金重用「毫克」,比率用「万分点」。
//! 「分 × 万分点」已经是 10^6 量级,再乘一个金重(毫克,可达 10^6)
//! 会到 10^12------`i64` 还剩得下,但**再来一次乘法就悬了**。
//! 中间量一律 `i128`,出口处再受控地转回 `i64`。
//!
//! ## ★ 舍入方向:远离零
//!
//! `divide_rounded(-a, b) == -divide_rounded(a, b)` 是本层的恒等式,
//! 工程验收会真的跑它(`main.rs` 的体检表)。
//! 向零截断(Rust 的 `/` 默认行为)**不满足**这条恒等式,
//! 于是「退 1 分」与「收 1 分」的舍入方向不一致,差额会在合计里累积。

/// 把 `numerator / denominator` 按**远离零方向四舍五入**成一个 `i64`。
///
/// # 参数
/// - `numerator`:分子(可为负)。
/// - `denominator`:分母(**必须非 0**)。
///
/// # Panics
/// 分母为 0 时 panic。这是**调用方的逻辑错误**,不是可以降级的业务情况------
/// 返回 0 会让「这一项除不出来」变成「这一项是 0 元」,而报表看起来完全正常。
pub fn divide_rounded(numerator: i128, denominator: i128) -> i64 {
    assert!(
        denominator != 0,
        "除数为零:这不是可降级的业务情况,静默返回 0 会让报表看起来正常"
    );
    // 符号单独记:只在最后乘回去,中间全按正数算,避免负号干扰取余。
    let is_negative: bool = (numerator < 0) != (denominator < 0);
    // ⚠️ 用 `unsigned_abs` 而不是 `abs`:`i128::MIN` 的 `abs()` 会溢出 panic。
    let dividend: u128 = numerator.unsigned_abs();
    let divisor: u128 = denominator.unsigned_abs();
    let quotient: u128 = dividend / divisor;
    let remainder: u128 = dividend % divisor;
    // 远离零方向:余数达到除数的一半就进 1。
    let adjusted: u128 = if remainder * 2 >= divisor {
        quotient + 1
    } else {
        quotient
    };
    let magnitude: i128 = i128::try_from(adjusted).expect("除法结果超出 i128 可表示范围");
    let signed: i128 = if is_negative { -magnitude } else { magnitude };
    i64::try_from(signed).expect("除法结果超出 i64 可表示范围")
}

/// 比率转万分点:`numerator / denominator × 10000`,四舍五入。
///
/// ## 为什么比率一律用「万分点」整数
///
/// 毛利率 13.00% 与 13.005% 在浮点里都能表示,但**能不能精确比较**是另一回事:
/// 两个「看起来相等」的浮点比率在报表上可能因 1e-15 的差而分叉。
/// 万分点整数没有这个问题,且 `1300` 这种数字读者能直接读成 `13.00%`。
pub fn ratio_basis_points(numerator: i128, denominator: i128) -> i64 {
    divide_rounded(numerator * 10_000, denominator)
}

/// 按整数比例缩放:`value × numerator / denominator`,四舍五入。
///
/// 「金重(毫克) × 金价(分/克) / 1000」这类算式一律走这里,
/// 不在业务代码里裸写乘除。
pub fn scale_rounded(value: i64, numerator: i128, denominator: i128) -> i64 {
    divide_rounded(i128::from(value) * numerator, denominator)
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : currency_code.rs
//! 币种:开放型标签(`const fn new` 结构体,不是 `enum`)。
//!
//! ## ★ 判据:会不会被当作查找键 + 会不会不断增加
//!
//! - **会不断增加**(接新地区就多一个币种)→ 开放型结构体,
//!   工程外新增只需在装配点声明一个 `const`,**不用回来改本文件**;
//! - **会被当作查找键** → 实现 `PartialEq`(汇率表按币种查);
//! - 但**不实现 `Ord` / `Hash`**:本工程从不排序币种、也不拿币种做哈希键,
//!   不实现用不上的 trait 本身就是一种防错(多实现一个,就多一处「看起来能用」的用法)。
//!
//! ## ⚠️ 为什么字段是 `&'static str`
//!
//! `String` 会让 `CurrencyCode` 不能 `Copy`,于是 `Money` 也不能 `Copy`,
//! 而金额在本工程里到处按值传递。币种编码是编译期常量,
//! 用 `&'static str` 换 `Copy` 是划算的。

/// 币种:`code` 是机器编码,`chinese_name` 给人读,`symbol` 用于排版。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct CurrencyCode {
    /// 机器编码,如 `"CNY"`。
    code: &'static str,
    /// 中文名称,如 `"人民币"`。
    chinese_name: &'static str,
    /// 金额符号,如 `"¥"`。
    symbol: &'static str,
}

impl CurrencyCode {
    /// 声明一个币种。`const fn` 使得它可以写进 `const` 常量里。
    pub const fn new(code: &'static str, chinese_name: &'static str, symbol: &'static str) -> Self {
        Self {
            code,
            chinese_name,
            symbol,
        }
    }

    /// 机器编码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

    /// 中文名称。
    pub const fn chinese_name(&self) -> &'static str {
        self.chinese_name
    }

    /// 金额符号。
    pub const fn symbol(&self) -> &'static str {
        self.symbol
    }
}

/// 人民币:集团合并口径的记账本位币。
pub const CNY: CurrencyCode = CurrencyCode::new("CNY", "人民币", "¥");
/// 港币:中国香港门店的本位币。
pub const HKD: CurrencyCode = CurrencyCode::new("HKD", "港币", "HK$");
/// 美元:跨境采购结算币。
pub const USD: CurrencyCode = CurrencyCode::new("USD", "美元", "US$");

/// 集团允许出现的币种全集。
///
/// ⚠️ 这张表只用于**校验**(流水的币种必须在这里面),
/// 不用于「枚举有哪些币种」------那会把新增币种变成改领域层文件。
pub const BUILTIN_CURRENCY_CODES: [CurrencyCode; 3] = [CNY, HKD, USD];

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : instrument_model.rs
//! 检测仪器型号:**开放型标签**。
//!
//! ## 为什么开放
//!
//! 型号会不断增加(新购一批激光打标机就是一个新型号)。
//! 若做成 `enum`,工程外新增型号要回来改领域层文件------
//! 那与「可扩展性必须被实证」这条硬纪律直接冲突。
//!
//! ## ★ 型号自带「采购单价」与「校准周期」
//!
//! 这两项是**型号的属性**而不是个体的属性,因此放在型号上:
//!
//! - 采购单价用来量化对象池省下的钱(复用一次 = 省下一次采购);
//! - 校准周期决定归还时「还能不能直接回池」。
//!
//! ⚠️ 采购单价是 `Money`(带币种),不是裸整数:
//! 集团采购里有港币结算的设备,裸整数相加会得出一个「看起来对」的数。

use crate::domain::{CurrencyCode, Money};

/// 检测仪器型号。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct InstrumentModel {
    /// 机器编码,如 `"BALANCE"`。
    code: &'static str,
    /// 中文名称,如 `"电子天平"`。
    chinese_name: &'static str,
    /// 用途说明(写进报表,让读者知道这设备干什么用)。
    purpose_text: &'static str,
    /// ★ 采购单价(带币种)。省下的复用成本按它算。
    acquisition_cost: Money,
    /// 校准周期(月)。归还时若已超过校准到期月,必须送校准。
    calibration_interval_months: u32,
}

impl InstrumentModel {
    /// 声明一个型号。
    pub const fn new(
        code: &'static str,
        chinese_name: &'static str,
        purpose_text: &'static str,
        acquisition_cost: Money,
        calibration_interval_months: u32,
    ) -> Self {
        Self {
            code,
            chinese_name,
            purpose_text,
            acquisition_cost,
            calibration_interval_months,
        }
    }

    /// 机器编码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

    /// 中文名称。
    pub const fn chinese_name(&self) -> &'static str {
        self.chinese_name
    }

    /// 用途说明。
    pub const fn purpose_text(&self) -> &'static str {
        self.purpose_text
    }

    /// 采购单价。
    pub const fn acquisition_cost(&self) -> Money {
        self.acquisition_cost
    }

    /// 校准周期(月)。
    pub const fn calibration_interval_months(&self) -> u32 {
        self.calibration_interval_months
    }

    /// 币种(采购单价的币种)。
    pub const fn currency(&self) -> CurrencyCode {
        self.acquisition_cost.currency()
    }
}

/// 电子天平(内置)。
pub const MODEL_BALANCE: InstrumentModel = InstrumentModel::new(
    "BALANCE",
    "电子天平",
    "称量金重(0.001 克精度)",
    Money::new(480_000, crate::domain::CNY),
    12,
);

/// 测金仪(内置)。
pub const MODEL_GOLD_TESTER: InstrumentModel = InstrumentModel::new(
    "GOLD_TESTER",
    "测金仪",
    "无损检测成色",
    Money::new(1_260_000, crate::domain::CNY),
    6,
);

/// 宝石显微镜(内置)。
pub const MODEL_GEM_MICROSCOPE: InstrumentModel = InstrumentModel::new(
    "GEM_MICROSCOPE",
    "宝石显微镜",
    "观察内含物与切工",
    Money::new(890_000, crate::domain::CNY),
    12,
);

/// 内置型号全集(**装配用**,不是「合法型号白名单」)。
pub const BUILTIN_INSTRUMENT_MODELS: [InstrumentModel; 3] = [
    MODEL_BALANCE,
    MODEL_GOLD_TESTER,
    MODEL_GEM_MICROSCOPE,
];


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : instrument_state.rs
//! 仪器生命周期状态:**封闭枚举**。
//!
//! ## 为什么这里是 `enum` 而不是 `const fn new` 结构体
//!
//! 判据是同一条:**这个维度会不会不断增加**。
//! 门店、型号、用途会不断增加 → 开放型;
//! 「一台仪器此刻处于什么状态」**不会**------状态机的取值在设计时就是固定的,
//! 手工把它开放出来,等于放弃编译器对「所有状态都被处理」的检查。
//!
//! ## ★ 四个状态的判据是「此刻它在不在池里」
//!
//! 对象池的全部语义都建立在「**在池里**」与「**不在池里**」的区分上:
//!
//! - `Idle`        ------ 在池里,可被借出;
//! - `InUse`       ------ 不在池里,被某家门店持有;
//! - `InCalibration`------ 不在池里,归还时校验没过,送去校准/维修;
//! - `Retired`     ------ 永久离池(累计使用次数到上限或不可修复)。
//!
//! ⚠️ 只有 `Idle` 算「在池里」。`InCalibration` 看起来像是「暂时不在」,
//! 但它**不能被借出**------若把它算进可用数,池的容量守恒就会在月底对不上。

/// 一台仪器的生命周期状态。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum InstrumentState {
    /// 在池中空闲:可被借出。
    Idle,
    /// 借出使用中:不在池中。
    InUse,
    /// 校准 / 维修中:不在池中,且**不可借出**。
    InCalibration,
    /// 已退役:永久离池。
    Retired,
}

impl InstrumentState {
    /// 中文名称。
    pub const fn chinese_text(&self) -> &'static str {
        match self {
            InstrumentState::Idle => "在池空闲",
            InstrumentState::InUse => "借出使用",
            InstrumentState::InCalibration => "校准维修",
            InstrumentState::Retired => "已退役",
        }
    }

    /// 机器编码(写进指纹,便于跨运行比对)。
    pub const fn machine_text(&self) -> &'static str {
        match self {
            InstrumentState::Idle => "IDLE",
            InstrumentState::InUse => "IN_USE",
            InstrumentState::InCalibration => "IN_CALIBRATION",
            InstrumentState::Retired => "RETIRED",
        }
    }

    /// ★ 此刻是否**在池里**(对象池的可用库存口径)。
    ///
    /// 只有 `Idle` 为真。见模块文档:把 `InCalibration` 算进来会让容量守恒失效。
    pub const fn is_in_pool(&self) -> bool {
        matches!(self, InstrumentState::Idle)
    }

    /// 是否可被借出。
    ///
    /// ⚠️ 与 [`InstrumentState::is_in_pool`] **同义但不同名**:
    /// 两个名字对应两个问题(「在不在池里」「能不能借」),
    /// 现在答案恰好一样,将来若引入「预约锁定」就会分开------
    /// 那时只有一个名字的工程要在所有调用点里分辨该改哪个。
    pub const fn can_be_borrowed(&self) -> bool {
        self.is_in_pool()
    }

    /// 是否已经永久离池。
    pub const fn is_out_of_pool_permanently(&self) -> bool {
        matches!(self, InstrumentState::Retired)
    }
}

/// 内置状态全集。
///
/// ★ 这张表的存在意义是**让「新增状态」变成一件要改两处的事**:
/// 加了枚举变体就必须加到这张表里,否则 `client` 的装配会漏掉它。
pub const BUILTIN_INSTRUMENT_STATES: [InstrumentState; 4] = [
    InstrumentState::Idle,
    InstrumentState::InUse,
    InstrumentState::InCalibration,
    InstrumentState::Retired,
];


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : instrument_unit.rs
//! 仪器个体:**对象池里的那个「对象」**。
//!
//! ## ★ 个体与型号分开,是对象池存在的前提
//!
//! 「电子天平」是型号,「BALANCE-0003」是个体。
//! 对象池复用的是**个体**------同一台天平第 7 次被借出,
//! 与「第 7 次买一台新天平」在报表上差了一整台设备的钱。
//! 若把个体与型号合成一个概念,「复用」这个词就没有载体了。
//!
//! ## ⚠️ 本文件**不引用机制层**
//!
//! 领域层不知道「什么是池」。`PooledObject` 那个 trait 由 `client` 层来 impl
//! (同一个 crate 内为本地类型实现本地 trait 是允许的)。
//!
//! 为什么值得绕这一下:一旦领域对象自己写着 `impl PooledObject`,
//! 「换一种池策略」就要动领域层;而现在换策略只动 `client`。
//!
//! ## ★ 归还校验的两条判据
//!
//! 一台仪器归还时能否**直接回池**(而不是送去校准),只看两条:
//!
//! 1. 累计使用次数没到退役上限;
//! 2. 校准到期月**晚于**当前月(含当月仍有效)。
//!
//! 这两条都是**业务知识**,所以放在领域层,机制层只提供「校验不过就隔离」的能力。

use crate::domain::{InstrumentModel, InstrumentState, MonthKey, StoreCode};

/// ★ 累计使用次数上限:到这个数就退役。
///
/// ## 为什么要有上限
///
/// 没有上限的话,「复用」在报表上永远是纯收益,
/// 于是「我们用了对象池所以很省」这句话**无法被反驳**------
/// 一个不能被反驳的结论不是结论。
///
/// ## ⚠️ 为什么是 6 这么小
///
/// 判据是「退役这条路径必须**真的被走到一次**」。
/// 设成 12 的话,本工程的演示场景里没有任何一台能借满 12 次,
/// 于是「退役台数 == 退役记录数」这条检查会变成 `0 == 0`------
/// 一个恒真的废检查。
/// 6 是让内置 + 扩展的场景恰好触发一次退役的值:
/// 退役一旦发生,「退役后池空了要不要补货」这个真问题才会出现。
pub const RETIREMENT_USAGE_LIMIT: usize = 6;

/// 一台仪器个体。
#[derive(Clone, Debug)]
pub struct InstrumentUnit {
    /// ★ 个体序列号的**机器形式**,如 `"BALANCE-0003"`。
    ///
    /// 它是身份的唯一载体:对象池的全部价值主张
    /// (「你拿到的还是上次那台」)都由它证明。
    serial_number: String,
    /// 所属型号。
    model: InstrumentModel,
    /// 当前状态。
    state: InstrumentState,
    /// ★ 累计被借出次数(只增不减)。
    usage_count: usize,
    /// 校准到期月(**含当月仍有效**)。
    calibration_due_month: MonthKey,
    /// 当前持有人(在借时有值;归还复位时清空)。
    ///
    /// ⚠️ `Option<StoreCode>` 而不是一个空串:
    /// 「没有持有人」与「持有人叫空」必须可区分。
    holder: Option<StoreCode>,
}

impl InstrumentUnit {
    /// 造一台仪器(**采购**)。
    ///
    /// # 参数
    /// - `serial_number`:个体序列号。
    /// - `model`:所属型号。
    /// - `calibration_due_month`:出厂校准到期月。
    pub fn new(serial_number: &str, model: InstrumentModel, calibration_due_month: MonthKey) -> Self {
        Self {
            serial_number: serial_number.to_string(),
            model,
            state: InstrumentState::Idle,
            usage_count: 0,
            calibration_due_month,
            holder: None,
        }
    }

    /// 序列号(身份)。
    pub fn serial_number(&self) -> &str {
        self.serial_number.as_str()
    }

    /// 所属型号。
    pub const fn model(&self) -> InstrumentModel {
        self.model
    }

    /// 当前状态。
    pub const fn state(&self) -> InstrumentState {
        self.state
    }

    /// 累计被借出次数。
    pub const fn usage_count(&self) -> usize {
        self.usage_count
    }

    /// 校准到期月。
    pub const fn calibration_due_month(&self) -> MonthKey {
        self.calibration_due_month
    }

    /// 当前持有人。
    pub const fn holder(&self) -> Option<StoreCode> {
        self.holder
    }

    /// 采购单价(来自型号)。
    pub const fn acquisition_cost(&self) -> crate::domain::Money {
        self.model.acquisition_cost()
    }

    // ---------------------------------------------------------------------
    // 生命周期动作
    // ---------------------------------------------------------------------

    /// ★ 借出:置为 `InUse`、累计次数 +1。
    ///
    /// 由机制层在**借出成功之后**统一调用(经 `PooledObject::on_acquire`),
    /// 因此**不允许**调用方自己调------
    /// 自己调就会出现「借到了但忘了记」,而次数正是价值量化的分子。
    ///
    /// # Panics
    /// 若此刻不在池中(`state != Idle`)就 panic。
    /// 「从池里取出一台不在池里的仪器」是装配错误,不是业务分支------
    /// 静默跳过会让容量守恒差一个数,而那个数要月底才有人发现。
    pub fn note_borrowed(&mut self) {
        assert!(
            self.state.is_in_pool(),
            "仪器 {} 此刻不在池中(状态={}),不能借出",
            self.serial_number,
            self.state.chinese_text()
        );
        self.state = InstrumentState::InUse;
        self.usage_count += 1;
    }

    /// 记下当前持有人(**只设持有人**,不改状态、不加次数)。
    ///
    /// ⚠️ 为什么与 [`InstrumentUnit::note_borrowed`] 分开:
    /// 「被借出」是机制事实(谁借都一样的动作),
    /// 「被谁借走」是业务事实(只有业务层知道门店)。
    /// 合成一个方法,机制层就被迫要知道什么是门店。
    pub fn set_holder(&mut self, holder: StoreCode) {
        self.holder = Some(holder);
    }

    /// ★ 归还校验:这台仪器能不能**直接回池**。
    ///
    /// # 参数
    /// - `month`:归还当月。
    ///
    /// ## 判据(两条,见模块文档)
    ///
    /// 1. 累计次数未达退役上限;
    /// 2. 校准到期月不早于当月。
    pub fn is_reusable_on_return(&self, month: MonthKey) -> bool {
        if self.usage_count >= RETIREMENT_USAGE_LIMIT {
            return false;
        }
        !self.calibration_due_month.is_before(&month)
    }

    /// ★ 归还校验没过时,为什么不能回池(写进报表的理由)。
    ///
    /// 返回 `None` 表示可以回池。
    pub fn rejection_reason_text(&self, month: MonthKey) -> Option<&'static str> {
        if self.usage_count >= RETIREMENT_USAGE_LIMIT {
            return Some("累计使用次数已达退役上限");
        }
        if self.calibration_due_month.is_before(&month) {
            return Some("校准已过期");
        }
        None
    }

    /// ★ 复位:清掉上一次使用的痕迹,让它重新成为「一台没人用过的仪器」。
    ///
    /// ⚠️ 方法名与 `PooledObject::reset_for_reuse` 刻意不同名:
    /// 同名会让 trait impl 里出现 `self.reset_for_reuse()` 这种
    /// **看起来对、实际递归**的调用。
    ///
    /// ## 为什么这一步不能省
    ///
    /// 对象池的复用是**同一个对象**的复用,不是「新建一个长得一样的」。
    /// 不复位的话,下一个借用方会读到上一个借用方留下的持有人------
    /// 那一刻起,池与「全局变量」没有区别。
    pub fn clear_for_reuse(&mut self) {
        self.state = InstrumentState::Idle;
        self.holder = None;
    }

    /// 送校准 / 维修:置为 `InCalibration` 并清空持有人。
    pub fn mark_in_calibration(&mut self) {
        self.state = InstrumentState::InCalibration;
        self.holder = None;
    }

    /// 校准完成回池:置为 `Idle`、按型号周期**顺延**校准到期月。
    ///
    /// # 参数
    /// - `month`:完成校准的当月。到期月 = 当月 + 型号周期。
    pub fn mark_calibration_done(&mut self, month: MonthKey) {
        self.state = InstrumentState::Idle;
        self.calibration_due_month = month.plus_months(self.model.calibration_interval_months());
        self.holder = None;
    }

    /// 退役:永久离池。
    pub fn mark_retired(&mut self) {
        self.state = InstrumentState::Retired;
        self.holder = None;
    }

    /// ★ 身份文本(报表的「是哪一台」列用它)。
    pub fn identity_text(&self) -> String {
        self.serial_number.clone()
    }

    /// ★ 指纹:`序列号|状态|累计次数|校准到期月`。
    ///
    /// 用于「归还前后是同一台」之外的另一件事:
    /// **两条不同路径跑完,每一台的指纹必须逐字节相同**。
    /// 若少了状态或次数,一台「被多借了三次的仪器」会与「只借过一次的」同指纹。
    pub fn fingerprint_text(&self) -> String {
        format!(
            "{}|{}|{}|{}",
            self.serial_number,
            self.state.machine_text(),
            self.usage_count,
            self.calibration_due_month.machine_text()
        )
    }

    /// 中文摘要(表格里的一行说明)。
    pub fn summary_text(&self) -> String {
        format!(
            "{}({},第 {} 次使用)",
            self.serial_number,
            self.model.chinese_name(),
            self.usage_count
        )
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : money.rs
//! 金额:整数最小单位(分)+ 自带币种。
//!
//! ## ★ 自带币种是类型安全,不是装饰
//!
//! 跨币种相加(人民币 + 港币)在类型上是**两个数相加**,编译器看不出错,
//! 报表也看不出错------只有做集团合并的人会在月底发现差了几万。
//! 因此 [`Money::plus`] 里放一条 `debug_assert!`:
//! 币种不同就在调试构建里立刻 panic,而不是返回一个「看起来对」的数。
//!
//! ## ★ 规范文本形式留在领域层
//!
//! `chinese_text()`(`¥12,345.67`)之所以不放表示层,
//! 判据是「这个文本形式是不是所有层都要用」:
//! 机制层报错、分析层结论、表示层表格都要把金额嵌进句子里。
//! 若搬去 `app`,`pool` / `analysis` 就得反向依赖 `app`------那才是真违规。
//!
//! ## ⚠️ 全程不出现浮点
//!
//! 千分位用整数反复除 10 取位拼出来。借道 `f64` 会在 10^15 量级上
//! 出现「同一笔金额两种写法」。

use crate::domain::arithmetic::scale_rounded;
use crate::domain::CurrencyCode;

/// 千分位分隔符。
const THOUSANDS_SEPARATOR: char = ',';

/// 一笔金额:**分**为单位的整数 + 币种。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Money {
    /// 以「分」为单位的整数金额。
    minor_units: i64,
    /// 币种。
    currency: CurrencyCode,
}

impl Money {
    /// 由「分」整数与币种构造。
    pub const fn new(minor_units: i64, currency: CurrencyCode) -> Self {
        Self {
            minor_units,
            currency,
        }
    }

    /// 零额。
    ///
    /// ⚠️ 「零」也必须走统一构造路径:散着写 `Money::new(0, c)` 时,
    /// 币种迟早会被写错一次,而 0 元在报表上**看不出币种错了**。
    pub const fn zero(currency: CurrencyCode) -> Self {
        Self::new(0, currency)
    }

    /// 以「分」为单位的整数。
    pub const fn minor_units(&self) -> i64 {
        self.minor_units
    }

    /// 币种。
    pub const fn currency(&self) -> CurrencyCode {
        self.currency
    }

    /// 是否为零额。
    pub const fn is_zero(&self) -> bool {
        self.minor_units == 0
    }

    /// 同币种相加。
    ///
    /// # Panics
    /// 币种不同时 `debug_assert!` 失败。
    /// 这是**接线错误**(把两本账加在了一起),不是业务情况。
    pub fn plus(&self, other: Money) -> Money {
        debug_assert!(
            self.currency == other.currency,
            "跨币种相加:{} + {} ------ 这两本账不能相加,先折算再求和",
            self.currency.code(),
            other.currency.code()
        );
        Money::new(self.minor_units + other.minor_units, self.currency)
    }

    /// 同币种相减(`self - other`)。
    pub fn minus(&self, other: Money) -> Money {
        debug_assert!(
            self.currency == other.currency,
            "跨币种相减:{} - {}",
            self.currency.code(),
            other.currency.code()
        );
        Money::new(self.minor_units - other.minor_units, self.currency)
    }

    /// 取相反数(用于把「成本」折成「对毛利的贡献」)。
    pub fn negate(&self) -> Money {
        Money::new(-self.minor_units, self.currency)
    }

    /// 按万分点缩放:`self × basis_points / 10000`。
    ///
    /// 用于「按占比分摊」------分摊口径也走唯一舍入口径,
    /// 不会出现「分摊后各份之和 ≠ 总额」之外的第二种差法。
    pub fn scale_basis_points(&self, basis_points: i64) -> Money {
        Money::new(
            scale_rounded(self.minor_units, i128::from(basis_points), 10_000),
            self.currency,
        )
    }

    /// 按整数比例缩放:`self × numerator / denominator`。
    pub fn scale_ratio(&self, numerator: i128, denominator: i128) -> Money {
        Money::new(
            scale_rounded(self.minor_units, numerator, denominator),
            self.currency,
        )
    }

    /// 纯数字文本(`12,345.67`),不含币种符号。
    ///
    /// 用于「金额 ÷ 克数」这类已经把币种写进列头的场合。
    pub fn amount_text(&self) -> String {
        // 符号单独处理:负号要放在最前面,不能夹在千分位里。
        // ⚠️ 先升到 i128 再取绝对值:`i64::MIN` 的 `abs()` 会溢出。
        let is_negative: bool = self.minor_units < 0;
        let magnitude: i128 = i128::from(self.minor_units).unsigned_abs() as i128;
        let yuan: i128 = magnitude / 100;
        let cents: i128 = magnitude % 100;
        let sign: &str = if is_negative { "-" } else { "" };
        format!("{}{}.{:02}", sign, group_thousands(yuan), cents)
    }

    /// 规范文本(`¥12,345.67`):符号 + 千分位。
    pub fn chinese_text(&self) -> String {
        format!("{}{}", self.currency.symbol(), self.amount_text())
    }

    /// 两个金额是否相等(含币种)。
    ///
    /// 用于审计比对:金额相等但币种不同,在本工程里是**不相等**。
    pub fn is_same_as(&self, other: &Money) -> bool {
        self.minor_units == other.minor_units && self.currency == other.currency
    }
}

/// 把一个非负整数的十进制表示插上千分位。
///
/// ⚠️ 用整数反复除 10 取位,全程不出现浮点:
/// `f64` 在 10^15 以上会开始丢低位,而金额合计确实会到那个量级。
fn group_thousands(value: i128) -> String {
    let digits: Vec<char> = value.to_string().chars().collect();
    let mut buffer: Vec<char> = Vec::with_capacity(digits.len() + digits.len() / 3);
    for (index, character) in digits.iter().enumerate() {
        let from_right: usize = digits.len() - index;
        if index > 0 && from_right % 3 == 0 {
            buffer.push(THOUSANDS_SEPARATOR);
        }
        buffer.push(*character);
    }
    buffer.into_iter().collect()
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : month_key.rs
//! 会计月份:`年 + 月` 的值对象。
//!
//! ## 为什么不用 `CalendarDate`
//!
//! 月度经营分析的粒度是**月**,不是日。用「某月 1 号」代表一个月会带来
//! 「代表日是哪天」这种本不该存在的问题(月末调整单会挂在代表日上)。
//! 因此单独一个 `MonthKey`。
//!
//! ## ★ `month_index` 是排序与比较的唯一口径
//!
//! 跨年比较不能写 `year < year || (year == year && month < month)`------
//! 这种写法迟早会被复制漏改一处。折成 `year * 12 + (month - 1)` 一个整数后,
//! 「先后」「相差几个月」都是一次减法。

/// 会计月份。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct MonthKey {
    /// 年份(如 `2026`)。
    year: i32,
    /// 月份(`1..=12`)。
    month: u32,
}

impl MonthKey {
    /// 声明一个会计月份。
    pub const fn new(year: i32, month: u32) -> Self {
        Self { year, month }
    }

    /// 年份。
    pub const fn year(&self) -> i32 {
        self.year
    }

    /// 月份。
    pub const fn month(&self) -> u32 {
        self.month
    }

    /// 月份是否合法(`1..=12`)。
    pub const fn is_valid(&self) -> bool {
        self.month >= 1 && self.month <= 12
    }

    /// 是否与另一个月份相同。
    ///
    /// ⚠️ 单独一个 `const fn` 而不是用 `==`:
    /// `PartialEq` 的 `==` **不是 const fn**,在 `const fn` 里用不了。
    pub const fn is_same(&self, other: &MonthKey) -> bool {
        self.year == other.year && self.month == other.month
    }

    /// ★ 折成一个整数的月份序号:`year * 12 + (month - 1)`。
    ///
    /// 「先后」「相差几个月」全部由它算,不写复合比较。
    pub const fn month_index(&self) -> i32 {
        self.year * 12 + (self.month as i32) - 1
    }

    /// 上一个会计月份。
    pub fn previous(&self) -> MonthKey {
        let index: i32 = self.month_index() - 1;
        MonthKey::new(index.div_euclid(12), index.rem_euclid(12) as u32 + 1)
    }

    /// 下一个会计月份。
    pub fn next(&self) -> MonthKey {
        let index: i32 = self.month_index() + 1;
        MonthKey::new(index.div_euclid(12), index.rem_euclid(12) as u32 + 1)
    }

    /// 去年同月(同比的分母所在月份)。
    pub const fn same_month_last_year(&self) -> MonthKey {
        MonthKey::new(self.year - 1, self.month)
    }

    /// ★ 往后推 `count` 个月(校准到期月顺延用)。
    ///
    /// ⚠️ 为什么不用 `year*12+month` 手算再拆:
    /// 那种算式在跨年处容易漏 `-1`,而漏了之后**只在 12 月出错**------
    /// 一年只错一次的错误最难被发现。复用 `month_index` 是唯一的写法。
    ///
    /// # 参数
    /// - `count`:要往后推几个月(`0` 表示就是当月)。
    pub fn plus_months(&self, count: u32) -> MonthKey {
        let index: i32 = self.month_index() + count as i32;
        MonthKey::new(index.div_euclid(12), index.rem_euclid(12) as u32 + 1)
    }

    /// 与另一个月份相差几个月(`self - other`)。
    pub const fn months_since(&self, other: &MonthKey) -> i32 {
        self.month_index() - other.month_index()
    }

    /// 是否早于另一个月份。
    pub const fn is_before(&self, other: &MonthKey) -> bool {
        self.month_index() < other.month_index()
    }

    /// 机器文本(`2026-10`)。定宽,可直接进表格列。
    pub fn machine_text(&self) -> String {
        format!("{:04}-{:02}", self.year, self.month)
    }

    /// 中文文本(`2026年10月`)。
    pub fn chinese_text(&self) -> String {
        format!("{}年{}月", self.year, self.month)
    }

    /// 该月的自然天数(用于把「月」折算成「日均」这类派生口径)。
    ///
    /// ⚠️ 闰年判据走 `support` 层,不在本文件重写一遍------
    /// 重写一次就多一处「两处算法不一致」的风险。
    pub fn days(&self) -> u32 {
        crate::support::days_in_month(self.year, self.month)
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : service_record.rs
//! 服务记录:一台仪器被送去校准 / 维修 / 退役的**可审计事实**。
//!
//! ## 为什么要有独立的服务记录,而不是改一改状态就完事
//!
//! 状态是「此刻」,记录是「发生过什么」。
//! 一台天平在 2026-03 校准过、2026-09 又校准过,
//! 这两件事在状态里**完全不可见**(两次之后状态都是 `Idle`)。
//! 没有记录,「这台设备是不是该退役了」就只能靠猜。
//!
//! ## ★ 记录只增不减
//!
//! 与全工程其它台账一致:**不提供删除或修改接口**。
//! 一条记录写错了,正确做法是补一条反向记录,
//! 而不是把原来那条改掉------后者会让「上个月的报表」与「这个月的报表」对不上,
//! 且没有任何痕迹说明为什么对不上。

use crate::domain::MonthKey;

/// 服务类型:**封闭枚举**。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ServiceKind {
    /// 校准(周期性,校准完回池)。
    Calibration,
    /// 维修(非周期性,修完回池)。
    Repair,
    /// 退役(永久离池)。
    Retirement,
}

impl ServiceKind {
    /// 中文名称。
    pub const fn chinese_text(&self) -> &'static str {
        match self {
            ServiceKind::Calibration => "校准",
            ServiceKind::Repair => "维修",
            ServiceKind::Retirement => "退役",
        }
    }

    /// 机器编码。
    pub const fn machine_text(&self) -> &'static str {
        match self {
            ServiceKind::Calibration => "CALIBRATION",
            ServiceKind::Repair => "REPAIR",
            ServiceKind::Retirement => "RETIREMENT",
        }
    }

    /// 这项服务做完之后,仪器是否**回池**。
    pub const fn returns_to_pool(&self) -> bool {
        !matches!(self, ServiceKind::Retirement)
    }
}

/// 一条服务记录。
#[derive(Clone, Debug)]
pub struct ServiceRecord {
    /// 被服务的仪器序列号。
    serial_number: String,
    /// 发生月份。
    month: MonthKey,
    /// 服务类型。
    kind: ServiceKind,
    /// 事由(写进报表的理由,如「校准已过期」)。
    reason_text: String,
    /// ★ 本次服务花掉的成本(**分**)。
    ///
    /// ⚠️ 为什么成本要记在记录上而不是型号上:
    /// 同一型号的两次维修花掉的钱不一样,记在型号上只能取平均,
    /// 而「不准用平均数」是本工程的一贯纪律------平均会掩盖掉最贵的那一次。
    service_cost_minor_units: i64,
}

impl ServiceRecord {
    /// 记一条服务记录。
    pub fn new(
        serial_number: &str,
        month: MonthKey,
        kind: ServiceKind,
        reason_text: &str,
        service_cost_minor_units: i64,
    ) -> Self {
        Self {
            serial_number: serial_number.to_string(),
            month,
            kind,
            reason_text: reason_text.to_string(),
            service_cost_minor_units,
        }
    }

    /// 仪器序列号。
    pub fn serial_number(&self) -> &str {
        self.serial_number.as_str()
    }

    /// 发生月份。
    pub const fn month(&self) -> MonthKey {
        self.month
    }

    /// 服务类型。
    pub const fn kind(&self) -> ServiceKind {
        self.kind
    }

    /// 事由。
    pub fn reason_text(&self) -> &str {
        self.reason_text.as_str()
    }

    /// 本次服务成本(分)。
    pub const fn service_cost_minor_units(&self) -> i64 {
        self.service_cost_minor_units
    }

    /// 指纹(跨运行比对用)。
    pub fn fingerprint_text(&self) -> String {
        format!(
            "{}|{}|{}|{}",
            self.serial_number,
            self.month.machine_text(),
            self.kind.machine_text(),
            self.service_cost_minor_units
        )
    }
}

/// 内置服务类型全集(装配与清点用)。
pub const BUILTIN_SERVICE_KINDS: [ServiceKind; 3] = [
    ServiceKind::Calibration,
    ServiceKind::Repair,
    ServiceKind::Retirement,
];


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : store_code.rs
//! 门店编码:开放型标签。
//!
//! ## 为什么是 `const fn new` 结构体而不是 `enum`
//!
//! 判据只有一条:**这个维度会不会不断增加**。
//! 门店会(开新店就多一个),所以必须开放------
//! 否则工程外新增一家店要回来改领域层文件,
//! 而「可扩展性必须被实证」这条硬纪律要求:**扩展只能发生在装配点**。
//!
//! ## ⚠️ 为什么不实现 `Ord`
//!
//! 本工程从不对门店排序,只按**声明顺序**输出(声明顺序稳定 → 两次运行逐字节一致)。
//! 实现一个用不上的 `Ord`,等于给「按门店排序」这种没被设计过的用法开了口子。

/// 门店编码。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct StoreCode {
    /// 机器编码,如 `"STORE-CN-01"`。
    code: &'static str,
    /// 中文名称,如 `"广州天河店"`。
    chinese_name: &'static str,
}

impl StoreCode {
    /// 声明一家门店。
    pub const fn new(code: &'static str, chinese_name: &'static str) -> Self {
        Self { code, chinese_name }
    }

    /// 机器编码。
    pub const fn code(&self) -> &'static str {
        self.code
    }

    /// 中文名称。
    pub const fn chinese_name(&self) -> &'static str {
        self.chinese_name
    }
}

/// 广州天河店(内置)。
pub const STORE_CN_01: StoreCode = StoreCode::new("STORE-CN-01", "广州天河店");
/// 香港铜锣湾店(内置)。
pub const STORE_HK_01: StoreCode = StoreCode::new("STORE-HK-01", "香港铜锣湾店");

/// 内置门店全集。
///
/// ⚠️ 这张表只用于**装配**(建图时遍历),不用于「校验某店是否合法」------
/// 那会把工程外新增门店挡在门外。
pub const BUILTIN_STORE_CODES: [StoreCode; 2] = [STORE_CN_01, STORE_HK_01];


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : usage_request.rs
//! 借用申请:门店要在某个月借用某种型号的仪器。
//!
//! ## 为什么申请是「型号」而不是「某台具体仪器」
//!
//! 门店要的是「一台能称金重的天平」,不是「BALANCE-0003 这台」。
//! 让申请指向个体,等于把调度权交给了申请方------
//! 那时对象池就退化成一个登记表,池化调度(哪台空闲借哪台)无从谈起。
//!
//! ## ⚠️ 申请本身不含「结果」
//!
//! 借到了没有、借到哪一台,由 `client` 层的借出动作产生。
//! 若把结果写回申请,申请就变成了一个**状态会变**的对象,
//! 而「两次运行逐字节一致」要求同一份输入序列跑到同一份结果------
//! 状态会变的输入会让复现变得不可能。

use crate::domain::{InstrumentModel, MonthKey, StoreCode};

/// 一条借用申请。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct UsageRequest {
    /// 借用门店。
    store: StoreCode,
    /// 要借的**型号**(不是个体)。
    model: InstrumentModel,
    /// 借用当月。
    month: MonthKey,
    /// 用途说明(写进报表,说明为什么要借)。
    purpose_text: &'static str,
}

impl UsageRequest {
    /// 声明一条借用申请。
    pub const fn new(
        store: StoreCode,
        model: InstrumentModel,
        month: MonthKey,
        purpose_text: &'static str,
    ) -> Self {
        Self { store, model, month, purpose_text }
    }

    /// 借用门店。
    pub const fn store(&self) -> StoreCode {
        self.store
    }

    /// 型号。
    pub const fn model(&self) -> InstrumentModel {
        self.model
    }

    /// 借用当月。
    pub const fn month(&self) -> MonthKey {
        self.month
    }

    /// 用途说明。
    pub const fn purpose_text(&self) -> &'static str {
        self.purpose_text
    }

    /// 机器文本(台账与报表用,`门店|型号|月份`)。
    pub fn machine_text(&self) -> String {
        format!(
            "{}|{}|{}",
            self.store.code(),
            self.model.code(),
            self.month.machine_text()
        )
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : object_pool.rs
//! 对象池本体:**借出 / 归还 / 隔离 / 退役**,外加一本只增不减的台账。
//!
//! ## ★ 本文件是机制层,零 `crate::` 依赖
//!
//! 它不知道什么是仪器、门店、校准。它只知道:
//! 「有一堆同类型的可复用对象,有人来借,有人来还,还回来时要先验一下」。
//!
//! ## ★ 四个区域,而不是「一个 Vec + 一个布尔位」
//!
//! | 区域 | 含义 | 是否计入容量 |
//! |---|---|---|
//! | `idle` | 在池空闲,可借 | 是 |
//! | `borrowed` | 借出中 | 是 |
//! | `quarantined` | 归还校验没过,待处理 | 是 |
//! | `retired` | 已退役,永久离池 | **否** |
//!
//! 用「一个 Vec + 状态位」也能表达同样的信息,但那样
//! 「池里现在有几台可用」要遍历全表数一遍------
//! 而**每数一遍就多一处口径**,月底对不上时无从下手。
//! 四个区域让「有几台可用」等于 `idle.len()`,没有第二种写法。
//!
//! ## ★ 借出取**队首**(FIFO)而不是队尾
//!
//! 取队尾(LIFO)会让最后归还的那台被反复借出,
//! 于是「退役上限」会集中在少数几台上:
//! 3 台天平里 1 台被借 12 次退役,另外 2 台全新------
//! 而报表上的「平均使用次数」看起来完全正常。
//! FIFO 让磨损均匀,这不是优化,是**让平均值有意义**的前提。

use std::cell::RefCell;
use std::rc::Rc;

use super::pool_handle::PoolHandle;
use super::pool_ledger::{PoolLedger, PoolSnapshot};
use super::pooled_object::PooledObject;
use super::reuse_policy::ReusePolicy;

/// ★ 归还校验器:判断一个对象归还后能不能**直接回池**。
///
/// 返回 `Ok(())` 表示可以回池;`Err(理由)` 表示要隔离。
///
/// ## 为什么是闭包而不是 trait 方法
///
/// 判据需要业务参数(当月是哪个月),而机制层不该知道月份。
/// 闭包在装配点构造,可以捕获任何业务状态------
/// 这是「机制层提供能力、业务层决定判据」的接线处。
pub type ReturnVerifier<T> = Box<dyn Fn(&T) -> Result<(), String>>;

/// ★ 对象工厂:造一个新对象。
///
/// # 参数
/// - `sequence`:这是池造的第几个对象(**从 0 起**),用于生成序列号。
///
/// ⚠️ 为什么把序号传进去:序列号若由工厂自己用全局计数器生成,
/// 两次运行的编号会依赖运行顺序;传序号进来则编号只依赖**装配顺序**。
pub type ObjectFactory<T> = Box<dyn Fn(usize) -> T>;

/// 归还结果:**必须可见**,不能静默。
#[derive(Clone, Debug)]
pub enum ReleaseOutcome {
    /// 校验通过,已回池(**这才是省下一次采购的那一次**)。
    ReturnedToPool {
        /// 仪器身份。
        identity: String,
    },
    /// 校验没过,已隔离(送校准 / 待退役)。
    Quarantined {
        /// 仪器身份。
        identity: String,
        /// 为什么不能回池(由校验器给出)。
        reason: String,
    },
    /// ★ 归还的不是从这个池借出的对象(**装配错误**,正常必须为 0)。
    Rejected {
        /// 仪器身份。
        identity: String,
    },
}

impl ReleaseOutcome {
    /// 机器编码(台账与体检用)。
    pub const fn machine_text(&self) -> &'static str {
        match self {
            ReleaseOutcome::ReturnedToPool { .. } => "RETURNED",
            ReleaseOutcome::Quarantined { .. } => "QUARANTINED",
            ReleaseOutcome::Rejected { .. } => "REJECTED",
        }
    }

    /// 中文名称。
    pub const fn chinese_text(&self) -> &'static str {
        match self {
            ReleaseOutcome::ReturnedToPool { .. } => "已回池",
            ReleaseOutcome::Quarantined { .. } => "已隔离",
            ReleaseOutcome::Rejected { .. } => "异物归还被拒",
        }
    }

    /// 身份。
    pub fn identity(&self) -> &str {
        match self {
            ReleaseOutcome::ReturnedToPool { identity } => identity.as_str(),
            ReleaseOutcome::Quarantined { identity, .. } => identity.as_str(),
            ReleaseOutcome::Rejected { identity } => identity.as_str(),
        }
    }

    /// ★ 是否算「省下一次采购」(只有回池才算)。
    pub const fn counts_as_reuse(&self) -> bool {
        matches!(self, ReleaseOutcome::ReturnedToPool { .. })
    }
}

/// 对象池。
pub struct ObjectPool<T: PooledObject> {
    /// 池名(报表用,便于三档对照时分辨)。
    name: String,
    /// 复用策略。
    policy: ReusePolicy,
    /// 空闲区(**队首优先借出**)。
    idle: Vec<Rc<RefCell<T>>>,
    /// 借出中。
    borrowed: Vec<Rc<RefCell<T>>>,
    /// 隔离区(归还校验没过,等业务处理)。
    quarantined: Vec<Rc<RefCell<T>>>,
    /// 退役区(永久离池,**不计入容量**)。
    retired: Vec<Rc<RefCell<T>>>,
    /// 对象工厂。
    factory: ObjectFactory<T>,
    /// 归还校验器。
    verifier: ReturnVerifier<T>,
    /// 台账(只增不减)。
    ledger: PoolLedger,
    /// 已造对象总数(含预热、含退役的)。
    created_total: usize,
    /// 借出序号。
    sequence: usize,
}

impl<T: PooledObject> ObjectPool<T> {
    /// 建一个池。
    ///
    /// # 参数
    /// - `name`:池名。
    /// - `policy`:复用策略。
    /// - `factory`:对象工厂(造新对象 = 采购)。
    /// - `verifier`:归还校验器(判据由业务给)。
    pub fn new(
        name: &str,
        policy: ReusePolicy,
        factory: ObjectFactory<T>,
        verifier: ReturnVerifier<T>,
    ) -> Self {
        Self {
            name: name.to_string(),
            policy,
            idle: Vec::new(),
            borrowed: Vec::new(),
            quarantined: Vec::new(),
            retired: Vec::new(),
            factory,
            verifier,
            ledger: PoolLedger::new(),
            created_total: 0,
            sequence: 0,
        }
    }

    /// ★ 预热:开业前先造好 `count` 台放进池里。
    ///
    /// ## 为什么预热要单列一个动作
    ///
    /// 预热也是「采购」(计进 `creation_total`),
    /// 但它**没有对应的借出申请**------
    /// 若与按需新建混在一起,「新建次数」就同时含着
    /// 「为应付借出而买」与「提前备货而买」两件不同的事,
    /// 于是「省了多少次采购」这句话的分母变得说不清。
    pub fn prewarm(&mut self, count: usize) {
        for _ in 0..count {
            let object: Rc<RefCell<T>> = self.create_object();
            self.idle.push(object);
            self.ledger.record_prewarm();
        }
    }

    /// 造一个新对象(记 `creation_total` 与台账)。
    fn create_object(&mut self) -> Rc<RefCell<T>> {
        let sequence: usize = self.created_total;
        let object: T = (self.factory)(sequence);
        self.created_total += 1;
        self.ledger.record_creation();
        Rc::new(RefCell::new(object))
    }

    /// ★ 借出。
    ///
    /// 返回 `None` 表示**没借到**(池空且策略不允许新建)。
    ///
    /// ⚠️ 「没借到」必须是一个显式结果而不是 panic:
    /// 借不到是**业务结局**(这一单做不了),不是程序错误。
    pub fn acquire(&mut self) -> Option<PoolHandle<T>> {
        // ① 池里有空闲 → 命中,直接复用(取队首,理由见模块文档)。
        if let Some(object) = self.idle.first() {
            let object: Rc<RefCell<T>> = Rc::clone(object);
            self.idle.remove(0);
            self.ledger.record_hit();
            self.ledger.record_acquisition();
            object.borrow_mut().on_acquire();
            self.sequence += 1;
            self.borrowed.push(Rc::clone(&object));
            return Some(PoolHandle::new(object, self.sequence));
        }
        // ② 池空:按策略决定新建还是拒绝。
        let existing_total: usize = self.capacity_total();
        if !self.policy.allows_creation_when_empty(existing_total) {
            self.ledger.record_rejection();
            return None;
        }
        let object: Rc<RefCell<T>> = self.create_object();
        self.ledger.record_acquisition();
        object.borrow_mut().on_acquire();
        self.sequence += 1;
        self.borrowed.push(Rc::clone(&object));
        Some(PoolHandle::new(object, self.sequence))
    }

    /// ★ 归还:凭句柄归还,返回一个**可见的结局**。
    ///
    /// ## 为什么必须凭句柄
    ///
    /// 句柄是本池某次借出的凭证。拿着别处来的对象来还,
    /// 会让「池内总数」凭空多一台------
    /// 而容量守恒是月底唯一能对账的东西,一旦失真就没有别的凭据了。
    /// 因此这里用 `Rc::ptr_eq` 在 `borrowed` 里**真的找一遍**,
    /// 找不到就记 `foreign_return_total` 并返回 `Rejected`。
    pub fn release(&mut self, handle: PoolHandle<T>) -> ReleaseOutcome {
        let object: Rc<RefCell<T>> = handle.into_object();
        let identity: String = object.borrow().identity_text();
        let position: Option<usize> = self
            .borrowed
            .iter()
            .position(|candidate| Rc::ptr_eq(candidate, &object));
        let Some(index) = position else {
            self.ledger.record_foreign_return();
            return ReleaseOutcome::Rejected { identity };
        };
        self.borrowed.remove(index);
        self.ledger.record_release();

        // ⚠️ 先把判定取到局部变量:`&object.borrow()` 是一个临时 `Ref`,
        //    若在 `match` 的条件位置上直接借用,它会活到整个 `match` 结束,
        //    而分支里要 `borrow_mut()` ------ 那就撞上了同一把 `RefCell` 的借用规则。
        let verdict: Result<(), String> = (self.verifier)(&object.borrow());
        match verdict {
            Ok(()) => {
                object.borrow_mut().reset_for_reuse();
                self.idle.push(Rc::clone(&object));
                self.ledger.record_reuse();
                ReleaseOutcome::ReturnedToPool { identity }
            }
            Err(reason) => {
                object.borrow_mut().on_quarantine();
                self.quarantined.push(Rc::clone(&object));
                self.ledger.record_quarantine();
                ReleaseOutcome::Quarantined { identity, reason }
            }
        }
    }

    /// 取出全部隔离对象(业务层处理送修 / 退役)。
    ///
    /// ⚠️ 取走之后这些对象**不再属于池**:
    /// 若业务层忘记把它们送回来([`ObjectPool::admit_repaired`])或直接退役
    /// ([`ObjectPool::retire`]),容量守恒就会少一台,且没有任何一行输出说明。
    pub fn take_quarantined(&mut self) -> Vec<Rc<RefCell<T>>> {
        std::mem::take(&mut self.quarantined)
    }

    /// 校准 / 维修完成,重新入池。
    pub fn admit_repaired(&mut self, object: Rc<RefCell<T>>) {
        self.idle.push(object);
    }

    /// 退役:永久离池(**不再计入容量**)。
    pub fn retire(&mut self, object: Rc<RefCell<T>>) {
        self.retired.push(object);
    }

    // ---------------------------------------------------------------------
    // 读数(全部是「此刻」的实时值;报表请用 snapshot())
    // ---------------------------------------------------------------------

    /// 池名。
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// 复用策略。
    pub const fn policy(&self) -> ReusePolicy {
        self.policy
    }

    /// 空闲数(`idle.len()`,没有第二种写法)。
    pub fn idle_total(&self) -> usize {
        self.idle.len()
    }

    /// 借出中数。
    pub fn borrowed_total(&self) -> usize {
        self.borrowed.len()
    }

    /// 隔离数(**待处理**,正常应被业务层及时取走)。
    pub fn quarantined_total(&self) -> usize {
        self.quarantined.len()
    }

    /// 退役数(累计,永久)。
    pub fn retired_total(&self) -> usize {
        self.retired.len()
    }

    /// ★ 容量总数 = 空闲 + 借出 + 隔离(**不含退役**)。
    ///
    /// 退役的对象已经不是池的一部分,把它们算进容量,
    /// 「限量扩容」的上限会被一批再也回不来的对象占住------
    /// 那时池会永远拒绝借出,而报表上的「已造 8 台」看起来很正常。
    pub fn capacity_total(&self) -> usize {
        self.idle_total() + self.borrowed_total() + self.quarantined_total()
    }

    /// 累计造过多少个对象(含预热、含已退役)。
    pub fn created_total(&self) -> usize {
        self.created_total
    }

    /// 台账快照(**报表一律读它**)。
    pub fn snapshot(&self) -> PoolSnapshot {
        self.ledger.snapshot()
    }

    /// 台账本体(**只给审计用**,报表不要直接读实时计数)。
    pub fn ledger(&self) -> &PoolLedger {
        &self.ledger
    }

    /// 池内全部对象的身份(**收集后排序**,保证两次运行逐字节一致)。
    ///
    /// ⚠️ 为什么排序:四个 `Vec` 的入队顺序取决于借还历史,
    /// 而「池里现在有哪几台」这个**事实**与顺序无关。
    /// 直接按 `Vec` 顺序输出,会把「先借后还」的顺序泄漏进报表,
    /// 于是同一份数据集在不同借还顺序下长得不一样。
    pub fn identity_list_sorted(&self) -> Vec<String> {
        let mut identities: Vec<String> = Vec::new();
        for region in [&self.idle, &self.borrowed, &self.quarantined, &self.retired] {
            for object in region.iter() {
                identities.push(object.borrow().identity_text());
            }
        }
        identities.sort();
        identities
    }

    /// ★ 池内全部对象(**按身份排序**后返回)。
    ///
    /// 报表与统计要遍历池内对象时走这里,
    /// 不要直接读四个 `Vec`(它们是私有的,且顺序取决于借还历史)。
    ///
    /// ⚠️ 返回的是 `Rc<RefCell<T>>`,拿到它就能改对象------
    /// 这是**有意的**:业务层需要「取走隔离对象去送修」这类能力。
    /// 只读报表请只用 `identity_list_sorted()` / `fingerprint_list_sorted()`。
    pub fn objects_sorted(&self) -> Vec<(String, Rc<RefCell<T>>)> {
        let mut pairs: Vec<(String, Rc<RefCell<T>>)> = Vec::new();
        for region in [&self.idle, &self.borrowed, &self.quarantined, &self.retired] {
            for object in region.iter() {
                pairs.push((object.borrow().identity_text(), Rc::clone(object)));
            }
        }
        pairs.sort_by(|left, right| left.0.cmp(&right.0));
        pairs
    }

    /// 池内全部对象的指纹(**排序后**返回,理由同上)。
    pub fn fingerprint_list_sorted(&self) -> Vec<(String, String)> {
        let mut pairs: Vec<(String, String)> = Vec::new();
        for region in [&self.idle, &self.borrowed, &self.quarantined, &self.retired] {
            for object in region.iter() {
                let borrowed = object.borrow();
                pairs.push((borrowed.identity_text(), borrowed.fingerprint_text()));
            }
        }
        pairs.sort();
        pairs
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : pool_audit.rs
//! 池契约审计:**对象池的六条不变量**。
//!
//! ## ★ 每条规则都必须有「已知会失败的输入」
//!
//! 一条不可能失败的规则不是规则,它只是没被跑过的注释。
//! 因此每条规则自带 `known_failing_input_text()`,
//! 并且在幕八的报表里**逐条印出来**------
//! 读者能看见「这条规则在什么样的数据上会红」,
//! 才可能相信它现在是绿的不是因为没在跑。
//!
//! ## 六条规则
//!
//! | 编码 | 判据 | 已知会失败的输入 |
//! |---|---|---|
//! | `CAPACITY` | 空闲+在借+隔离 == 已造−已退役 | 取走隔离对象后忘记送回 |
//! | `ACQUIRE` | 借出成功 == 命中 + 新建 | 某条借出路径漏记一个计数 |
//! | `RELEASE` | 归还 == 回池 + 隔离 | 归还时先 return 再记数 |
//! | `FOREIGN` | 异物归还次数 == 0 | 拿着别处来的对象来还 |
//! | `BALANCE` | 借出成功 == 归还 + 在借 | 借了不还(泄漏) |
//! | `EFFECTIVE` | 命中次数 > 0 | 从未预热且只借一次 |
//!
//! ## ⚠️ `EFFECTIVE` 不是恒等式,它是**价值判据**
//!
//! 前五条描述「池没被用坏」,第六条描述「池有没有起作用」。
//! 一条对象池演示若只有前五条,就会得出
//! 「我们用了对象池(且没用坏)」这种**不可反驳**的结论。
//! 把 `命中 > 0` 也变成一条会红的规则,才能让
//! 「其实一次都没复用上」这件事有地方暴露。

use super::object_pool::ObjectPool;
use super::pooled_object::PooledObject;
use super::reuse_policy::ReusePolicy;

/// 对一个池的完整观测(**一次性取齐**,与台账快照同理)。
#[derive(Clone, Debug)]
pub struct PoolObservation {
    /// 池名。
    pub pool_name: String,
    /// 复用策略。
    pub policy: ReusePolicy,
    /// 新建次数。
    pub created_total: usize,
    /// 命中次数。
    pub hit_total: usize,
    /// 借出成功次数。
    pub acquisition_total: usize,
    /// 拒绝次数。
    pub rejection_total: usize,
    /// 归还次数。
    pub release_total: usize,
    /// 回池次数。
    pub reuse_total: usize,
    /// 隔离次数。
    pub quarantine_total: usize,
    /// 异物归还次数。
    pub foreign_return_total: usize,
    /// 预热次数。
    pub prewarm_total: usize,
    /// 空闲数。
    pub idle_total: usize,
    /// 在借数。
    pub borrowed_total: usize,
    /// 隔离区对象数。
    pub quarantined_total: usize,
    /// 退役数。
    pub retired_total: usize,
}

impl PoolObservation {
    /// 容量总数(空闲 + 在借 + 隔离)。
    pub fn capacity_total(&self) -> usize {
        self.idle_total + self.borrowed_total + self.quarantined_total
    }

    /// ★ 容量守恒的差:`空闲+在借+隔离 − (已造 − 已退役)`。
    ///
    /// 非 0 意味着有对象**既不在任何区域、也没被记为退役**------
    /// 那就是凭空消失了一台。
    pub fn capacity_gap(&self) -> i64 {
        self.capacity_total() as i64 - (self.created_total as i64 - self.retired_total as i64)
    }

    /// ★ 借出恒等式的差:`借出 − (命中 + 新建 − 预热)`。
    ///
    /// 减掉预热的理由见 [`super::pool_ledger::PoolSnapshot::acquisition_identity_gap`]:
    /// 预热计入新建,却不伴随任何一次借出。
    pub fn acquisition_gap(&self) -> i64 {
        self.acquisition_total as i64
            - (self.hit_total + self.created_total - self.prewarm_total) as i64
    }

    /// 归还恒等式的差:`归还 − (回池 + 隔离)`。
    pub fn release_gap(&self) -> i64 {
        self.release_total as i64 - (self.reuse_total + self.quarantine_total) as i64
    }

    /// ★ 借还平衡的差:`借出成功 − (归还 + 此刻在借)`。
    ///
    /// 非 0 意味着有借出**既没归还也不在借出列表里**------那就是泄漏。
    pub fn balance_gap(&self) -> i64 {
        self.acquisition_total as i64 - (self.release_total + self.borrowed_total) as i64
    }

    /// 复用率(万分点)。
    pub fn reuse_ratio_basis_points(&self) -> i64 {
        if self.acquisition_total == 0 {
            return 0;
        }
        ((self.hit_total as i128) * 10_000 / (self.acquisition_total as i128)) as i64
    }
}

/// 对一个池做一次观测(**一次取齐**全部计数)。
pub fn observe_pool<T: PooledObject>(pool: &ObjectPool<T>) -> PoolObservation {
    let snapshot = pool.snapshot();
    PoolObservation {
        pool_name: pool.name().to_string(),
        policy: pool.policy(),
        created_total: pool.created_total(),
        hit_total: snapshot.hit_total,
        acquisition_total: snapshot.acquisition_total,
        rejection_total: snapshot.rejection_total,
        release_total: snapshot.release_total,
        reuse_total: snapshot.reuse_total,
        quarantine_total: snapshot.quarantine_total,
        foreign_return_total: snapshot.foreign_return_total,
        prewarm_total: snapshot.prewarm_total,
        idle_total: pool.idle_total(),
        borrowed_total: pool.borrowed_total(),
        quarantined_total: pool.quarantined_total(),
        retired_total: pool.retired_total(),
    }
}

/// 审计规则(封闭枚举)。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PoolAuditRule {
    /// 容量守恒。
    CapacityConservation,
    /// 借出恒等式。
    AcquisitionIdentity,
    /// 归还恒等式。
    ReleaseIdentity,
    /// 异物归还为 0。
    NoForeignReturn,
    /// 借还平衡(无泄漏)。
    NoLeakedAcquisition,
    /// ★ 池确实起了作用(命中 > 0)。
    PoolIsEffective,
}

impl PoolAuditRule {
    /// 规则编码。
    pub const fn code(&self) -> &'static str {
        match self {
            PoolAuditRule::CapacityConservation => "CAPACITY",
            PoolAuditRule::AcquisitionIdentity => "ACQUIRE",
            PoolAuditRule::ReleaseIdentity => "RELEASE",
            PoolAuditRule::NoForeignReturn => "FOREIGN",
            PoolAuditRule::NoLeakedAcquisition => "BALANCE",
            PoolAuditRule::PoolIsEffective => "EFFECTIVE",
        }
    }

    /// 中文名称。
    pub const fn chinese_text(&self) -> &'static str {
        match self {
            PoolAuditRule::CapacityConservation => "容量守恒:空闲+在借+隔离 == 已造−已退役",
            PoolAuditRule::AcquisitionIdentity => {
                "借出恒等:借出成功 == 命中 + (新建 − 预热)"
            }
            PoolAuditRule::ReleaseIdentity => "归还恒等:归还 == 回池 + 隔离",
            PoolAuditRule::NoForeignReturn => "异物归还:归还了不属于本池的对象必须为 0",
            PoolAuditRule::NoLeakedAcquisition => "借还平衡:借出成功 == 归还 + 此刻在借",
            PoolAuditRule::PoolIsEffective => "池确实起效:命中次数 > 0(否则没复用上)",
        }
    }

    /// ★ 已知会失败的输入(**没有这条的规则不是规则**)。
    pub const fn known_failing_input_text(&self) -> &'static str {
        match self {
            PoolAuditRule::CapacityConservation => {
                "调用 take_quarantined() 取走隔离对象后既不送回也不退役"
            }
            PoolAuditRule::AcquisitionIdentity => {
                "新增一条借出路径时只记了命中、忘了记借出成功"
            }
            PoolAuditRule::ReleaseIdentity => "归还时先提前 return,漏记回池或隔离中的一个",
            PoolAuditRule::NoForeignReturn => "拿着另一个池(或自己 new 出来)的对象来归还",
            PoolAuditRule::NoLeakedAcquisition => "借出后不归还(借用凭证被丢弃)",
            PoolAuditRule::PoolIsEffective => "一个从未预热的池,只被借出一次",
        }
    }

    /// 内置规则全集。
    pub const BUILTIN: [PoolAuditRule; 6] = [
        PoolAuditRule::CapacityConservation,
        PoolAuditRule::AcquisitionIdentity,
        PoolAuditRule::ReleaseIdentity,
        PoolAuditRule::NoForeignReturn,
        PoolAuditRule::NoLeakedAcquisition,
        PoolAuditRule::PoolIsEffective,
    ];
}

/// 一条规则的检查结果。
#[derive(Clone, Debug)]
pub struct PoolAuditCheck {
    /// 规则。
    rule: PoolAuditRule,
    /// ★ 适用总数(**恒为 1**:每条规则对每个池只判一次)。
    applicable_total: usize,
    /// 符合数(0 或 1)。
    conforming_total: usize,
    /// 实测文本。
    actual_text: String,
    /// 违规对象(本规则没有对象维度,用池名)。
    violating_names: Vec<String>,
}

impl PoolAuditCheck {
    /// 由规则、是否符合、实测文本、池名组装。
    fn new(rule: PoolAuditRule, conforming: bool, actual_text: &str, pool_name: &str) -> Self {
        Self {
            rule,
            applicable_total: 1,
            conforming_total: if conforming { 1 } else { 0 },
            actual_text: actual_text.to_string(),
            violating_names: if conforming { Vec::new() } else { vec![pool_name.to_string()] },
        }
    }

    /// 规则。
    pub const fn rule(&self) -> PoolAuditRule {
        self.rule
    }

    /// 适用总数。
    pub const fn applicable_total(&self) -> usize {
        self.applicable_total
    }

    /// 符合数。
    pub const fn conforming_total(&self) -> usize {
        self.conforming_total
    }

    /// 实测文本。
    pub fn actual_text(&self) -> &str {
        self.actual_text.as_str()
    }

    /// 违规对象名。
    pub fn violating_names(&self) -> &[String] {
        self.violating_names.as_slice()
    }

    /// ★ 是否通过:符合数 == 适用数,且**适用数 > 0**。
    ///
    /// ⚠️ 「且适用数 > 0」这一半不能省:
    /// 一条规则若在本次数据上根本没有适用对象,
    /// `0 == 0` 会让它显示成「通过」------那是**没跑过**冒充**跑过了**。
    pub fn passed(&self) -> bool {
        self.applicable_total > 0 && self.conforming_total == self.applicable_total
    }
}

/// 对一个池跑全部规则。
pub fn audit_pool(observation: &PoolObservation) -> Vec<PoolAuditCheck> {
    PoolAuditRule::BUILTIN
        .iter()
        .map(|rule| {
            let (conforming, actual_text): (bool, String) = match rule {
                PoolAuditRule::CapacityConservation => {
                    let gap: i64 = observation.capacity_gap();
                    (
                        gap == 0,
                        format!(
                            "空闲 {} + 在借 {} + 隔离 {} = {};已造 {} − 已退役 {} = {};差 {}",
                            observation.idle_total,
                            observation.borrowed_total,
                            observation.quarantined_total,
                            observation.capacity_total(),
                            observation.created_total,
                            observation.retired_total,
                            observation.created_total as i64 - observation.retired_total as i64,
                            gap
                        ),
                    )
                }
                PoolAuditRule::AcquisitionIdentity => {
                    let gap: i64 = observation.acquisition_gap();
                    (
                        gap == 0,
                        format!(
                            "借出 {};命中 {} + (新建 {} − 预热 {}) = {};差 {}",
                            observation.acquisition_total,
                            observation.hit_total,
                            observation.created_total,
                            observation.prewarm_total,
                            observation.hit_total + observation.created_total
                                - observation.prewarm_total,
                            gap
                        ),
                    )
                }
                PoolAuditRule::ReleaseIdentity => {
                    let gap: i64 = observation.release_gap();
                    (
                        gap == 0,
                        format!(
                            "归还 {};回池 {} + 隔离 {} = {};差 {}",
                            observation.release_total,
                            observation.reuse_total,
                            observation.quarantine_total,
                            observation.reuse_total + observation.quarantine_total,
                            gap
                        ),
                    )
                }
                PoolAuditRule::NoForeignReturn => (
                    observation.foreign_return_total == 0,
                    format!("异物归还 {} 次", observation.foreign_return_total),
                ),
                PoolAuditRule::NoLeakedAcquisition => {
                    let gap: i64 = observation.balance_gap();
                    (
                        gap == 0,
                        format!(
                            "借出 {};归还 {} + 在借 {} = {};差 {}",
                            observation.acquisition_total,
                            observation.release_total,
                            observation.borrowed_total,
                            observation.release_total + observation.borrowed_total,
                            gap
                        ),
                    )
                }
                PoolAuditRule::PoolIsEffective => (
                    observation.hit_total > 0,
                    format!(
                        "命中 {} 次,复用率 {} 万分点",
                        observation.hit_total,
                        observation.reuse_ratio_basis_points()
                    ),
                ),
            };
            PoolAuditCheck::new(*rule, conforming, actual_text.as_str(), observation.pool_name.as_str())
        })
        .collect()
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : pool_handle.rs
//! 借出凭证:**归还必须凭它**。
//!
//! ## ★ 为什么要一个凭证,而不是直接把对象交出去
//!
//! 池借出的是 `Rc<RefCell<T>>`,把它直接交给调用方,
//! 调用方就能**绕过池**直接改对象,甚至不归还------
//! 那时刻起「池里还有几台」这句话没人能回答。
//!
//! 凭证把「借用权」打包成一个**一次性**的东西:
//! 归还时把凭证交回池,池销毁它。凭证没了,借用权也就没了。
//!
//! ## ⚠️ 为什么不用 Drop 自动归还
//!
//! Rust 的 RAII 很诱人(`Drop` 时自动还池),但本工程**刻意不用**:
//!
//! 1. 自动归还把「什么时候还」变成由**作用域**决定,
//!    而演示要精确控制「借了不还」这个场景(那是对象池最常见的泄漏);
//! 2. 自动归还时无法返回「校验没过,送去校准」这类**结果**------
//!    `Drop` 没有返回值,只能把结果写进池,于是结果就变得不可见。
//!
//! 「不可见的归还结果」正是对象池最难查的一类问题:
//! 池悄悄少了一台,而没有任何一行输出说明它去哪了。

use std::cell::RefCell;
use std::rc::Rc;

use super::pooled_object::PooledObject;

/// 借出凭证。
#[derive(Debug)]
pub struct PoolHandle<T: PooledObject> {
    /// 被借出的那个对象(**与池共享同一个 `Rc`**)。
    object: Rc<RefCell<T>>,
    /// 第几次借出(1 起,池的借出序号)。
    sequence: usize,
    /// ★ 借出时刻的身份文本。
    ///
    /// ⚠️ 为什么要**存一份**而不是用时再取:
    /// 借出期间对象可能被改(累计次数 +1),
    /// 而「借的是哪一台」这个事实不该随之后的变化而改变。
    identity_at_acquisition: String,
}

impl<T: PooledObject> PoolHandle<T> {
    /// 由池构造(**只有池能造**:凭证代表池的一次授权)。
    ///
    /// # 参数
    /// - `object`:被借出的对象。
    /// - `sequence`:借出序号。
    pub(crate) fn new(object: Rc<RefCell<T>>, sequence: usize) -> Self {
        let identity_at_acquisition: String = object.borrow().identity_text();
        Self { object, sequence, identity_at_acquisition }
    }

    /// 借出序号(第几次借出)。
    pub const fn sequence(&self) -> usize {
        self.sequence
    }

    /// ★ 借出时刻的身份(「借的是哪一台」)。
    pub fn identity_at_acquisition(&self) -> &str {
        self.identity_at_acquisition.as_str()
    }

    /// 此刻的身份(与借出时刻比对,可发现「借出期间被掉包」)。
    pub fn identity_now(&self) -> String {
        self.object.borrow().identity_text()
    }

    /// 指纹(此刻)。
    pub fn fingerprint_text(&self) -> String {
        self.object.borrow().fingerprint_text()
    }

    /// ★ 借用期内读取对象(只读)。
    ///
    /// 只给 `Rc` 的克隆而不给 `&mut`:借用期内改对象要走业务动作,
    /// 直接改会让池失去对状态的掌握。
    pub fn object(&self) -> Rc<RefCell<T>> {
        Rc::clone(&self.object)
    }

    /// 由池取回内部对象(**归还时用**,之后凭证不再可用)。
    pub(crate) fn into_object(self) -> Rc<RefCell<T>> {
        self.object
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : pool_ledger.rs
//! 池台账:**只增不减**的一次次动作计数。
//!
//! ## ★ 为什么只增不减
//!
//! 池的全部价值主张都是**计数**支撑的(省了多少次新建、拒绝了多少次)。
//! 一个可以清零的计数器,会让「报表先渲染的行」与「后渲染的行」
//! 来自不同时刻的池------读者会以为这些数字是同一时刻的快照,事实恰好相反。
//! 因此本结构**不提供任何 reset / set 方法**(由红线 R5 守护)。
//!
//! ## 报表一律读 `snapshot()`
//!
//! `snapshot()` 把此刻的九个计数**一次取齐**成一个不可变结构。
//! 这样一张表里的十行数字必然来自同一时刻;
//! 逐格取实时计数则会让「读取 = 命中 + 新建」这类恒等式偶发对不上,
//! 而这种偶发正是最难查的那类错误。

use std::cell::Cell;

/// 池台账。
#[derive(Debug, Default)]
pub struct PoolLedger {
    /// 新建次数(池自己去「造」了一个对象)。
    creation_total: Cell<usize>,
    /// ★ 命中次数(借出时**池里有空闲**,直接复用)。
    hit_total: Cell<usize>,
    /// 借出成功次数(命中 + 新建)。
    acquisition_total: Cell<usize>,
    /// ★ 拒绝次数(池空且策略不允许新建)。
    rejection_total: Cell<usize>,
    /// 归还次数(凭句柄归还,含校验没过的)。
    release_total: Cell<usize>,
    /// ★ 归还后**回池**的次数(真正省下一次采购的那些次)。
    reuse_total: Cell<usize>,
    /// ★ 归还校验没过、被隔离的次数。
    quarantine_total: Cell<usize>,
    /// 归还了不属于本池的对象的次数(**装配错误探测器**)。
    foreign_return_total: Cell<usize>,
    /// 预热次数(开业前先造好放进池里)。
    prewarm_total: Cell<usize>,
}

impl PoolLedger {
    /// 建一本空台账。
    pub fn new() -> Self {
        Self::default()
    }

    /// 记一次新建。
    pub fn record_creation(&self) {
        self.creation_total.set(self.creation_total.get() + 1);
    }

    /// 记一次命中(借出时池里有空闲)。
    pub fn record_hit(&self) {
        self.hit_total.set(self.hit_total.get() + 1);
    }

    /// 记一次借出成功。
    pub fn record_acquisition(&self) {
        self.acquisition_total.set(self.acquisition_total.get() + 1);
    }

    /// 记一次拒绝。
    pub fn record_rejection(&self) {
        self.rejection_total.set(self.rejection_total.get() + 1);
    }

    /// 记一次归还。
    pub fn record_release(&self) {
        self.release_total.set(self.release_total.get() + 1);
    }

    /// 记一次「归还后回池」。
    pub fn record_reuse(&self) {
        self.reuse_total.set(self.reuse_total.get() + 1);
    }

    /// 记一次「归还后被隔离」。
    pub fn record_quarantine(&self) {
        self.quarantine_total.set(self.quarantine_total.get() + 1);
    }

    /// 记一次「归还了不属于本池的对象」。
    ///
    /// ⚠️ 这个计数**正常必须为 0**。它不是统计,是探测器:
    /// 一旦非 0,说明有调用方拿着别处来的对象来还,容量守恒从那一刻起失真。
    pub fn record_foreign_return(&self) {
        self.foreign_return_total.set(self.foreign_return_total.get() + 1);
    }

    /// 记一次预热。
    pub fn record_prewarm(&self) {
        self.prewarm_total.set(self.prewarm_total.get() + 1);
    }

    /// 新建次数。
    pub fn creation_total(&self) -> usize {
        self.creation_total.get()
    }

    /// 命中次数。
    pub fn hit_total(&self) -> usize {
        self.hit_total.get()
    }

    /// 借出成功次数。
    pub fn acquisition_total(&self) -> usize {
        self.acquisition_total.get()
    }

    /// 拒绝次数。
    pub fn rejection_total(&self) -> usize {
        self.rejection_total.get()
    }

    /// 归还次数。
    pub fn release_total(&self) -> usize {
        self.release_total.get()
    }

    /// 回池次数。
    pub fn reuse_total(&self) -> usize {
        self.reuse_total.get()
    }

    /// 隔离次数。
    pub fn quarantine_total(&self) -> usize {
        self.quarantine_total.get()
    }

    /// 异物归还次数(正常为 0)。
    pub fn foreign_return_total(&self) -> usize {
        self.foreign_return_total.get()
    }

    /// 预热次数。
    pub fn prewarm_total(&self) -> usize {
        self.prewarm_total.get()
    }

    /// 动作总数(所有计数之和,用于「台账不是空的」这类体检)。
    pub fn action_total(&self) -> usize {
        self.creation_total()
            + self.hit_total()
            + self.acquisition_total()
            + self.rejection_total()
            + self.release_total()
            + self.reuse_total()
            + self.quarantine_total()
            + self.foreign_return_total()
            + self.prewarm_total()
    }

    /// ★ 取快照:把九个计数一次取齐。
    pub fn snapshot(&self) -> PoolSnapshot {
        PoolSnapshot {
            creation_total: self.creation_total.get(),
            hit_total: self.hit_total.get(),
            acquisition_total: self.acquisition_total.get(),
            rejection_total: self.rejection_total.get(),
            release_total: self.release_total.get(),
            reuse_total: self.reuse_total.get(),
            quarantine_total: self.quarantine_total.get(),
            foreign_return_total: self.foreign_return_total.get(),
            prewarm_total: self.prewarm_total.get(),
        }
    }
}

/// 台账快照:**不可变**,报表一律读它。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct PoolSnapshot {
    /// 新建次数。
    pub creation_total: usize,
    /// 命中次数。
    pub hit_total: usize,
    /// 借出成功次数。
    pub acquisition_total: usize,
    /// 拒绝次数。
    pub rejection_total: usize,
    /// 归还次数。
    pub release_total: usize,
    /// 回池次数。
    pub reuse_total: usize,
    /// 隔离次数。
    pub quarantine_total: usize,
    /// 异物归还次数。
    pub foreign_return_total: usize,
    /// 预热次数。
    pub prewarm_total: usize,
}

impl PoolSnapshot {
    /// ★ 借出成功 = 命中 + (新建 − 预热)(恒等式一)。
    ///
    /// ## ⚠️ 为什么减掉预热
    ///
    /// 预热也是「造一个对象」(所以计进 `creation_total`),
    /// 但它**不伴随任何一次借出**------
    /// 若不减掉它,恒等式会差一个预热台数,
    /// 而这个差在报表上长得就像「漏记了一次借出」。
    /// 本工程第一次跑体检时就被它抓到过:严格池 借到 8 / 命中 8 / 新建 3,
    /// 差 3 恰好就是三个型号的预热数。
    ///
    /// 这个差若非 0,说明某条借出路径忘了记其中一个计数。
    pub fn acquisition_identity_gap(&self) -> i64 {
        self.acquisition_total as i64
            - (self.hit_total + self.creation_total - self.prewarm_total) as i64
    }

    /// ★ 归还 = 回池 + 隔离(恒等式二)。
    pub fn release_identity_gap(&self) -> i64 {
        self.release_total as i64 - (self.reuse_total + self.quarantine_total) as i64
    }

    /// ★ 复用率(万分点):命中 / 借出成功。
    ///
    /// 分母为 0 时返回 0 ------ ⚠️ 但调用方必须先判「借出次数 > 0」,
    /// 否则「没借过」会被显示成「复用率 0%」,与「借了 100 次一次都没复用」无法区分。
    pub fn reuse_ratio_basis_points(&self) -> i64 {
        if self.acquisition_total == 0 {
            return 0;
        }
        ((self.hit_total as i128) * 10_000 / (self.acquisition_total as i128)) as i64
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : pooled_object.rs
//! 池化对象契约:**一个对象要能被池化,必须回答这四个问题**。
//!
//! ## ★ 本文件是机制层的一部分,零 `crate::` 依赖
//!
//! 与 `support` 同秩:本层不知道什么是「仪器」「门店」「校准」。
//! 一旦这里出现 `use crate::domain`,「换一个业务也要用对象池」就得改机制层,
//! 机制就不再是机制。
//!
//! ## 为什么是四个方法,不多不少
//!
//! | 方法 | 回答的问题 | 少它会怎样 |
//! |---|---|---|
//! | `identity_text` | 你是哪一个 | 无法证明「还回来的是同一台」 |
//! | `fingerprint_text` | 你此刻长什么样 | 两条路径无法逐字节比对 |
//! | `on_acquire` | 被借出时要做什么 | 累计次数只能由调用方记,迟早漏一处 |
//! | `reset_for_reuse` | 归还回池前要清什么 | 下一个借用方读到上一个的残留 |
//! | `on_quarantine` | 没能回池时要做什么 | 「送修」这件事没有落点 |
//!
//! ## ⚠️ 为什么没有 `can_return_to_pool`
//!
//! 「归还时能不能直接回池」看起来该在这里,但它需要**业务参数**
//! (当月是哪个月、校准到期没有),而机制层不该知道月份。
//! 因此判据改用**闭包**注入到池上(见 [`super::object_pool::ObjectPool::new`])------
//! 闭包是在装配点写的,可以捕获任何业务状态。
//!
//! 这不是绕路:判据若写死在 trait 里,「换一种归还判据」就要改 trait,
//! 而 trait 一改,**所有实现方**都得跟着改。

/// 可池化对象。
pub trait PooledObject {
    /// ★ 身份文本:全工程「是哪一个对象」的唯一答案。
    fn identity_text(&self) -> String;

    /// ★ 指纹:身份 + 此刻的状态。两条路径跑完必须逐字节相同。
    fn fingerprint_text(&self) -> String;

    /// 被借出时调用(累计次数 +1 之类)。
    ///
    /// ⚠️ 由池在**借出成功之后**调用,且只调一次。
    /// 若由调用方自己调,就会出现「借到了但忘了记」------
    /// 而次数正是对象池价值量化的分子,漏一处整张表就错。
    fn on_acquire(&mut self);

    /// ★ 归还回池前的复位。
    ///
    /// 不复位的话,下一次借用方会读到上一次借用方留下的东西;
    /// 那一刻起,「池」与「一个全局变量」没有区别。
    fn reset_for_reuse(&mut self);

    /// 归还校验没过、被隔离时调用(业务层在这里置「送修 / 退役」之类)。
    fn on_quarantine(&mut self);
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : reuse_policy.rs
//! 复用策略:**池借空了怎么办**。
//!
//! ## ★ 三档策略存在的理由:让「我们用了对象池所以很省」这句话可以被反驳
//!
//! 只有一个策略时,「池很省」是**不可证伪**的------
//! 因为没人见过「不省」的那条路长什么样。
//! 三档在同一份申请序列上跑出三个不同的「新建次数」,
//! 「省了多少」才是一个能算出来的差,而不是一句主张。
//!
//! | 策略 | 借空时 | 新建次数 | 拒绝次数 |
//! |---|---|---|---|
//! | `StrictPool` | 拒绝借出 | 只等于预热数 | 可能 > 0 |
//! | `GrowOnDemand` | 立刻新建 | 等于**峰值并发数** | 恒为 0 |
//! | `GrowWithLimit` | 先新建,到上限后拒绝 | 被上限截断 | 可能 > 0 |
//!
//! ## ⚠️ `GrowOnDemand` 不是「更好」,它是**对照组**
//!
//! 按需新建永远不拒绝,看起来最「好用」,
//! 但它的新建次数等于峰值并发数------这正是**不用池**时的采购量。
//! 把它放进对照表,读者才能看见「不池化」的真实代价。

/// 复用策略。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ReusePolicy {
    /// ★ 严格池:借空就拒绝,一台都不多买。
    StrictPool,
    /// ★ 按需扩容:借空就新建,没有上限(**不用池的对照组**)。
    GrowOnDemand,
    /// ★ 限量扩容:借空先新建,池内总数到 `max_total` 之后改为拒绝。
    GrowWithLimit {
        /// 池内对象总数上限(含在借与隔离的)。
        max_total: usize,
    },
}

impl ReusePolicy {
    /// 中文名称。
    pub const fn chinese_text(&self) -> &'static str {
        match self {
            ReusePolicy::StrictPool => "严格池(借空即拒绝)",
            ReusePolicy::GrowOnDemand => "按需扩容(永不拒绝)",
            ReusePolicy::GrowWithLimit { .. } => "限量扩容(到顶即拒绝)",
        }
    }

    /// 机器编码。
    pub const fn machine_text(&self) -> &'static str {
        match self {
            ReusePolicy::StrictPool => "STRICT_POOL",
            ReusePolicy::GrowOnDemand => "GROW_ON_DEMAND",
            ReusePolicy::GrowWithLimit { .. } => "GROW_WITH_LIMIT",
        }
    }

    /// 上限(`None` 表示没有上限)。
    pub const fn max_total(&self) -> Option<usize> {
        match self {
            ReusePolicy::StrictPool => None,
            ReusePolicy::GrowOnDemand => None,
            ReusePolicy::GrowWithLimit { max_total } => Some(*max_total),
        }
    }

    /// ★ 借空时是否允许新建。
    ///
    /// # 参数
    /// - `existing_total`:此刻池内对象总数(含在借与隔离)。
    pub const fn allows_creation_when_empty(&self, existing_total: usize) -> bool {
        match self {
            ReusePolicy::StrictPool => false,
            ReusePolicy::GrowOnDemand => true,
            ReusePolicy::GrowWithLimit { max_total } => existing_total < *max_total,
        }
    }

    /// ★ 是否**可能**拒绝借出(用于体检表:这一档的拒绝次数应不应该 > 0)。
    pub const fn can_reject(&self) -> bool {
        !matches!(self, ReusePolicy::GrowOnDemand)
    }
}

/// 内置策略全集(装配、对照实验与接口清点用)。
pub const BUILTIN_REUSE_POLICIES: [ReusePolicy; 3] = [
    ReusePolicy::StrictPool,
    ReusePolicy::GrowOnDemand,
    ReusePolicy::GrowWithLimit { max_total: 4 },
];

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : calendar_date.rs
//! ★ Sakamoto 算法求星期 + 逐日推进 + 工作日推进。
//!
//! ## ⚠️ 既有工程的一条硬教训(本文件照做)
//!
//! **`(26 * (m + 1)) / 10` 不能代替月份偏移表**。它只是 `floor(2.6 * (m + 1))`
//! 的近似,与偏移表 `[0,3,2,5,0,3,5,1,4,6,2,4]` 在 3 月、9 月等多个月份不等,
//! 会导致**整年错一位**。既有工程因此吃过亏。
//! **有公认表格的算法,宁可把表写出来,也不要用「看起来等价」的算式。**
//!
//! ## 为什么「推进」用逐日循环而不是儒略日公式
//!
//! 逐日推进显然正确(只需处理「月末进位」「年末进位」「闰年 2 月」三件事);
//! 儒略日公式是 O(1),但多一个可能写错的公式。
//! 本工程的日期跨度都在一年以内,调用次数是个位数量级,性能无关紧要。
//! **当「明显正确的笨办法」与「可能写错的聪明办法」成本相当时,选笨办法。**

/// ★ 月份偏移量表(Sakamoto 算法)。
///
/// 下标 = 月份 − 1。⚠️ **不要**把它化简成 `(26 * (m + 1)) / 10` 之类,
/// 二者在 3 月、9 月不等价(见模块文档)。
const SAKAMOTO_MONTH_OFFSETS: [i32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];

/// 中文星期名,下标与 [`CalendarDate::weekday_index`] 一致:`0 = 周日`。
const WEEKDAY_CHINESE_NAMES: [&str; 7] = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];

/// 一个公历日期(年 / 月 / 日,三个整数)。
///
/// 刻意**不派生** `Default`:一个「默认日期」在业务上没有意义,
/// 能让调用方写出 `CalendarDate::default()` 只会掩盖「忘了传日期」这类错误。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CalendarDate {
    /// 公历年(如 2026)。用 `i32` 是因为要参与 `year - 1` 的中间运算。
    year: i32,
    /// 公历月,取值 `1..=12`。
    month: u32,
    /// 公历日,取值 `1..=31`(具体上界随年月变化)。
    day: u32,
}

impl CalendarDate {
    /// 构造。**故意不校验**------校验在 [`Self::is_valid`],
    /// 因为「构造一个待校验的日期」本身是合法需求(本工程用它做反例)。
    pub const fn new(year: i32, month: u32, day: u32) -> Self {
        CalendarDate { year, month, day }
    }

    /// 年份。
    pub const fn year(&self) -> i32 {
        self.year
    }

    /// 月份。
    pub const fn month(&self) -> u32 {
        self.month
    }

    /// 日。
    pub const fn day(&self) -> u32 {
        self.day
    }

    /// 判断年月日组合是否落在合法范围内。
    pub fn is_valid(&self) -> bool {
        if self.month < 1 || self.month > 12 {
            return false;
        }
        if self.day < 1 {
            return false;
        }
        self.day <= days_in_month(self.year, self.month)
    }

    /// 返回**星期索引**:`0 = 周日, 1 = 周一, ... 6 = 周六`(Sakamoto 算法)。
    ///
    /// ⚠️ 最后用 `rem_euclid(7)` 而不是 `% 7`:
    /// `%` 对负数会得到负数(Rust 与 C 一致),当作索引用会在公元前的年份上
    /// 直接 panic。虽然本工程用不到那些年份,但 `rem_euclid` 是正确的写法,
    /// **不依赖调用方的取值范围假设**。
    pub fn weekday_index(&self) -> usize {
        let month: i32 = self.month as i32;
        let day: i32 = self.day as i32;

        // ★ 1 月 / 2 月必须借上一年的年份------这一步不可省略。
        let adjusted_year: i32 = if month < 3 { self.year - 1 } else { self.year };

        // 月份偏移量查表(★ 不化简成算式)。
        let month_offset: i32 = SAKAMOTO_MONTH_OFFSETS[(self.month - 1) as usize];

        let raw_index: i32 = adjusted_year
            + adjusted_year / 4
            - adjusted_year / 100
            + adjusted_year / 400
            + month_offset
            + day;

        raw_index.rem_euclid(7) as usize
    }

    /// 中文星期名(如「周三」)。
    pub fn weekday_chinese_text(&self) -> &'static str {
        WEEKDAY_CHINESE_NAMES[self.weekday_index()]
    }

    /// 是否周末(周六或周日)。
    ///
    /// ⚠️ 写成 `index == 0 || index == 6` 而不是 `index >= 5`:
    /// 索引 0 是**周日**,用 `>= 5` 会把周五也算成周末。
    pub fn is_weekend(&self) -> bool {
        let index: usize = self.weekday_index();
        index == 0 || index == 6
    }

    /// 机器格式:`2026-10-07`(定长 10 列,适合进表格)。
    ///
    /// 定长是本函数存在的理由:`{}-{}-{}` 直接拼会让 2026-1-7 变成 8 列,
    /// 与 `2026-10-07`(10 列)宽度不同,表格立刻歪掉。
    pub fn machine_text(&self) -> String {
        format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
    }

    /// 中文格式:`2026年10月7日`(**不定长**,仅用于正文说明,不进定宽列)。
    pub fn chinese_text(&self) -> String {
        format!("{}年{}月{}日", self.year, self.month, self.day)
    }

    /// 中文格式 + 星期:`2026年10月7日(周三)`。
    pub fn chinese_text_with_weekday(&self) -> String {
        format!("{}({})", self.chinese_text(), self.weekday_chinese_text())
    }

    /// 返回当天的前一天 / 后一天(由 `step` 决定,取值 `+1` 或 `-1`)。
    fn step_one_day(&self, step: i32) -> CalendarDate {
        let mut year: i32 = self.year;
        let mut month: u32 = self.month;
        let mut day: i32 = self.day as i32 + step;

        if day < 1 {
            // 向前借一个月。
            month -= 1;
            if month < 1 {
                month = 12;
                year -= 1;
            }
            day = days_in_month(year, month) as i32;
        } else if day > days_in_month(year, month) as i32 {
            // 向后进一个月。
            day = 1;
            month += 1;
            if month > 12 {
                month = 1;
                year += 1;
            }
        }

        CalendarDate::new(year, month, day as u32)
    }

    /// 加 / 减若干**自然日**。
    ///
    /// ⚠️ 循环带硬上限:算错的日期会变成**卡住的程序**,
    /// 那是比「算错」更糟的失败形态(既有工程的教训)。
    pub fn add_days(&self, delta: i32) -> CalendarDate {
        let mut current: CalendarDate = *self;
        let step: i32 = if delta >= 0 { 1 } else { -1 };
        let mut remaining: i32 = delta.abs();
        let mut guard: i32 = 0;
        while remaining > 0 && guard < 100_000 {
            current = current.step_one_day(step);
            remaining -= 1;
            guard += 1;
        }
        current
    }

    /// 推进若干**工作日**(跳过周末)。
    ///
    /// ★ **起点当天不计入**:`add_working_days(1)` 得到「下一个工作日」,
    /// 这正是「受理日的次日开工」的语义。
    ///
    /// ⚠️ 因此「工期 N 个工作日的完成日」要写成 `add_working_days(N - 1)`------
    /// 那个 `− 1` 不是笔误,是**把起点那天收回来**。这两个 `±1` 必须成对理解。
    pub fn add_working_days(&self, delta: i32) -> CalendarDate {
        let mut current: CalendarDate = *self;
        let step: i32 = if delta >= 0 { 1 } else { -1 };
        let mut remaining: i32 = delta.abs();
        let mut guard: i32 = 0;
        while remaining > 0 && guard < 100_000 {
            current = current.step_one_day(step);
            if !current.is_weekend() {
                remaining -= 1;
            }
            guard += 1;
        }
        current
    }
}

/// 是否闰年。
///
/// ★ 三条规则,**第三条最容易被漏**:
/// ① 能被 4 整除;② 不能被 100 整除;③ **除非**能被 400 整除。
/// 因此校验时必须用 **2100 年**做探针(2000 年是闰年,发现不了漏了第三条)。
pub const fn is_leap_year(year: i32) -> bool {
    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

/// 某年某月的天数。
pub const fn days_in_month(year: i32, 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
            }
        }
        // 非法月份返回 0:让 `is_valid` 判 false,而不是 panic。
        _ => 0,
    }
}

/// 两个日期之间的天数差(`to - from`,可为负)。
///
/// ⚠️ 同样带上界,理由同 [`CalendarDate::add_days`]。
pub fn days_between(from: CalendarDate, to: CalendarDate) -> i64 {
    let mut count: i64 = 0;
    let mut current: CalendarDate = from;
    let mut guard: i64 = 0;
    if from <= to {
        while current < to && guard < 100_000 {
            current = current.step_one_day(1);
            count += 1;
            guard += 1;
        }
    } else {
        while current > to && guard < 100_000 {
            current = current.step_one_day(-1);
            count -= 1;
            guard += 1;
        }
    }
    count
}

/// 把日期渲染成**表格单元格**文本。
///
/// ⚠️ 刻意走 [`CalendarDate::machine_text`](定长 10 列)而不是中文格式:
/// 中文格式不定长(`2026年10月7日` 与 `2026年1月7日` 宽度不同),
/// 进表格会歪。中文格式只在注解里出现。
pub fn report_cell_text(date: CalendarDate) -> String {
    date.machine_text()
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : deterministic_code.rs
//! 确定性编码:FNV-1a 64 位 + base36 定长输出。
//!
//! ## 为什么不用 `DefaultHasher`
//!
//! `std::collections::hash_map::DefaultHasher` 的值**跨 Rust 版本不保证稳定**,
//! 而本工程的验收线是「两次运行逐字节一致」------跨版本不稳定意味着
//! 换一次工具链,输出的编号就全变了。
//! 更实际的问题是:**读者无法复算**。自己实现 FNV-1a 后,
//! 编号的生成规则写在这 80 行里,任何人都能手算验一遍。
//!
//! ## 为什么要段间分隔符
//!
//! 把 `["AB", "C"]` 与 `["A", "BC"]` 直接拼起来都是 `"ABC"`,会**撞哈希**。
//! 段间插入 `0x1f`(ASCII Unit Separator)之后两者不同。
//! 这是既有工程的实测教训:**拼接型哈希必须有分隔符**。

/// FNV-1a 64 位偏移基址。
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
/// FNV-1a 64 位质数。
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
/// 段间分隔符:ASCII Unit Separator(**选它的理由就是它几乎不会出现在文本里**)。
const SEGMENT_SEPARATOR: u8 = 0x1f;

/// 把若干段拼成一个**稳定的 64 位哈希**。
///
/// 段与段之间插入 [`SEGMENT_SEPARATOR`],防止 `["AB","C"]` 与 `["A","BC"]` 碰撞。
pub fn stable_hash(parts: &[&str]) -> u64 {
    let mut hash: u64 = FNV_OFFSET_BASIS;
    for (index, part) in parts.iter().enumerate() {
        // 除第一段外,先喂一个分隔符。
        if index > 0 {
            hash ^= u64::from(SEGMENT_SEPARATOR);
            hash = hash.wrapping_mul(FNV_PRIME);
        }
        for byte in part.as_bytes() {
            hash ^= u64::from(*byte);
            hash = hash.wrapping_mul(FNV_PRIME);
        }
    }
    hash
}

/// base36 字符集(数字 + 大写字母)。
const BASE36_ALPHABET: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

/// 由若干段生成**定长**的 base36 编码。
///
/// 定长是刻意的:编号要进定宽表格列,长度不定会把列顶歪。
///
/// ⚠️ `width` 取值 `1..=13`:64 位无符号数在 base36 下最多 13 位,
/// 超过 13 位高位全是 `0`,写出来是**假精度**(看起来信息量大,其实都是零)。
pub fn deterministic_code(parts: &[&str], width: usize) -> String {
    debug_assert!(width >= 1 && width <= 13, "base36 最多 13 位,width={} 会产出假精度", width);
    let mut value: u64 = stable_hash(parts);
    let mut buffer: Vec<char> = Vec::with_capacity(width);
    for _ in 0..width {
        let digit: usize = (value % 36) as usize;
        buffer.push(BASE36_ALPHABET[digit] as char);
        value /= 36;
    }
    buffer.iter().rev().collect()
}

/// 带前缀的确定性编码:`前缀-XXXXXX`。
///
/// 前缀让编号**可人读**(`CARD-` / `TPL-` / `FIX-`),
/// 后面那段保证唯一且可复算。
pub fn prefixed_code(prefix: &str, parts: &[&str], width: usize) -> String {
    format!("{}-{}", prefix, deterministic_code(parts, width))
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : text_layout.rs
//! 显示宽度与版面工具:**中文占 2 列**这一事实的全部实现都在这里。
//!
//! ## 为什么不能直接用 `{:<N}` 或 `str::len()`
//!
//! `{:<N}` 按**字符数**填充,而终端里一个汉字占 2 个显示列。
//! 于是「工序段」(3 字 = 6 列) 与 `STAGE`(5 列) 在同一格里会错开 1 列,
//! 整张表立刻歪掉------**而且歪得很整齐**,看不出来是错的。
//! `str::len()` 是**字节数**,中文是 3 字节,更离谱。
//!
//! ## 宽度模型:按 `east_asian_width` 的 `W` / `F` 判 2 列
//!
//! 这是 Unicode 的标准属性。⚠️ 但要注意两类**看起来像宽字符其实不是**的:
//! - `─`(U+2500,制表符):属性是 `A`(歧义),本工程按 **1 列**计;
//! - `...`(U+2026,省略号):属性是 `A`,本工程按 **1 列**计。
//!
//! ⚠️ 既有工程踩过的坑:曾凭印象写「U+2500 落在 CJK 区间 `U+2E80..=0x303E` 里」,
//! 而 **U+2500 = 9472 < 0x2E80 = 11904**,根本没落进去。
//! 结论:**有公认表格的判断,宁可让机器算一遍**------所以本文件有
//! [`self_check_width_model`] 这条永久自检。

/// 判断一个字符是否占 **2 个显示列**。
///
/// 区间表按 Unicode `East_Asian_Width` 的 `W` / `F` 摘取,
/// 各区间**互不重叠**(写重叠区间不会有错,但会让读者以为有优先级)。
pub const fn is_wide_character(character: char) -> bool {
    let code: u32 = character as u32;
    matches!(code,
        // CJK 统一汉字及扩展(不含后面单独列出的兼容区)
        0x1100..=0x115F     // 谚文首字母
        | 0x2E80..=0x303E   // CJK 部首、康熙部首、CJK 符号与标点
        | 0x3041..=0x33FF   // 平假名、片假名、注音、谚文兼容、CJK 兼容
        | 0x3400..=0x4DBF   // CJK 扩展 A
        | 0x4E00..=0x9FFF   // CJK 基本区(最常用的汉字都在这里)
        | 0xA000..=0xA4CF   // 彝文
        | 0xAC00..=0xD7A3   // 谚文音节
        | 0xF900..=0xFAFF   // CJK 兼容汉字
        | 0xFE10..=0xFE19   // 竖排标点
        | 0xFE30..=0xFE6F   // CJK 兼容形式
        | 0xFF00..=0xFF60   // 全角 ASCII、全角标点
        | 0xFFE0..=0xFFE6   // 全角符号
        | 0x1F300..=0x1F64F // 表情符号(常用段)
        | 0x1F900..=0x1F9FF // 补充表情符号
        | 0x20000..=0x2FFFD // CJK 扩展 B..F
        | 0x30000..=0x3FFFD // CJK 扩展 G
    )
}

/// 计算一个字符串的**显示宽度**(中文 2 列、英文 1 列)。
pub fn display_width(text: &str) -> usize {
    text.chars().map(|character| if is_wide_character(character) { 2 } else { 1 }).sum()
}

/// 右填充(左对齐):不足补空格,**超出不截断**。
///
/// 不截断是刻意的:调用方要截断请显式调 [`truncate_to_width`],
/// 让「这里可能被截」这件事在代码里看得见。
pub fn pad_right(text: &str, width: usize) -> String {
    let used: usize = display_width(text);
    if used >= width {
        return text.to_string();
    }
    format!("{}{}", text, " ".repeat(width - used))
}

/// 左填充(右对齐):数字列用这个。
pub fn pad_left(text: &str, width: usize) -> String {
    let used: usize = display_width(text);
    if used >= width {
        return text.to_string();
    }
    format!("{}{}", " ".repeat(width - used), text)
}

/// 居中填充:**左侧少、右侧多**(既有多余的一列给右边)。
///
/// 为什么必须定死「奇数给右边」:不定死时,同一个表头在不同行
/// 可能一个偏左一个偏右,看起来像内容变了。
pub fn pad_center(text: &str, width: usize) -> String {
    let used: usize = display_width(text);
    if used >= width {
        return text.to_string();
    }
    let total: usize = width - used;
    let left: usize = total / 2;
    let right: usize = total - left;
    format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
}

/// 按显示宽度截断,**不劈开半个汉字**。
///
/// ⚠️ 返回值宽度可能在 `width - 1 ..= width` 之间浮动:
/// 放不下一个 2 列宽的汉字时,它会少补 1 列。
/// 因此**任何「必须恰好等于 width」的断言都是错的**。
pub fn truncate_to_width(text: &str, width: usize) -> String {
    let mut used: 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 used + character_width > width {
            break;
        }
        result.push(character);
        used += character_width;
    }
    result
}

/// 可见省略:放不下时截断并追加 `...`,**让读者知道这里被省了**。
///
/// ⚠️ 必须先给 `...` **留 1 列**:写成 `truncate(text, width) + "..."`
/// 会让结果变成 `width + 1` 列,把整行顶歪。
pub fn elide_text(text: &str, width: usize) -> String {
    if display_width(text) <= width {
        return text.to_string();
    }
    // width 为 0 时无法放省略号,退化成空串(不 panic:这是排版,不是逻辑错误)。
    if width == 0 {
        return String::new();
    }
    let keep: usize = width - 1;
    format!("{}...", truncate_to_width(text, keep))
}

/// 生成一条指定宽度的分隔线。
///
/// ⚠️ 拼 `width` 个 `─`,**不是** `width / 2` 个。
/// `─`(U+2500) 按 1 列计(见模块文档),写 `width / 2` 会得到一半长的线。
pub fn horizontal_rule(width: usize) -> String {
    "─".repeat(width)
}

/// ★ 宽度模型的**永久自检**:返回 `(探针名, 是否通过, 实测说明)`。
///
/// 为什么要有它:宽度常量写错时,报表**看起来完全正常**(只是分隔线短一截、
/// 某一列窄一点),肉眼几乎不可能发现。自检把「看起来正常」变成「有数字对不上」。
///
/// 探针的选择:`0 / 1 / 2` 覆盖边界;`17 / 92 / 153` 覆盖奇偶与较大值。
pub fn self_check_width_model() -> Vec<(String, bool, String)> {
    let mut probes: Vec<(String, bool, String)> = Vec::new();

    // 探针一:`horizontal_rule(n)` 的宽度必须**恰好**是 n(制表符按 1 列计)。
    for width in [0usize, 1, 2, 17, 92, 153] {
        let rule: String = horizontal_rule(width);
        let actual: usize = display_width(&rule);
        probes.push((
            format!("分隔线宽度 {}", width),
            actual == width,
            format!("期望 {} 列,实测 {} 列", width, actual),
        ));
    }

    // 探针二:`elide_text` 的结果**绝不超过**目标宽度(省略号占 1 列)。
    for width in [1usize, 2, 17, 92] {
        let source: String = "工序段".repeat(60);
        let elided: String = elide_text(&source, width);
        let actual: usize = display_width(&elided);
        probes.push((
            format!("省略后宽度 ≤ {}", width),
            actual <= width,
            format!("原串 {} 列,省略后 {} 列", display_width(&source), actual),
        ));
    }

    // 探针三:够宽时 `elide_text` 必须**原样返回**(一个字都不能少)。
    let untouched: &str = "镶嵌工艺路线卡";
    let pass_through: String = elide_text(untouched, 40);
    probes.push((
        "够宽时原样返回".to_string(),
        pass_through == untouched,
        format!("原串 {} 列", display_width(untouched)),
    ));

    probes
}
rust 复制代码
//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : audit_check_view.rs
//! 审计结果视图:**表示层能认识的那一版契约审计结果**。
//!
//! ## 为什么要有这一层视图
//!
//! 契约审计由机制层 `pool::pool_audit` 产出(类型是 `PoolAuditCheck`)。
//! 若表示层直接 import 它,`app -> pool` 就会出现------
//! 而表示层的红线是「只认识 `analysis` 与 `domain`」。
//!
//! 这不是形式主义:表示层一旦能直接读机制层类型,
//! 「这一列显示什么」就会被机制的结构牵着走,
//! 于是改一条规则就要改报表------分层就此失效。
//!
//! ## ★ 视图里带上「已知会失败的输入」
//!
//! 每条规则的 `known_failing_input_text()` 一并带过来,
//! 由报表**逐条印出**。读者能看见「这条规则在什么数据上会红」,
//! 才可能相信它现在是绿的不是因为没在跑。

use crate::pool::PoolAuditCheck;

/// 一条契约检查的只读视图。
#[derive(Clone, Debug)]
pub struct AuditCheckView {
    /// 规则编码。
    rule_code: String,
    /// 规则中文名。
    rule_name: String,
    /// 期望文本。
    expectation_text: String,
    /// 实测文本。
    actual_text: String,
    /// 是否通过。
    passed: bool,
    /// 违规对象(用 `、` 连接,无则 `---`)。
    violation_text: String,
    /// ★ 已知会失败的输入。
    known_failing_input_text: String,
}

impl AuditCheckView {
    /// 由机制层的检查结果生成视图。
    pub fn from_check(check: &PoolAuditCheck) -> Self {
        Self {
            rule_code: check.rule().code().to_string(),
            rule_name: check.rule().chinese_text().to_string(),
            expectation_text: format!("{}/{}", check.conforming_total(), check.applicable_total()),
            actual_text: check.actual_text().to_string(),
            passed: check.passed(),
            violation_text: if check.violating_names().is_empty() {
                "---".to_string()
            } else {
                check.violating_names().join("、")
            },
            known_failing_input_text: check.rule().known_failing_input_text().to_string(),
        }
    }

    /// 规则编码。
    pub fn rule_code(&self) -> &str {
        self.rule_code.as_str()
    }

    /// 规则中文名。
    pub fn rule_name(&self) -> &str {
        self.rule_name.as_str()
    }

    /// 期望文本。
    pub fn expectation_text(&self) -> &str {
        self.expectation_text.as_str()
    }

    /// 实测文本。
    pub fn actual_text(&self) -> &str {
        self.actual_text.as_str()
    }

    /// 是否通过。
    pub const fn passed(&self) -> bool {
        self.passed
    }

    /// 违规对象文本。
    pub fn violation_text(&self) -> &str {
        self.violation_text.as_str()
    }

    /// 已知会失败的输入。
    pub fn known_failing_input_text(&self) -> &str {
        self.known_failing_input_text.as_str()
    }
}

/// 批量转换。
pub fn audit_check_views(checks: &[PoolAuditCheck]) -> Vec<AuditCheckView> {
    checks.iter().map(AuditCheckView::from_check).collect()
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : capacity_census.rs
//! 容量盘点:**池里到底有几台,它们各在哪**。
//!
//! ## ★ 容量守恒是月底唯一能对账的东西
//!
//! 空闲 + 在借 + 隔离 == 已造 − 已退役。
//! 这条等式一旦失真,报表上所有数字都还能看、还自洽,
//! 唯独「我们一共有几台设备」没人能回答------
//! 而那恰恰是资产报表存在的理由。
//!
//! ## ⚠️ 退役**不计入**容量
//!
//! 退役的仪器已经不是池的一部分。把它们算进容量,
//! 「限量扩容」的上限会被一批再也回不来的对象占住,
//! 于是池会永远拒绝借出,而报表上的「已造 8 台」看起来完全正常。

use crate::analysis::CheckLine;
use crate::client::PoolSession;

/// 一座站的容量探针。
#[derive(Clone, Copy, Debug)]
pub struct CapacityProbe {
    /// 站名。
    station_name_text: usize,
    /// 空闲台数。
    idle_total: usize,
    /// 在借台数。
    borrowed_total: usize,
    /// 隔离台数。
    quarantined_total: usize,
    /// 退役台数。
    retired_total: usize,
    /// 已造台数。
    created_total: usize,
    /// 借出峰值。
    peak_borrowed_total: usize,
}

impl CapacityProbe {
    /// 由一次会话生成探针。
    ///
    /// ⚠️ `station_name_text` 这个字段刻意**只存名字长度**而不是名字:
    /// 站名是 `app` 的事,本层只关心「有几座站」。
    /// 若在这里存名字,本文件就要引入 `String` 与站名的拼接规则,
    /// 而那条规则应该只有一处(在 `app`)。
    pub fn from_session(session: &PoolSession, station_name_length: usize) -> Self {
        let observation = session.station().observation();
        Self {
            station_name_text: station_name_length,
            idle_total: observation.idle_total,
            borrowed_total: observation.borrowed_total,
            quarantined_total: observation.quarantined_total,
            retired_total: observation.retired_total,
            created_total: observation.created_total,
            peak_borrowed_total: session.station().peak_borrowed_total(),
        }
    }

    /// 站名长度(**只用于体检**:站名不为空)。
    pub const fn station_name_text(&self) -> usize {
        self.station_name_text
    }

    /// 空闲台数。
    pub const fn idle_total(&self) -> usize {
        self.idle_total
    }

    /// 在借台数。
    pub const fn borrowed_total(&self) -> usize {
        self.borrowed_total
    }

    /// 隔离台数。
    pub const fn quarantined_total(&self) -> usize {
        self.quarantined_total
    }

    /// 退役台数。
    pub const fn retired_total(&self) -> usize {
        self.retired_total
    }

    /// 已造台数。
    pub const fn created_total(&self) -> usize {
        self.created_total
    }

    /// 借出峰值。
    pub const fn peak_borrowed_total(&self) -> usize {
        self.peak_borrowed_total
    }

    /// ★ 容量总数(空闲 + 在借 + 隔离)。
    pub const fn capacity_total(&self) -> usize {
        self.idle_total + self.borrowed_total + self.quarantined_total
    }

    /// ★ 容量守恒的差:容量 − (已造 − 已退役)。
    pub const fn capacity_gap(&self) -> i64 {
        self.capacity_total() as i64 - (self.created_total as i64 - self.retired_total as i64)
    }
}

/// 容量盘点的四条检查。
pub fn capacity_checks(probes: &[CapacityProbe]) -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();

    // ① ★ 容量守恒:差 == 0。
    //    已知会失败的输入:take_quarantined() 之后既不送回也不退役。
    let conserved: usize = probes.iter().filter(|probe| probe.capacity_gap() == 0).count();
    lines.push(CheckLine::new(
        "★ 容量守恒:空闲+在借+隔离 == 已造−已退役",
        format!("{} 座站全部为 0(已知会失败:取走隔离对象后不处理)", probes.len()).as_str(),
        format!("{}/{}", conserved, probes.len()).as_str(),
        conserved == probes.len(),
    ));

    // ② 隔离区已清空:业务层及时处理过。
    //    已知会失败的输入:把 service_quarantined() 挪到循环外只调一次。
    let cleared: usize = probes
        .iter()
        .filter(|probe| probe.quarantined_total() == 0)
        .count();
    lines.push(CheckLine::new(
        "隔离区已清空(归还校验没过的都处理过了)",
        format!("{} 座站全部为 0(已知会失败:把处理挪到循环外只调一次)", probes.len()).as_str(),
        format!("{}/{}", cleared, probes.len()).as_str(),
        cleared == probes.len(),
    ));

    // ③ ★ 已造台数 >= 借出峰值(否则峰值那一刻是怎么借到的)。
    //    已知会失败的输入:把峰值改成「台账里的最大值」而不是在借时更新。
    let enough: usize = probes
        .iter()
        .filter(|probe| probe.created_total() >= probe.peak_borrowed_total())
        .count();
    lines.push(CheckLine::new(
        "★ 已造台数 >= 借出峰值(峰值那刻够借)",
        format!("{} 座站全部成立(已知会失败:峰值事后从台账推)", probes.len()).as_str(),
        format!("{}/{}", enough, probes.len()).as_str(),
        enough == probes.len(),
    ));

    // ④ 站名非空(**防呆**:站名空了报表会有一列全是空格)。
    let named: usize = probes
        .iter()
        .filter(|probe| probe.station_name_text() > 0)
        .count();
    lines.push(CheckLine::new(
        "每座站都有名字",
        format!("{} 座站全部非空(已知会失败:型号中文名写成空串)", probes.len()).as_str(),
        format!("{}/{}", named, probes.len()).as_str(),
        named == probes.len(),
    ));

    lines
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : check_line.rs
//! 检查项与检查报告:全工程「应当 == 实测」的**统一形状**。
//!
//! ## 为什么单独一个文件
//!
//! 工程里有六处独立的检查(求值纪律、读取时刻、契约审计、恒等、拓扑、价值量化),
//! 若各写一套「期望/实测/结论」,最后一张体检表就得认识六种类型。
//! 统一成 [`CheckLine`] 之后,体检表只有一个来源,
//! 而**新增一处检查 = 多几行 `push`**,不需要改体检表。
//!
//! ## ★ 为什么 `verdict_text()` 只在这里产生一次
//!
//! 「通过 / 未通过」的写法若散在各处,迟早会出现一处写成「符合/不符合」,
//! 而读者会以为那是另一种判断。判据只有一个:`passed`。

/// 一条检查项。
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CheckLine {
    /// 检查项名称。
    item: String,
    /// 期望文本(**应当**是什么)。
    expectation_text: String,
    /// 实测文本(**实际**是什么)。
    actual_text: String,
    /// 是否通过。
    passed: bool,
}

impl CheckLine {
    /// 由四个字段构造。
    pub fn new(item: &str, expectation_text: &str, actual_text: &str, passed: bool) -> Self {
        Self {
            item: item.to_string(),
            expectation_text: expectation_text.to_string(),
            actual_text: actual_text.to_string(),
            passed,
        }
    }

    /// 检查项名称。
    pub fn item(&self) -> &str {
        &self.item
    }

    /// 期望文本。
    pub fn expectation_text(&self) -> &str {
        &self.expectation_text
    }

    /// 实测文本。
    pub fn actual_text(&self) -> &str {
        &self.actual_text
    }

    /// 是否通过。
    pub const fn passed(&self) -> bool {
        self.passed
    }

    /// ★ 结论文本:全工程唯一一处产生「通过 / 未通过」的地方。
    pub const fn verdict_text(&self) -> &'static str {
        if self.passed {
            "通过"
        } else {
            "未通过"
        }
    }
}

/// 一组检查项。
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CheckReport {
    /// 检查项清单。
    lines: Vec<CheckLine>,
}

impl CheckReport {
    /// 空报告。
    pub fn new() -> Self {
        Self { lines: Vec::new() }
    }

    /// 追加一条。
    pub fn push(&mut self, line: CheckLine) {
        self.lines.push(line);
    }

    /// 全部检查项。
    pub fn lines(&self) -> &[CheckLine] {
        &self.lines
    }

    /// 条数。
    pub fn line_total(&self) -> usize {
        self.lines.len()
    }

    /// 通过条数。
    pub fn passed_total(&self) -> usize {
        self.lines.iter().filter(|line| line.passed()).count()
    }

    /// 未通过条数。
    pub fn failed_total(&self) -> usize {
        self.line_total() - self.passed_total()
    }

    /// 是否全部通过。
    pub fn all_passed(&self) -> bool {
        self.passed_total() == self.line_total()
    }

    /// 未通过的检查项名称(拼成一串,供注解使用)。
    ///
    /// ⚠️ 全部通过时返回「无」而不是空串:空串在报表上看起来像「忘了填」。
    pub fn failed_items_text(&self) -> String {
        let failed: Vec<&str> = self
            .lines
            .iter()
            .filter(|line| !line.passed())
            .map(|line| line.item())
            .collect();
        if failed.is_empty() {
            "无".to_string()
        } else {
            failed.join("、")
        }
    }
}

impl Default for CheckReport {
    fn default() -> Self {
        Self::new()
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : identity_census.rs
//! 身份证据:**池里的那台,就是当初借出去的那台**。
//!
//! ## ★ 这一条是对象池与「工厂」的分界线
//!
//! 一个池若每次归还时**新建一个一模一样的**放回去
//! (同序列号、同状态、同累计次数),
//! 那么台账上的「复用次数」一样会 +1,
//! 「省下的采购金额」一样算得出来------
//! 但那是假复用:真正的省来自「**同一台**设备被第二次使用」。
//!
//! 因此本文件不比对序列号,而是比对**指针**(`Rc::ptr_eq`):
//! 序列号可以被复制,指针不能。
//!
//! ## ⚠️ 绝不打印地址
//!
//! 指针相等是/否可以进报表,指针**值**不行:
//! 地址随运行变(ASLR),打印出来会破坏「两次运行逐字节一致」。

use crate::analysis::CheckLine;
use crate::client::IdentityEvidence;

/// 身份证据的四条检查。
///
/// # 参数
/// - `evidence`:会话产出的身份证据。
/// - `retired_total`:本次会话退役了多少台(由调用方数出来,不在这里猜)。
pub fn identity_checks(evidence: &IdentityEvidence, retired_total: usize) -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();

    // ① ★ 复用确实发生:借出次数 > 用到的不同对象数。
    //    已知会失败的输入:每次都新建(那不是池,是工厂)。
    lines.push(CheckLine::new(
        "★ 复用确实发生:借出次数 > 用到的不同台数",
        "借出次数严格大于不同台数(已知会失败:每次借出都新建一台)",
        format!(
            "{} > {}",
            evidence.acquisition_total, evidence.distinct_object_total
        )
        .as_str(),
        evidence.reuse_happened(),
    ));

    // ② 用到的不同台数 <= 已造台数(不可能用了没造过的设备)。
    //    已知会失败的输入:把 created_total 改成「预热台数」而不是累计造过数。
    lines.push(CheckLine::new(
        "用到的不同台数 <= 已造台数",
        "<=(已知会失败:把已造台数写成预热台数)",
        format!(
            "{} <= {}",
            evidence.distinct_object_total, evidence.created_total
        )
        .as_str(),
        evidence.distinct_object_total <= evidence.created_total,
    ));

    // ③ ★ 仍在池里的台数 == 不同台数 − 退役台数。
    //    已知会失败的输入:退役时忘记把对象从池里移出去。
    let expected_in_pool: i64 = evidence.distinct_object_total as i64 - retired_total as i64;
    lines.push(CheckLine::new(
        "★ 仍在池里的台数 == 不同台数 − 退役台数",
        "相等(已知会失败:退役时忘了把对象移出池)",
        format!(
            "{} == {} − {}",
            evidence.still_in_pool_total, evidence.distinct_object_total, retired_total
        )
        .as_str(),
        evidence.still_in_pool_total as i64 == expected_in_pool,
    ));

    // ④ 平均每台被用次数 > 1(万分点口径:> 10000)。
    //    已知会失败的输入:申请只有 1 条(那时必然是 1.0000)。
    let per_object: i64 = evidence.usage_per_object_basis_points();
    lines.push(CheckLine::new(
        "平均每台被用次数 > 1.0000",
        "> 10000 万分点(已知会失败:申请只有 1 条)",
        format!("{} 万分点", per_object).as_str(),
        per_object > 10_000,
    ));

    lines
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : instrument_view.rs
//! 仪器个体视图:**表示层能认识的那一版仪器**。
//!
//! ## 为什么要有视图类型
//!
//! 表示层要显示「序列号 / 型号 / 状态 / 累计次数 / 持有人 / 校准到期月 / 采购单价」。
//! 若直接把 `InstrumentUnit` 交给表示层,
//! 表示层就拿到了「改状态」「加次数」的能力------
//! 而一张报表**改了数据**是最难被发现的一类错误。
//!
//! 视图里全是已经格式化好的 `String` 与不可变计数:
//! 表示层拿到它,唯一能做的事就是把它显示出来。
//!
//! ## ⚠️ 为什么文本在**分析层**格式化
//!
//! 判据与既有工程一致:「这个文本形式是不是所有层都要用」。
//! 金额、月份的中文写法在体检表、对照表、恒等表里都要用,
//! 放表示层会让 `analysis` 反向依赖 `app`。

use crate::domain::{InstrumentUnit, StoreCode};

/// 一台仪器的只读视图。
#[derive(Clone, Debug)]
pub struct InstrumentUnitView {
    /// 序列号(身份)。
    serial_number: String,
    /// 型号中文名。
    model_name: String,
    /// 状态中文名。
    state_text: String,
    /// 累计被借次数。
    usage_count: usize,
    /// 持有人文本(无人持有时为 `---`)。
    holder_text: String,
    /// 校准到期月文本。
    calibration_due_text: String,
    /// 采购单价文本。
    acquisition_cost_text: String,
    /// 指纹(跨运行比对用)。
    fingerprint: String,
}

impl InstrumentUnitView {
    /// 由一台仪器生成视图。
    pub fn from_unit(unit: &InstrumentUnit) -> Self {
        let holder_text: String = match unit.holder() {
            Some(store) => store.chinese_name().to_string(),
            None => "---".to_string(),
        };
        Self {
            serial_number: unit.identity_text(),
            model_name: unit.model().chinese_name().to_string(),
            state_text: unit.state().chinese_text().to_string(),
            usage_count: unit.usage_count(),
            holder_text,
            calibration_due_text: unit.calibration_due_month().chinese_text(),
            acquisition_cost_text: unit.acquisition_cost().chinese_text(),
            fingerprint: unit.fingerprint_text(),
        }
    }

    /// 序列号。
    pub fn serial_number(&self) -> &str {
        self.serial_number.as_str()
    }

    /// 型号中文名。
    pub fn model_name(&self) -> &str {
        self.model_name.as_str()
    }

    /// 状态中文名。
    pub fn state_text(&self) -> &str {
        self.state_text.as_str()
    }

    /// 累计被借次数。
    pub const fn usage_count(&self) -> usize {
        self.usage_count
    }

    /// 持有人文本。
    pub fn holder_text(&self) -> &str {
        self.holder_text.as_str()
    }

    /// 校准到期月文本。
    pub fn calibration_due_text(&self) -> &str {
        self.calibration_due_text.as_str()
    }

    /// 采购单价文本。
    pub fn acquisition_cost_text(&self) -> &str {
        self.acquisition_cost_text.as_str()
    }

    /// 指纹。
    pub fn fingerprint(&self) -> &str {
        self.fingerprint.as_str()
    }
}

/// 门店文本(无人时为 `---`)。
///
/// ⚠️ 单独一个函数而不是在调用点 `map_or`:
/// 「没人持有」的显示方式全工程只有一种,
/// 写在调用点里就会出现一处写 `---`、一处写 `无` 的分裂。
pub fn holder_display_text(holder: Option<StoreCode>) -> String {
    match holder {
        Some(store) => store.chinese_name().to_string(),
        None => "---".to_string(),
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : ledger_snapshot_view.rs
//! 台账快照视图:**表示层能认识的那一版池台账**。
//!
//! ## ★ 为什么报表读快照而不是实时计数
//!
//! 池台账**只增不减**,但它一直在增:
//! 渲染「采购次数」这一行时的池,与渲染「归还次数」这一行时的池
//! 是**同一时刻**的(渲染期间没有新动作)。
//! 这听起来很安全------直到有人在幕十三为了清点接口又借了一次,
//! 于是先渲染的几行与后渲染的几行来自不同时刻,
//! 读者会以为「清点没有副作用」,事实恰好相反。
//!
//! 快照把九个计数**一次取齐**,让一张表里的十行必然同源。
//!
//! ## ⚠️ 这个视图本身是「那份事故」的产物
//!
//! 既有工程在幕十三做过一次「为了展示而多借一次」,
//! 结果「命中次数 == 借到 − 新建」差了 1(61 vs 60)。
//! 那个 1 没有任何一行输出指出它的来源------
//! 因此本工程从第一版起就规定:**报表一律读快照**。

use crate::pool::PoolSnapshot;

/// 台账快照的只读视图(**行标题 + 值的文本**)。
#[derive(Clone, Debug)]
pub struct LedgerSnapshotView {
    /// 逐行(行标题,值文本)。
    rows: Vec<(String, String)>,
}

impl LedgerSnapshotView {
    /// 由快照生成视图。
    ///
    /// # 参数
    /// - `snapshot`:池台账快照。
    /// - `moment_text`:快照时刻的说明(**必须写清是哪个时刻**)。
    pub fn from_snapshot(snapshot: &PoolSnapshot) -> Self {
        let rows: Vec<(String, String)> = vec![
            (
                "★ 新建次数(真的花钱买了几台)".to_string(),
                format!("{} 次", snapshot.creation_total),
            ),
            (
                "★ 命中次数(借出时池里有空闲、直接复用)".to_string(),
                format!("{} 次", snapshot.hit_total),
            ),
            (
                "借出成功次数(命中 + 新建)".to_string(),
                format!("{} 次", snapshot.acquisition_total),
            ),
            (
                "★ 拒绝次数(池空且策略不允许新建)".to_string(),
                format!("{} 次", snapshot.rejection_total),
            ),
            (
                "归还次数(凭句柄归还的总数)".to_string(),
                format!("{} 次", snapshot.release_total),
            ),
            (
                "★ 回池次数(真正省下一次采购的那些次)".to_string(),
                format!("{} 次", snapshot.reuse_total),
            ),
            (
                "★ 隔离次数(归还校验没过)".to_string(),
                format!("{} 次", snapshot.quarantine_total),
            ),
            (
                "异物归还次数(正常必须为 0)".to_string(),
                format!("{} 次", snapshot.foreign_return_total),
            ),
            (
                "预热次数(开业前先备的货)".to_string(),
                format!("{} 次", snapshot.prewarm_total),
            ),
            (
                "★ 恒等式一:借出 − (命中 + 新建 − 预热)".to_string(),
                format!("{}", snapshot.acquisition_identity_gap()),
            ),
            (
                "恒等式二:归还 − (回池 + 隔离)".to_string(),
                format!("{}", snapshot.release_identity_gap()),
            ),
            (
                "复用率(命中 / 借出成功)".to_string(),
                format!("{} 万分点", snapshot.reuse_ratio_basis_points()),
            ),
        ];
        Self { rows }
    }

    /// 逐行。
    pub fn rows(&self) -> &[(String, String)] {
        self.rows.as_slice()
    }

    /// 行数。
    pub fn row_total(&self) -> usize {
        self.rows.len()
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : lifecycle_census.rs
//! 生命周期盘点:**每一台仪器此刻在哪一档状态上**。
//!
//! ## ★ 为什么按状态数台数,而不是只看「总共几台」
//!
//! 「一共 8 台」是一个无法验证的数字:
//! 它同时包含「8 台都能用」与「2 台能用、6 台在修」。
//! 按状态分摊之后,「能用的有几台」才有答案,
//! 而那正是借用申请真正关心的量。
//!
//! ## ⚠️ 状态为 0 的档也要出现在表里
//!
//! 「校准维修 0 台」与「报表里压根没有这一行」是两件不同的事:
//! 前者说明没事发生,后者说明这一档**没被统计**。
//! 因此本文件的统计**遍历内置状态全集**而不是遍历实际出现过的状态。

use crate::analysis::InstrumentUnitView;
use crate::domain::{ServiceKind, ServiceRecord, BUILTIN_INSTRUMENT_STATES};
use crate::analysis::CheckLine;

/// 一档状态的台数。
#[derive(Clone, Copy, Debug)]
pub struct LifecycleProbe {
    /// 状态中文名。
    state_name: &'static str,
    /// 状态机器编码。
    state_machine: &'static str,
    /// 处于该状态的台数。
    unit_total: usize,
    /// 该状态是否**在池里**(`Idle` 才为真)。
    in_pool: bool,
}

impl LifecycleProbe {
    /// 状态中文名。
    pub const fn state_name(&self) -> &'static str {
        self.state_name
    }

    /// 状态机器编码。
    pub const fn state_machine(&self) -> &'static str {
        self.state_machine
    }

    /// 台数。
    pub const fn unit_total(&self) -> usize {
        self.unit_total
    }

    /// 是否在池里。
    pub const fn in_pool(&self) -> bool {
        self.in_pool
    }
}

/// 按状态统计台数(**遍历内置状态全集**,理由见模块文档)。
pub fn lifecycle_statistics(views: &[InstrumentUnitView]) -> Vec<LifecycleProbe> {
    BUILTIN_INSTRUMENT_STATES
        .iter()
        .map(|state| LifecycleProbe {
            state_name: state.chinese_text(),
            state_machine: state.machine_text(),
            unit_total: views
                .iter()
                .filter(|view| view.state_text() == state.chinese_text())
                .count(),
            in_pool: state.is_in_pool(),
        })
        .collect()
}

/// 服务记录统计(按类型)。
pub fn service_statistics(records: &[ServiceRecord]) -> Vec<(ServiceKind, usize)> {
    crate::domain::BUILTIN_SERVICE_KINDS
        .iter()
        .map(|kind| {
            let total: usize = records.iter().filter(|record| record.kind() == *kind).count();
            (*kind, total)
        })
        .collect()
}

/// 生命周期的四条检查。
///
/// # 参数
/// - `probes`:状态分布。
/// - `views`:全部仪器视图(用于查累计次数上限)。
/// - `records`:服务记录。
/// - `quarantined_total`:本次会话归还后被隔离的笔数。
pub fn lifecycle_checks(
    probes: &[LifecycleProbe],
    views: &[InstrumentUnitView],
    records: &[ServiceRecord],
    quarantined_total: usize,
) -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();

    // ① 状态档数 == 4(为 0 的档也要在表里)。
    //    已知会失败的输入:把统计改成「遍历实际出现过的状态」。
    lines.push(CheckLine::new(
        "状态档数 == 4(为 0 的档也要出现)",
        "4 档(已知会失败:改成遍历实际出现过的状态)",
        format!("{} 档", probes.len()).as_str(),
        probes.len() == BUILTIN_INSTRUMENT_STATES.len(),
    ));

    // ② ★ 服务记录数 == 隔离笔数(每隔离一次必有且只有一条服务记录)。
    //    已知会失败的输入:归还时先提前 return,漏掉一次服务登记。
    lines.push(CheckLine::new(
        "★ 服务记录数 == 归还后被隔离的笔数",
        "相等(已知会失败:归还时漏登记一次服务)",
        format!("{} == {}", records.len(), quarantined_total).as_str(),
        records.len() == quarantined_total,
    ));

    // ③ ★ 退役台数 == 退役类服务记录数。
    //    已知会失败的输入:直接调 mark_retired() 而不记服务记录。
    let retired_units: usize = probes
        .iter()
        .filter(|probe| probe.state_machine() == "RETIRED")
        .map(|probe| probe.unit_total())
        .sum();
    let retirement_records: usize = records
        .iter()
        .filter(|record| record.kind() == ServiceKind::Retirement)
        .count();
    lines.push(CheckLine::new(
        "★ 退役台数 == 退役类服务记录数",
        "相等(已知会失败:直接调退役而不记服务记录)",
        format!("{} == {}", retired_units, retirement_records).as_str(),
        retired_units == retirement_records,
    ));

    // ④ 每台仪器的累计次数 <= 退役上限。
    //    已知会失败的输入:把上限改成 0(那第一台借出后就会超)。
    let limit: usize = crate::domain::RETIREMENT_USAGE_LIMIT;
    let over: usize = views.iter().filter(|view| view.usage_count() > limit).count();
    lines.push(CheckLine::new(
        "每台累计使用次数 <= 退役上限",
        format!("0 台超限(上限 {},已知会失败:把上限改成 0)", limit).as_str(),
        format!("{} 台超限", over).as_str(),
        over == 0,
    ));

    lines
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : policy_census.rs
//! 三档策略对照:**同一份申请序列,三种「池借空了怎么办」**。
//!
//! ## ★ 本文件是「对象池到底省了什么」的唯一出处
//!
//! 「省了多少」这句话最常见的失效方式是:
//! 只有一个方案,于是它的数字既是结论也是唯一的证据。
//! 三档跑同一份序列后,「省下的采购台数」变成
//! **两个可数数字之差**(按需扩容的新建次数 − 严格池的新建次数),
//! 而不是一句主张。
//!
//! ## ⚠️ 为什么不用「耗时」当度量
//!
//! 耗时不可复现(同一份代码两次运行差 20% 很正常),
//! 而本工程要求「两次运行逐字节一致」。
//! 「新建次数」与「采购金额」都是**整点数**,可数、可复算、可跨运行比对。

use crate::analysis::CheckLine;
use crate::client::PoolSession;
use crate::domain::{Money, CNY};

/// 一档策略的对照探针。
#[derive(Clone, Debug)]
pub struct PolicyProbe {
    /// 策略中文名。
    policy_name: String,
    /// 策略机器编码。
    policy_machine: String,
    /// 该策略**是否可能**拒绝借出。
    can_reject: bool,
    /// 申请笔数。
    request_total: usize,
    /// 借到笔数。
    granted_total: usize,
    /// 没借到笔数。
    rejected_total: usize,
    /// ★ 新建次数(**买过多少台**,**含预热**)。
    created_total: usize,
    /// ★ 预热台数(含在 `created_total` 里,恒等式里要减掉它)。
    prewarm_total: usize,
    /// ★ 命中次数(**复用过多少次**)。
    hit_total: usize,
    /// 归还后回池笔数。
    returned_total: usize,
    /// 归还后隔离笔数。
    quarantined_total: usize,
    /// ★ 借出峰值之和(**各型号峰值相加** = 不池化要买几台的合计)。
    ///
    /// ⚠️ 为什么是「各型号之和」而不是「最大值」:
    /// 一个型号一座站,「不池化要买几台」要对**每个型号**各买够它的峰值,
    /// 取最大值会把另外两个型号的需求抹掉------
    /// 那时「省下几台」会凭空变小,而这个变小没有任何一行输出会指出。
    peak_borrowed_total: usize,
    /// 采购花掉的钱(**分**,累加而不是取最后一站的文本)。
    acquisition_cost_minor_units: i64,
    /// 服务花掉的钱(**分**,逐条累加)。
    service_cost_minor_units: i64,
}

impl PolicyProbe {
    /// 由一次会话生成探针。
    pub fn from_session(caption: &str, session: &PoolSession) -> Self {
        let observation = session.station().observation();
        Self {
            policy_name: caption.to_string(),
            policy_machine: observation.policy.machine_text().to_string(),
            can_reject: observation.policy.can_reject(),
            request_total: session.borrow_records().len(),
            granted_total: session.granted_total(),
            rejected_total: session.rejected_total(),
            created_total: observation.created_total,
            prewarm_total: observation.prewarm_total,
            hit_total: observation.hit_total,
            returned_total: session.returned_to_pool_total(),
            quarantined_total: session.quarantined_total(),
            peak_borrowed_total: session.station().peak_borrowed_total(),
            acquisition_cost_minor_units: session.station().acquisition_cost_spent().minor_units(),
            service_cost_minor_units: session.station().service_cost_spent().minor_units(),
        }
    }

    /// ★ 由**多座站**的会话汇总出一档的探针。
    ///
    /// 为什么需要汇总版:一个型号一座站,三座站合起来才是「这一档」的全貌。
    /// 若只拿天平一座站做对照,显微镜那种「峰值 1、池化与不池化一样」
    /// 的型号就被悄悄排除了------
    /// 而**恰恰是那种型号**说明了池不是万能的。
    pub fn from_sessions(caption: &str, sessions: &[PoolSession]) -> Self {
        let mut probe: Option<PolicyProbe> = None;
        for session in sessions.iter() {
            let part: PolicyProbe = PolicyProbe::from_session(caption, session);
            probe = Some(match probe {
                None => part,
                Some(mut accumulated) => {
                    accumulated.request_total += part.request_total;
                    accumulated.granted_total += part.granted_total;
                    accumulated.rejected_total += part.rejected_total;
                    accumulated.created_total += part.created_total;
                    accumulated.prewarm_total += part.prewarm_total;
                    accumulated.acquisition_cost_minor_units += part.acquisition_cost_minor_units;
                    accumulated.service_cost_minor_units += part.service_cost_minor_units;
                    accumulated.hit_total += part.hit_total;
                    accumulated.returned_total += part.returned_total;
                    accumulated.quarantined_total += part.quarantined_total;
                    accumulated.peak_borrowed_total += part.peak_borrowed_total;
                    // 金额文本汇总不了(已是格式化好的串),保留最后一座站的文本,
                    // 报表里会另有一张**按型号**的明细表给出逐型号金额。
                    accumulated
                }
            });
        }
        probe.expect("至少要有一次会话才能汇总出探针")
    }

    /// 策略中文名。
    pub fn policy_name(&self) -> &str {
        self.policy_name.as_str()
    }

    /// 策略机器编码。
    pub fn policy_machine(&self) -> &str {
        self.policy_machine.as_str()
    }

    /// 是否可能拒绝。
    pub const fn can_reject(&self) -> bool {
        self.can_reject
    }

    /// 申请笔数。
    pub const fn request_total(&self) -> usize {
        self.request_total
    }

    /// 借到笔数。
    pub const fn granted_total(&self) -> usize {
        self.granted_total
    }

    /// 没借到笔数。
    pub const fn rejected_total(&self) -> usize {
        self.rejected_total
    }

    /// 新建次数。
    pub const fn created_total(&self) -> usize {
        self.created_total
    }

    /// 命中次数。
    pub const fn hit_total(&self) -> usize {
        self.hit_total
    }

    /// 回池笔数。
    pub const fn returned_total(&self) -> usize {
        self.returned_total
    }

    /// 隔离笔数。
    pub const fn quarantined_total(&self) -> usize {
        self.quarantined_total
    }

    /// 借出峰值。
    pub const fn peak_borrowed_total(&self) -> usize {
        self.peak_borrowed_total
    }

    /// 预热台数。
    ///
    /// ⚠️ 这个数是「借出 == 命中 + (新建 − 预热)」这条恒等式的一部分,
    /// 缺了它,恒等式会差一个预热台数------而那个差看起来就像漏记了一次借出。
    pub const fn prewarm_total(&self) -> usize {
        self.prewarm_total
    }

    /// 采购花掉的钱文本。
    pub fn acquisition_cost_text(&self) -> String {
        Money::new(self.acquisition_cost_minor_units, CNY).chinese_text()
    }

    /// 服务花掉的钱文本。
    pub fn service_cost_text(&self) -> String {
        Money::new(self.service_cost_minor_units, CNY).chinese_text()
    }

    /// 采购花掉的钱(分)。
    pub const fn acquisition_cost_minor_units(&self) -> i64 {
        self.acquisition_cost_minor_units
    }

    /// 服务花掉的钱(分)。
    pub const fn service_cost_minor_units(&self) -> i64 {
        self.service_cost_minor_units
    }

    /// ★ 危险等级:**只看两个数**(命中次数与新建次数)。
    ///
    /// 判据刻意不含策略名、不含金额:
    /// 「这台设备被复用过没有」只由这两个数回答,
    /// 掺进别的信息,读者就得先相信那段信息才能看懂这一列。
    pub fn risk_text(&self) -> String {
        if self.hit_total == 0 {
            return "★★ 一次都没复用:这不是池,这是工厂".to_string();
        }
        if self.created_total >= self.granted_total {
            return "★★ 每台都是新买的:池完全没起作用".to_string();
        }
        // ⚠️ 这里**只说复用次数**,不说「省下几次采购」:
        // 「省下几台」是**两个新建次数之差**(见幕十一),不是命中次数。
        // 在同一张表里混用两个口径,读者会以为它们本来就该相等。
        format!("★ 复用 {} 次(省下几台见幕十一)", self.hit_total)
    }

    /// 复用率(万分点)。
    pub fn reuse_ratio_basis_points(&self) -> i64 {
        if self.granted_total == 0 {
            return 0;
        }
        ((self.hit_total as i128) * 10_000 / (self.granted_total as i128)) as i64
    }
}

/// ★ 三档对照的六条检查。
///
/// 每条都带「已知会失败的输入」------
/// 一条不可能失败的规则不是规则,它只是没被跑过的注释。
pub fn policy_checks(probes: &[PolicyProbe]) -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();

    // ① ★ 三档齐全:三个机器编码必须各出现一次。
    //    已知会失败的输入:只跑一档(最常见的「对照」其实是独奏)。
    let required: [&str; 3] = ["STRICT_POOL", "GROW_ON_DEMAND", "GROW_WITH_LIMIT"];
    let covered: usize = required
        .iter()
        .filter(|code| probes.iter().any(|probe| probe.policy_machine() == **code))
        .count();
    lines.push(CheckLine::new(
        "三档策略齐全(严格池 / 按需扩容 / 限量扩容)",
        "3 种机器编码全部出现(已知会失败:只跑一档)",
        format!("{} 种", covered).as_str(),
        covered == required.len(),
    ));

    // ② 每档:借到 + 没借到 == 申请总数。
    //    已知会失败的输入:新增一条「既不借也不拒」的分支(比如静默跳过)。
    let mut balanced: usize = 0;
    for probe in probes.iter() {
        if probe.granted_total() + probe.rejected_total() == probe.request_total() {
            balanced += 1;
        }
    }
    lines.push(CheckLine::new(
        "每档:借到 + 没借到 == 申请总数",
        format!("{} 档全部成立(已知会失败:新增一条静默跳过的分支)", probes.len()).as_str(),
        format!("{}/{}", balanced, probes.len()).as_str(),
        balanced == probes.len(),
    ));

    // ③ ★ 每档:借到 == 命中 + (新建 − 预热)。
    //    ⚠️ 减掉预热是**被跑出来的**:第一版漏了这一项,
    //    于是严格池「借到 8、命中 8、新建 3」被判成不相等------
    //    差 3 恰好是三个型号的预热台数。
    let mut identity_ok: usize = 0;
    for probe in probes.iter() {
        if probe.granted_total()
            == probe.hit_total() + probe.created_total() - probe.prewarm_total()
        {
            identity_ok += 1;
        }
    }
    lines.push(CheckLine::new(
        "★ 每档:借到 == 命中 + (新建 − 预热)",
        format!("{} 档全部成立(已知会失败:某条借出路径漏记一个计数)", probes.len()).as_str(),
        format!("{}/{}", identity_ok, probes.len()).as_str(),
        identity_ok == probes.len(),
    ));

    // ④ ★ 严格池的新建次数 <= 按需扩容的新建次数。
    //    已知会失败的输入:把严格池的预热数设成 0(那时它也只能一路新建)。
    let strict: Option<&PolicyProbe> = probes.iter().find(|probe| probe.policy_machine() == "STRICT_POOL");
    let grow: Option<&PolicyProbe> = probes
        .iter()
        .find(|probe| probe.policy_machine() == "GROW_ON_DEMAND");
    let (strict_created, grow_created): (usize, usize) = match (strict, grow) {
        (Some(left), Some(right)) => (left.created_total(), right.created_total()),
        _ => (0, 0),
    };
    lines.push(CheckLine::new(
        "★ 严格池的新建次数 <= 按需扩容的新建次数",
        "严格池不该比不池化买得更多(已知会失败:预热设成 0)",
        format!("{} <= {}", strict_created, grow_created).as_str(),
        strict_created <= grow_created,
    ));

    // ⑤ ★ 至少有一档的命中次数 > 0(池确实复用上了)。
    //    已知会失败的输入:预热 0 台且只借一次。
    let effective_total: usize = probes.iter().filter(|probe| probe.hit_total() > 0).count();
    lines.push(CheckLine::new(
        "★ 至少一档命中次数 > 0(池确实复用上了)",
        ">= 1 档(已知会失败:预热 0 台且只借一次)",
        format!("{} 档", effective_total).as_str(),
        effective_total >= 1,
    ));

    // ⑥ ★ 至少有一档拒绝次数 > 0(容量约束真实存在)。
    //    已知会失败的输入:把限量扩容的上限调到很大(那时它永不拒绝)。
    let rejecting_total: usize = probes.iter().filter(|probe| probe.rejected_total() > 0).count();
    lines.push(CheckLine::new(
        "★ 至少一档出现「借不到」(容量约束真实存在)",
        ">= 1 档(已知会失败:把上限调到很大,让它永不拒绝)",
        format!("{} 档", rejecting_total).as_str(),
        rejecting_total >= 1,
    ));

    lines
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : value_quantification.rs
//! 价值量化:**对象池省下的到底是什么**。
//!
//! ## ★ 省下的不是时间,是「造一个新对象」这件事本身
//!
//! 本工程里「造」等于**采购一台设备**,所以省下的能用**金额**算出来。
//! 这是刻意选这个业务域的原因:
//! 若选一个「造对象只要几微秒」的例子,
//! 省下的就只能用**耗时**表达,而耗时不可复现------
//! 那正是「我们用了对象池所以很快」这句话永远无法被反驳的根源。
//!
//! ## ★ 省下的台数 = 两个可数数字之差
//!
//! ```text
//! 不池化要买几台  = 峰值并发在借数(按需扩容档的新建次数)
//! 池化实际买了几台 = 严格池档的新建次数
//! 省下的台数      = 前者 − 后者
//! ```
//!
//! ⚠️ 不用「命中次数」当省下的台数:
//! 命中 6 次只说明复用发生了 6 次,
//! 而「本来要买 8 台、实际买了 3 台」里的 5 台才是真正的差额。
//! 这两个数在稳态下并不相等,取哪个必须先说清。

use crate::analysis::CheckLine;
use crate::domain::{ratio_basis_points, Money};

/// 对象池价值报告。
#[derive(Clone, Debug)]
pub struct PoolValueReport {
    /// 申请笔数。
    request_total: usize,
    /// 借到笔数。
    granted_total: usize,
    /// 没借到笔数。
    rejected_total: usize,
    /// ★ 池化档实际造了几台(**严格池**的新建次数)。
    created_with_pool_total: usize,
    /// ★ 不池化要造几台(**按需扩容**档的新建次数 = 峰值并发)。
    created_without_pool_total: usize,
    /// ★ 命中次数(复用发生过几次)。
    hit_total: usize,
    /// ★ 归还后回池次数(真正可供下次使用的那些次)。
    reused_total: usize,
    /// 借出峰值(**全部型号合计**)。
    peak_borrowed_total: usize,
    /// ★ 省下的采购台数(各型号差额之和)。
    saved_purchase_total: i64,
    /// ★ 省下的采购金额(分,各型号「差额 × 该型号采购单价」之和)。
    ///
    /// ⚠️ 为什么存金额而不是存「单价 × 台数」:
    /// 三个型号单价不同,用同一个单价乘总台数会得出一个
    /// 「看起来对」的数------而错的金额比错的台数难发现得多。
    saved_money_minor_units: i64,
    /// 服务成本(分)。
    service_cost_minor_units: i64,
}

impl PoolValueReport {
    /// 由三档对照的结果组装。
    ///
    /// ⚠️ 十一个参数不是设计缺陷,是**这份报告的口径清单**:
    /// 每个数都来自不同层的不同计数,缺一个就没法算省下的台数。
    /// 若改成传一个 `&PoolSession`,本层就要认识 `client` 的具体结构,
    /// 而「报告只认数字」正是它能被独立复算的前提。
    pub fn new(
        request_total: usize,
        granted_total: usize,
        rejected_total: usize,
        created_with_pool_total: usize,
        created_without_pool_total: usize,
        hit_total: usize,
        reused_total: usize,
        peak_borrowed_total: usize,
        saved_purchase_total: i64,
        saved_money_minor_units: i64,
        service_cost_minor_units: i64,
    ) -> Self {
        Self {
            request_total,
            granted_total,
            rejected_total,
            created_with_pool_total,
            created_without_pool_total,
            hit_total,
            reused_total,
            peak_borrowed_total,
            saved_purchase_total,
            saved_money_minor_units,
            service_cost_minor_units,
        }
    }

    /// 申请笔数。
    pub const fn request_total(&self) -> usize {
        self.request_total
    }

    /// 借到笔数。
    pub const fn granted_total(&self) -> usize {
        self.granted_total
    }

    /// 没借到笔数。
    pub const fn rejected_total(&self) -> usize {
        self.rejected_total
    }

    /// 池化档已造台数。
    pub const fn created_with_pool_total(&self) -> usize {
        self.created_with_pool_total
    }

    /// 不池化要造台数。
    pub const fn created_without_pool_total(&self) -> usize {
        self.created_without_pool_total
    }

    /// 命中次数。
    pub const fn hit_total(&self) -> usize {
        self.hit_total
    }

    /// 回池次数。
    pub const fn reused_total(&self) -> usize {
        self.reused_total
    }

    /// 借出峰值。
    pub const fn peak_borrowed_total(&self) -> usize {
        self.peak_borrowed_total
    }

    /// ★ 省下的采购台数(各型号差额之和)。
    pub const fn saved_purchase_total(&self) -> i64 {
        self.saved_purchase_total
    }

    /// ★ 省下的采购金额(分)。
    pub const fn saved_money_minor_units(&self) -> i64 {
        self.saved_money_minor_units
    }

    /// 服务成本(分)。
    pub const fn service_cost_minor_units(&self) -> i64 {
        self.service_cost_minor_units
    }

    /// ★ 省下的采购台数(**按定义应等于**「不池化 − 池化」,自检③核对这一点)。
    pub const fn expected_saved_purchase_total(&self) -> i64 {
        self.created_without_pool_total as i64 - self.created_with_pool_total as i64
    }

    /// ★ 省下的采购金额。
    pub fn saved_money(&self) -> Money {
        Money::new(self.saved_money_minor_units, crate::domain::CNY)
    }

    /// 省下的比例(万分点):省下台数 / 不池化要买台数。
    pub fn saved_ratio_basis_points(&self) -> i64 {
        ratio_basis_points(
            i128::from(self.saved_purchase_total()),
            self.created_without_pool_total as i128,
        )
    }

    /// 一句话结论(报表与体检共用,**只有一处**)。
    pub fn summary_text(&self) -> String {
        format!(
            "借出 {} 次,池化只买了 {} 台,不池化要买 {} 台,省下 {} 台({})",
            self.granted_total,
            self.created_with_pool_total,
            self.created_without_pool_total,
            self.saved_purchase_total(),
            self.saved_money().chinese_text()
        )
    }

    /// ★ 五条自检(**这份报告自己也要被报告**)。
    pub fn self_check(&self) -> Vec<CheckLine> {
        let mut lines: Vec<CheckLine> = Vec::new();

        // ① 借到 + 没借到 == 申请。
        lines.push(CheckLine::new(
            "借到 + 没借到 == 申请总数",
            "相等(已知会失败:新增一条静默跳过的分支)",
            format!("{} + {} == {}", self.granted_total, self.rejected_total, self.request_total)
                .as_str(),
            self.granted_total + self.rejected_total == self.request_total,
        ));

        // ② ★ 省下的台数 >= 0(池化不该买得更多)。
        lines.push(CheckLine::new(
            "★ 省下的采购台数 >= 0",
            ">= 0(已知会失败:把严格池的预热数设成 99)",
            format!("{} >= 0", self.saved_purchase_total()).as_str(),
            self.saved_purchase_total() >= 0,
        ));

        // ③ ★ 省下的台数 == 不池化 − 池化(口径自检)。
        lines.push(CheckLine::new(
            "★ 省下台数 == 不池化要买 − 池化已买",
            "相等(已知会失败:把省下台数改成用命中次数算)",
            format!(
                "{} == {} − {}",
                self.saved_purchase_total(),
                self.created_without_pool_total,
                self.created_with_pool_total
            )
            .as_str(),
            self.saved_purchase_total() == self.expected_saved_purchase_total(),
        ));

        // ④ ★ 池化已造 >= 借出峰值(峰值那刻够借)。
        lines.push(CheckLine::new(
            "★ 池化已造台数 >= 借出峰值",
            ">=(已知会失败:峰值事后从台账推)",
            format!(
                "{} >= {}",
                self.created_with_pool_total, self.peak_borrowed_total
            )
            .as_str(),
            self.created_with_pool_total >= self.peak_borrowed_total,
        ));

        // ⑤ ★ 命中次数 > 0(池确实复用上了)。
        lines.push(CheckLine::new(
            "★ 命中次数 > 0(池确实复用上了)",
            "> 0(已知会失败:预热 0 台且只借一次)",
            format!("{} > 0", self.hit_total).as_str(),
            self.hit_total > 0,
        ));

        lines
    }
}
rust 复制代码
//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : instrument_station.rs
//! 检测站:一个型号 = 一座站 = 一个对象池。
//!
//! ## ★ 为什么按型号分站,而不是「一个大池装所有仪器」
//!
//! 门店借的是**型号**(「一台天平」),不是「随便一台设备」。
//! 一个大池装所有仪器,就得在池里按型号过滤------
//! 那时「池空了」的含义变成「这个型号空了」,
//! 而 `idle.len() == 0` 这个读数开始撒谎(池里明明还有显微镜)。
//! 按型号分站,「池空」永远只有一个含义。
//!
//! ## ⚠️ 采购金额**不单独记账**
//!
//! 已花掉的采购钱 = `已造台数 × 型号采购单价`,由 [`InstrumentStation::acquisition_cost_spent`]
//! **现算**,不另设累加器。
//! 累加器的唯一作用是「可以少记一次」------
//! 而这一次少记,在月底对不上时没有任何痕迹可查。
//!
//! ## 服务成本必须逐条记
//!
//! 与采购相反:每次维修花掉的钱**不一样**,只能逐条记。
//! 这里正好演示了「什么时候用派生、什么时候用累加」:
//! 单价固定的用派生,单价不固定的用累加。

use std::cell::RefCell;
use std::rc::Rc;

use crate::domain::{
    InstrumentModel, InstrumentUnit, Money, MonthKey, ServiceKind, ServiceRecord, StoreCode,
    UsageRequest,
};
use crate::pool::{
    observe_pool, ObjectPool, PoolHandle, PoolObservation, ReleaseOutcome, ReusePolicy,
};

/// ★ 单次校准 / 维修的服务单价(分)。
///
/// 为什么是常量而不是型号的属性:维修报价由服务商给,
/// 与本设备的型号无关。放在型号上会让人误以为「贵设备修得贵」,
/// 而真实报价里两者并不相关。
pub const SERVICE_COST_CALIBRATION_MINOR_UNITS: i64 = 32_000;
/// 单次退役处置费用(分,残值处理费)。
pub const SERVICE_COST_RETIREMENT_MINOR_UNITS: i64 = 5_000;

/// 一座检测站。
pub struct InstrumentStation {
    /// 型号(一座站只服务一个型号)。
    model: InstrumentModel,
    /// 当前业务月份(归还校验的判据输入)。
    current_month: MonthKey,
    /// 对象池本体。
    pool: ObjectPool<InstrumentUnit>,
    /// 服务记录(**只增不减**)。
    service_records: Vec<ServiceRecord>,
    /// 服务成本累计(逐条累加,见模块文档)。
    service_cost_minor_units: i64,
    /// ★ 借出峰值(并发最高时在借几台)。
    ///
    /// 这是「不池化要买几台」的直接答案,因此必须**在借出时就更新**,
    /// 不能事后从台账推------台账里没有「同一时刻」这个概念。
    peak_borrowed_total: usize,
}

impl InstrumentStation {
    /// 建一座站。
    ///
    /// # 参数
    /// - `model`:型号。
    /// - `policy`:复用策略。
    /// - `manufacture_month`:出厂月份(决定首批仪器的校准到期月)。
    /// - `current_month`:当前业务月份。
    /// - `prewarm_count`:预热台数(开业前先备几台)。
    pub fn new(
        model: InstrumentModel,
        policy: ReusePolicy,
        manufacture_month: MonthKey,
        current_month: MonthKey,
        prewarm_count: usize,
    ) -> Self {
        let mut pool: ObjectPool<InstrumentUnit> = ObjectPool::new(
            format!("{}池", model.chinese_name()).as_str(),
            policy,
            crate::client::instrument_factory(model, manufacture_month),
            crate::client::return_verifier(current_month),
        );
        pool.prewarm(prewarm_count);
        Self {
            model,
            current_month,
            pool,
            service_records: Vec::new(),
            service_cost_minor_units: 0,
            peak_borrowed_total: 0,
        }
    }

    /// 型号。
    pub const fn model(&self) -> InstrumentModel {
        self.model
    }

    /// 当前业务月份。
    pub const fn current_month(&self) -> MonthKey {
        self.current_month
    }

    /// 池本体(**只读**)。
    pub fn pool(&self) -> &ObjectPool<InstrumentUnit> {
        &self.pool
    }

    /// 服务记录。
    pub fn service_records(&self) -> &[ServiceRecord] {
        self.service_records.as_slice()
    }

    /// 借出峰值。
    pub const fn peak_borrowed_total(&self) -> usize {
        self.peak_borrowed_total
    }

    // ---------------------------------------------------------------------
    // 业务动作
    // ---------------------------------------------------------------------

    /// ★ 借出:按申请借一台给门店。
    ///
    /// 返回 `None` 表示**没借到**(池空且策略不允许新建)。
    ///
    /// ⚠️ 借到之后立刻记下持有人:这一步**只能**在这里做,
    /// 因为只有业务层知道是谁借的(机制层的 `acquire()` 没有门店这个概念)。
    pub fn borrow_for(&mut self, request: &UsageRequest) -> Option<PoolHandle<InstrumentUnit>> {
        assert!(
            request.model().code() == self.model.code(),
            "申请型号 {} 与本站型号 {} 不一致",
            request.model().code(),
            self.model.code()
        );
        let handle: PoolHandle<InstrumentUnit> = self.pool.acquire()?;
        handle.object().borrow_mut().set_holder(request.store());
        let borrowed_now: usize = self.pool.borrowed_total();
        if borrowed_now > self.peak_borrowed_total {
            self.peak_borrowed_total = borrowed_now;
        }
        Some(handle)
    }

    /// ★ 归还:凭句柄归还,结局**可见**。
    pub fn return_handle(&mut self, handle: PoolHandle<InstrumentUnit>) -> ReleaseOutcome {
        self.pool.release(handle)
    }

    /// ★ 取出隔离区里的对象(业务层拿去送修或退役)。
    ///
    /// 转发机制层的同名方法。拿到之后**必须**二选一:
    /// [`InstrumentStation::admit_repaired`](修好回池)或 [`InstrumentStation::retire`]。
    /// 取走之后既不送回也不退役,容量守恒就会少一台------
    /// 而本工程在幕十(续)里**真的这么做过一次**,
    /// 为的是确认 `CAPACITY` 那条规则确实会红。
    pub fn take_quarantined(&mut self) -> Vec<Rc<RefCell<InstrumentUnit>>> {
        self.pool.take_quarantined()
    }

    /// 把修好的仪器重新放进池里。
    pub fn admit_repaired(&mut self, object: Rc<RefCell<InstrumentUnit>>) {
        self.pool.admit_repaired(object);
    }

    /// 退役一台仪器(永久离池)。
    pub fn retire(&mut self, object: Rc<RefCell<InstrumentUnit>>) {
        self.pool.retire(object);
    }

    /// ★ 处理隔离区:能修的送去校准再回池,到退役上限的直接退役。
    ///
    /// ## 为什么这一步必须显式调用
    ///
    /// 隔离区是「池已经不认它、业务还没处理它」的中间态。
    /// 若池自己在归还时就决定送修还是退役,
    /// 「退役」这个**花钱的结论**就没有任何业务方参与------
    /// 而退役恰恰是最需要留痕的一件事(资产处置)。
    pub fn service_quarantined(&mut self) -> Vec<ServiceRecord> {
        let pending: Vec<Rc<RefCell<InstrumentUnit>>> = self.pool.take_quarantined();
        let mut produced: Vec<ServiceRecord> = Vec::new();
        for object in pending.into_iter() {
            let serial_number: String = object.borrow().identity_text();
            let reason: String = match object.borrow().rejection_reason_text(self.current_month) {
                Some(text) => text.to_string(),
                None => "归还未通过校验".to_string(),
            };
            if crate::client::should_retire(&object.borrow()) {
                object.borrow_mut().mark_retired();
                let record: ServiceRecord = ServiceRecord::new(
                    serial_number.as_str(),
                    self.current_month,
                    ServiceKind::Retirement,
                    reason.as_str(),
                    SERVICE_COST_RETIREMENT_MINOR_UNITS,
                );
                self.service_cost_minor_units += SERVICE_COST_RETIREMENT_MINOR_UNITS;
                self.pool.retire(Rc::clone(&object));
                produced.push(record);
            } else {
                // 校准完成:按型号周期顺延到期月,然后回池。
                object.borrow_mut().mark_calibration_done(self.current_month);
                let record: ServiceRecord = ServiceRecord::new(
                    serial_number.as_str(),
                    self.current_month,
                    ServiceKind::Calibration,
                    reason.as_str(),
                    SERVICE_COST_CALIBRATION_MINOR_UNITS,
                );
                self.service_cost_minor_units += SERVICE_COST_CALIBRATION_MINOR_UNITS;
                self.pool.admit_repaired(Rc::clone(&object));
                produced.push(record);
            }
        }
        for record in produced.iter() {
            self.service_records.push(record.clone());
        }
        produced
    }

    // ---------------------------------------------------------------------
    // 读数
    // ---------------------------------------------------------------------

    /// ★ 已花掉的采购金额 = 已造台数 × 型号采购单价(**现算**,不累加)。
    pub fn acquisition_cost_spent(&self) -> Money {
        self.model
            .acquisition_cost()
            .scale_ratio(self.pool.created_total() as i128, 1)
    }

    /// 服务成本累计(逐条累加)。
    pub fn service_cost_spent(&self) -> Money {
        Money::new(self.service_cost_minor_units, self.model.currency())
    }

    /// 服务次数。
    pub fn service_total(&self) -> usize {
        self.service_records.len()
    }

    /// 观测(一次取齐,供审计与报表用)。
    pub fn observation(&self) -> PoolObservation {
        observe_pool(&self.pool)
    }

    /// 站名。
    pub fn name(&self) -> String {
        format!("{}({})", self.pool.name(), self.model.chinese_name())
    }

    /// ★ 当前持有人列表(**按序列号排序**后返回)。
    ///
    /// ⚠️ 排序键只能是序列号:`StoreCode` 刻意**没有**实现 `Ord`
    /// (本工程从不对门店排序,只按声明顺序输出)。
    /// 若按持有人排序,就得给门店加上 `Ord`------
    /// 那是为一个报表需求去放宽领域层的类型约束。
    pub fn holder_list_sorted(&self) -> Vec<(String, Option<StoreCode>)> {
        let mut pairs: Vec<(String, Option<StoreCode>)> = self
            .pool
            .objects_sorted()
            .into_iter()
            .map(|(identity, object)| (identity, object.borrow().holder()))
            .collect();
        pairs.sort_by(|left, right| left.0.cmp(&right.0));
        pairs
    }
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : pool_session.rs
//! 会话:把一份场景在一座站上**跑一遍**。
//!
//! ## ★ 会话是「重放」,不是「模拟」
//!
//! 同一份 `UsageScenario` 在三档策略上各跑一次,
//! 得到三份 `PoolSession`。三者唯一的差异来源是策略,
//! 因此「新建次数之差」才能被解释为「策略造成的」。
//! 若每档各自造数据,这个解释就不成立。
//!
//! ## ★ 归还后**立刻**处理隔离区
//!
//! 归还校验没过的仪器会被放进隔离区。若等到最后统一处理,
//! 后续申请就会因为「隔离区占着容量」而借不到------
//! 那时报表上的「拒绝次数」里混着一件本可以避免的事,
//! 读者无法分辨「真借不到」与「没人去处理隔离区」。
//! 因此在循环里立刻处理,让拒绝次数只反映策略。

use std::cell::RefCell;
use std::rc::Rc;

use crate::client::InstrumentStation;
use crate::domain::{InstrumentModel, InstrumentUnit, ServiceRecord, UsageRequest};
use crate::pool::{ReleaseOutcome, ReusePolicy};

/// 一次借出的记录。
#[derive(Clone, Debug)]
pub struct BorrowRecord {
    /// 申请。
    request: UsageRequest,
    /// 是否借到。
    granted: bool,
    /// 借到的仪器序列号(没借到为 `"---`")。
    identity: String,
    /// 归还结局(没借到或未归还为 `None`,**会被回填**)。
    return_outcome: Option<ReleaseOutcome>,
    /// ★ 借到时该仪器的累计使用次数(**借出之后**的数)。
    usage_count_at_acquisition: usize,
}

impl BorrowRecord {
    /// 申请。
    pub const fn request(&self) -> UsageRequest {
        self.request
    }

    /// 是否借到。
    pub const fn granted(&self) -> bool {
        self.granted
    }

    /// 借到的序列号。
    pub fn identity(&self) -> &str {
        self.identity.as_str()
    }

    /// 归还结局。
    pub fn return_outcome(&self) -> Option<&ReleaseOutcome> {
        self.return_outcome.as_ref()
    }

    /// 借到时的累计使用次数。
    pub const fn usage_count_at_acquisition(&self) -> usize {
        self.usage_count_at_acquisition
    }

    /// ★ 这一笔是否**省下一次采购**(借到了 + 是复用而非新建 + 归还后回池)。
    ///
    /// ⚠️ 三个条件缺一不可:
    /// 借到了但走的是新建,没省;新建了但归还时送修,这台等于白买一次。
    pub fn saved_one_purchase(&self) -> bool {
        self.granted
            && self.usage_count_at_acquisition > 1
            && matches!(self.return_outcome, Some(ReleaseOutcome::ReturnedToPool { .. }))
    }

    /// 归还结局的中文文本(没借到或未归还写 `---`)。
    pub fn outcome_text(&self) -> String {
        match self.return_outcome.as_ref() {
            Some(outcome) => outcome.chinese_text().to_string(),
            None => "---".to_string(),
        }
    }
}

/// ★ 身份证据:**池里的那台,就是当初借出去的那台**。
///
/// ## 为什么这条是对象池的全部价值所在
///
/// 若池在归还时「新建一个一模一样的」放回去,
/// 台账上的复用次数一样会 +1,钱一样算省下来------
/// 但那是**假复用**:真正的省来自「同一台设备被第二次使用」。
/// 用 `Rc::ptr_eq` 比对,是让「同一台」这件事
/// 变成一个**能算出来的是/否**,而不是一句主张。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct IdentityEvidence {
    /// 借出成功次数。
    pub acquisition_total: usize,
    /// ★ 实际用到的**不同**对象数(按指针去重)。
    pub distinct_object_total: usize,
    /// ★ 其中此刻仍在池里(能被指针比中)的个数。
    pub still_in_pool_total: usize,
    /// 累计造过的对象数。
    pub created_total: usize,
}

impl IdentityEvidence {
    /// ★ 复用是否成立:`借出次数 > 用到的不同对象数`。
    ///
    /// 等式中若两边相等,说明每次借出都用了一台新的------
    /// 那不是池,那是工厂。
    pub fn reuse_happened(&self) -> bool {
        self.acquisition_total > self.distinct_object_total
    }

    /// 平均每台被用了几次(万分点,便于报表显示小数)。
    pub fn usage_per_object_basis_points(&self) -> i64 {
        if self.distinct_object_total == 0 {
            return 0;
        }
        ((self.acquisition_total as i128) * 10_000 / (self.distinct_object_total as i128)) as i64
    }
}

/// 一次会话:一份场景 × 一座站 × 一种策略。
pub struct PoolSession {
    /// 站(**会话结束后仍可读**,用于报表)。
    station: InstrumentStation,
    /// 逐笔借出记录。
    borrow_records: Vec<BorrowRecord>,
    /// 服务记录(会话期间产生的)。
    service_records: Vec<ServiceRecord>,
    /// ★ 借出过的对象(**按指针保存**),用于身份证据。
    acquired_objects: Vec<Rc<RefCell<InstrumentUnit>>>,
}

impl PoolSession {
    /// ★ 跑一遍场景。
    ///
    /// # 参数
    /// - `model`:本站服务的型号。
    /// - `policy`:复用策略。
    /// - `month`:业务当月。
    /// - `manufacture_month`:出厂月份。
    /// - `steps`:步骤序列(**只用本型号的那些**)。
    /// - `prewarm_count`:预热台数。
    ///
    /// ## 每步的顺序:先归还到期的,再借新的
    ///
    /// 反过来(先借后还)会让「同一步借了就还」的申请
    /// 把峰值凭空抬高 1------
    /// 那时峰值不再表示「同一时刻最多几台在外面」,
    /// 而「省下几台」正是靠峰值算的。
    pub fn run(
        model: InstrumentModel,
        policy: ReusePolicy,
        month: crate::domain::MonthKey,
        manufacture_month: crate::domain::MonthKey,
        steps: &[crate::client::UsageStep],
        prewarm_count: usize,
    ) -> Self {
        let mut station: InstrumentStation =
            InstrumentStation::new(model, policy, manufacture_month, month, prewarm_count);
        let mut borrow_records: Vec<BorrowRecord> = Vec::new();
        let mut service_records: Vec<ServiceRecord> = Vec::new();
        let mut acquired_objects: Vec<Rc<RefCell<InstrumentUnit>>> = Vec::new();
        // ★ 在借中的凭证:`(到期步号, 凭证, 该笔在 borrow_records 里的下标)`。
        let mut held: Vec<(usize, crate::pool::PoolHandle<InstrumentUnit>, usize)> = Vec::new();

        for (step_index, step) in steps.iter().enumerate() {
            if step.request().model().code() != model.code() {
                continue;
            }
            // ① 先归还到期的(**倒序移除**,避免下标错位)。
            let mut due_positions: Vec<usize> = Vec::new();
            for (position, (due_step, _, _)) in held.iter().enumerate() {
                if *due_step <= step_index {
                    due_positions.push(position);
                }
            }
            for position in due_positions.iter().rev() {
                let (_, handle, record_index): (usize, crate::pool::PoolHandle<InstrumentUnit>, usize) =
                    held.remove(*position);
                let outcome: ReleaseOutcome = station.return_handle(handle);
                // ★ 归还后立刻处理隔离区(理由见模块文档)。
                service_records.extend(station.service_quarantined());
                borrow_records[record_index].return_outcome = Some(outcome);
            }
            // ② 再借出本步的申请。
            match station.borrow_for(&step.request()) {
                None => {
                    borrow_records.push(BorrowRecord {
                        request: step.request(),
                        granted: false,
                        identity: "---".to_string(),
                        return_outcome: None,
                        usage_count_at_acquisition: 0,
                    });
                }
                Some(handle) => {
                    let identity: String = handle.identity_at_acquisition().to_string();
                    let usage_count: usize = handle.object().borrow().usage_count();
                    acquired_objects.push(handle.object());
                    let record_index: usize = borrow_records.len();
                    borrow_records.push(BorrowRecord {
                        request: step.request(),
                        granted: true,
                        identity,
                        return_outcome: None,
                        usage_count_at_acquisition: usage_count,
                    });
                    held.push((step_index + step.hold_steps(), handle, record_index));
                }
            }
        }

        // ★ 收尾:还没还的全部归还(**按借出顺序**,保证两次运行一致)。
        for (_, handle, record_index) in held.into_iter() {
            let outcome: ReleaseOutcome = station.return_handle(handle);
            service_records.extend(station.service_quarantined());
            borrow_records[record_index].return_outcome = Some(outcome);
        }

        Self { station, borrow_records, service_records, acquired_objects }
    }

    /// 站。
    pub fn station(&self) -> &InstrumentStation {
        &self.station
    }

    /// 逐笔借出记录。
    pub fn borrow_records(&self) -> &[BorrowRecord] {
        self.borrow_records.as_slice()
    }

    /// 服务记录。
    pub fn service_records(&self) -> &[ServiceRecord] {
        self.service_records.as_slice()
    }

    /// 借到笔数。
    pub fn granted_total(&self) -> usize {
        self.borrow_records.iter().filter(|record| record.granted).count()
    }

    /// 没借到笔数。
    pub fn rejected_total(&self) -> usize {
        self.borrow_records.iter().filter(|record| !record.granted).count()
    }

    /// 归还后回池笔数。
    pub fn returned_to_pool_total(&self) -> usize {
        self.borrow_records
            .iter()
            .filter(|record| {
                matches!(record.return_outcome(), Some(ReleaseOutcome::ReturnedToPool { .. }))
            })
            .count()
    }

    /// 归还后被隔离笔数。
    pub fn quarantined_total(&self) -> usize {
        self.borrow_records
            .iter()
            .filter(|record| {
                matches!(record.return_outcome(), Some(ReleaseOutcome::Quarantined { .. }))
            })
            .count()
    }

    /// ★ 身份证据:借出去的那些对象,此刻还在不在池里。
    ///
    /// 用 `Rc::ptr_eq` 逐个比对------**不打印地址**(地址随运行变,
    /// 打印会破坏「两次运行逐字节一致」)。
    pub fn identity_evidence(&self) -> IdentityEvidence {
        let pool_objects: Vec<(String, Rc<RefCell<InstrumentUnit>>)> =
            self.station.pool().objects_sorted();
        let mut distinct: Vec<Rc<RefCell<InstrumentUnit>>> = Vec::new();
        for candidate in self.acquired_objects.iter() {
            if !distinct.iter().any(|existing| Rc::ptr_eq(existing, candidate)) {
                distinct.push(Rc::clone(candidate));
            }
        }
        let still_in_pool_total: usize = distinct
            .iter()
            .filter(|object| {
                pool_objects
                    .iter()
                    .any(|(_, pooled)| Rc::ptr_eq(pooled, object))
            })
            .count();
        IdentityEvidence {
            acquisition_total: self.granted_total(),
            distinct_object_total: distinct.len(),
            still_in_pool_total,
            created_total: self.station.pool().created_total(),
        }
    }

    /// ★ 池内仪器清单(**排序后**),供报表逐台展示。
    pub fn inventory(&self) -> Vec<(String, Rc<RefCell<InstrumentUnit>>)> {
        self.station.pool().objects_sorted()
    }
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : pooled_instrument.rs
//! 接线处:**把 `InstrumentUnit` 接进 `ObjectPool`**。
//!
//! ## ★ 为什么这个 impl 在 `client` 而不是 `domain`
//!
//! `PooledObject` 是机制层的 trait。若由 `domain` 自己 impl,
//! 领域层就必须 `use crate::pool`------
//! 那时「这台仪器被池化」会变成**领域对象的固有属性**,
//! 换一种复用策略(比如改成「每次新建」)要改领域层文件。
//!
//! 放在 `client`,`domain` 对「什么是池」一无所知,
//! 「要不要池化」就退回成一个**装配决定**。
//!
//! ## ⚠️ trait 方法名与领域方法名刻意不同名
//!
//! 领域侧叫 `clear_for_reuse`,trait 侧叫 `reset_for_reuse`。
//! 若同名,impl 里写 `self.reset_for_reuse()` 会被解析成 trait 方法------
//! 一次**静默无限递归**(栈溢出前没有任何编译期提示)。

use crate::domain::{
    InstrumentModel, InstrumentUnit, MonthKey, RETIREMENT_USAGE_LIMIT,
};
use crate::pool::{ObjectFactory, PooledObject, ReturnVerifier};

impl PooledObject for InstrumentUnit {
    fn identity_text(&self) -> String {
        InstrumentUnit::identity_text(self)
    }

    fn fingerprint_text(&self) -> String {
        InstrumentUnit::fingerprint_text(self)
    }

    /// ★ 被借出时:状态转 `InUse`、累计次数 +1。
    ///
    /// 这一步由**池**调用,不是由借用方调用------
    /// 借用方只负责说明「谁借的」(见 `set_holder`)。
    fn on_acquire(&mut self) {
        self.note_borrowed();
    }

    fn reset_for_reuse(&mut self) {
        self.clear_for_reuse();
    }

    fn on_quarantine(&mut self) {
        self.mark_in_calibration();
    }
}

/// ★ 仪器工厂:池要「造」一台新仪器时用它。
///
/// # 参数
/// - `model`:型号。
/// - `manufacture_month`:出厂(= 首次校准)月份,到期月由它加型号周期得出。
///
/// ## 序列号只依赖装配顺序
///
/// 序列号是 `{型号}-{序号:04}`,序号由**池**传进来(池造的第几个)。
/// 若工厂自己维护一个全局计数器,两次运行的编号就会依赖运行顺序;
/// 传序号进来则编号只依赖装配顺序------这是「两次运行逐字节一致」的前提之一。
pub fn instrument_factory(
    model: InstrumentModel,
    manufacture_month: MonthKey,
) -> ObjectFactory<InstrumentUnit> {
    Box::new(move |sequence: usize| {
        let serial_number: String = format!("{}-{:04}", model.code(), sequence + 1);
        let due_month: MonthKey = manufacture_month.plus_months(model.calibration_interval_months());
        InstrumentUnit::new(serial_number.as_str(), model, due_month)
    })
}

/// ★ 归还校验器:这台仪器还回来能不能**直接回池**。
///
/// # 参数
/// - `month`:归还当月(**判据的关键输入**,机制层不知道它,因此在这里注入)。
///
/// ## 为什么判据在这里而不是机制层
///
/// 「校准过期了要送检」「用够 12 次要退役」都是**业务知识**,
/// 而且将来会变(改成 15 次、改成按小时计)。
/// 写在机制层就等于让机制层按月改一次。
pub fn return_verifier(month: MonthKey) -> ReturnVerifier<InstrumentUnit> {
    Box::new(move |unit: &InstrumentUnit| match unit.rejection_reason_text(month) {
        None => Ok(()),
        Some(reason) => Err(format!("{}({})", reason, month.chinese_text())),
    })
}

/// ★ 退役判据:隔离区里的这台还值不值得修。
///
/// 与「能不能回池」是**两个不同的判断**:
/// 校准过期是能修的,用到退役上限是不值得修的。
/// 合并成一个布尔量,报表就只能写「送修」,
/// 而读者真正想知道的是「这台是不是要报废了」。
pub fn should_retire(unit: &InstrumentUnit) -> bool {
    unit.usage_count() >= RETIREMENT_USAGE_LIMIT
}


//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:对象池简写模式  pool 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/11 7:56 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : objectpoolpattern
//!# File      : usage_scenario.rs
//! 借用场景:一整组**按时间顺序**的借用申请。
//!
//! ## ★ 场景是「输入」,不是「过程」
//!
//! 场景里只有申请,**没有**「借没借到」「借到哪一台」。
//! 那些是**结果**,由池按策略产生。
//! 若把结果写进场景,同一份场景就只能跑出一种结局,
//! 「三档策略对照」这件本工程最重要的事就无从谈起。
//!
//! ## ⚠️ 为什么申请序列要能被**重放**
//!
//! 三档策略要跑的是**同一份**申请序列。
//! 若每档各自造一份,两条路径的差异就无法区分
//! 「是策略造成的」还是「是数据造成的」------
//! 那正是「对照实验」最容易失效的地方。

use crate::domain::{InstrumentModel, MonthKey, UsageRequest};

/// 一个借用步骤:申请 + **持有几步**。
///
/// ## ★ 为什么要「持有几步」
///
/// 若每笔申请都是「借了立刻还」,同一时刻在外面永远只有一台,
/// 峰值恒为 1------那时**池与不池化买的数量完全一样**,
/// 「省下几台」必然是 0,整个对照实验是恒真的废表。
///
/// 「持有」让**并发**成为数据的一部分:
/// 峰值不再由代码决定,而由这份申请序列决定。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct UsageStep {
    /// 借用申请。
    request: UsageRequest,
    /// ★ 持有几步之后归还(`0` 表示下一步就还)。
    hold_steps: usize,
}

impl UsageStep {
    /// 声明一个步骤。
    pub const fn new(request: UsageRequest, hold_steps: usize) -> Self {
        Self { request, hold_steps }
    }

    /// 申请。
    pub const fn request(&self) -> UsageRequest {
        self.request
    }

    /// 持有几步。
    pub const fn hold_steps(&self) -> usize {
        self.hold_steps
    }
}

/// 一整组借用申请。
#[derive(Clone, Debug)]
pub struct UsageScenario {
    /// 场景名(报表用,便于分辨内置场景与扩展场景)。
    name: String,
    /// 业务当月(**归还校验的判据输入**)。
    month: MonthKey,
    /// 出厂月份(首批仪器的校准起点)。
    manufacture_month: MonthKey,
    /// 步骤序列(**按时间顺序**,下标即顺序)。
    steps: Vec<UsageStep>,
}

impl UsageScenario {
    /// 建一个场景。
    pub fn new(
        name: &str,
        month: MonthKey,
        manufacture_month: MonthKey,
        steps: Vec<UsageStep>,
    ) -> Self {
        Self { name: name.to_string(), month, manufacture_month, steps }
    }

    /// 场景名。
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// 业务当月。
    pub const fn month(&self) -> MonthKey {
        self.month
    }

    /// 出厂月份。
    pub const fn manufacture_month(&self) -> MonthKey {
        self.manufacture_month
    }

    /// 步骤序列。
    pub fn steps(&self) -> &[UsageStep] {
        self.steps.as_slice()
    }

    /// 申请条数(= 步骤条数)。
    pub fn request_total(&self) -> usize {
        self.steps.len()
    }

    /// ★ 场景涉及的型号(**保持声明顺序、线性去重**,不用哈希)。
    ///
    /// 用途是「建几座站」。顺序必须是声明顺序:
    /// 哈希集合的迭代顺序会打乱站的建造顺序,
    /// 于是「两次运行逐字节一致」又多了一个不确定来源。
    pub fn models(&self) -> Vec<InstrumentModel> {
        let mut models: Vec<InstrumentModel> = Vec::new();
        for step in self.steps.iter() {
            let model: InstrumentModel = step.request().model();
            if !models.iter().any(|existing| existing.code() == model.code()) {
                models.push(model);
            }
        }
        models
    }

    /// 指定型号的申请条数。
    pub fn request_total_for(&self, model: &InstrumentModel) -> usize {
        self.steps
            .iter()
            .filter(|step| step.request().model().code() == model.code())
            .count()
    }
}
相关推荐
Rain的Java大神之路1 小时前
Java 工程师做 AI,核心机会在「落地」而非「训练」
java·后端·ai编程
高晶1 小时前
ARM 嵌入式 Linux 开发:从体系结构到 LED 点亮实战
设计模式·程序员
柠檬味拥抱1 小时前
基于 YOLO11 的六类生活垃圾视觉分类系统 | 用于社区投放点智能督导、分拣线品类分流、投错率统计分析 | 附完整源码与效果演示
后端
程序员小新7001 小时前
RAG 实战与踩坑:交通法规智能问答助手
人工智能·后端
jimy11 小时前
标准的动态类型定义与“多态”无关,非多态对象同样会出现动态类型不同于静态类型的情形
开发语言·c++
冬、不眠的夜1 小时前
Nuitka 将 python 程序编译成 exe 指南
开发语言·python
CVer儿1 小时前
起源1引擎相关知识点2--World Brush+func_detail
开发语言
EAIReport1 小时前
SpringBoot 与 SpringCloud 深度解析:核心特点、差异与协同关系
spring boot·后端·spring cloud
梦里寻码2 小时前
从 0 到 1 用 NestJS + LangChain 搭一套可售卖的 SaaS AI 客服:挂件一行代码接入,知识库秒变 SEO 帮助中心
前端·后端·github