项目结构:

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()
}
}