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_report.rs
//! 幕十:契约审计(**六条不变量**)。
//!
//! ## ★ 每条规则的「已知会失败的输入」都印在注解里
//!
//! 一条不可能失败的规则不是规则,它只是没被跑过的注释。
//! 因此本表把每条规则的 `known_failing_input_text()` 逐条印出------
//! 读者能看见「这条规则在什么数据上会红」,
//! 才可能相信它现在是绿的不是因为没在跑。
//!
//! ## 六条规则分两类
//!
//! - 前五条描述「池**没被用坏**」(守恒、恒等、无异物、无泄漏);
//! - 第六条 `EFFECTIVE` 描述「池**有没有起作用**」(命中 > 0)。
//!
//! ⚠️ 少了第六条,演示会得出「我们用了对象池(且没用坏)」
//! 这种**不可反驳**的结论。把「命中 > 0」也变成一条会红的规则,
//! 「其实一次都没复用上」这件事才有地方暴露。
use crate::analysis::AuditCheckView;
use crate::app::{Table, TableColumn, DOCUMENT_WIDTH};
/// 审计表的列宽。
const RULE_CODE_WIDTH: usize = 10;
/// 规则列宽(最长「异物归还:归还了不属于本池的对象必须为 0」= 40 列)。
const RULE_NAME_WIDTH: usize = 42;
const EXPECTATION_WIDTH: usize = 8;
/// 实测列宽(**可省略**:恒等式展开后可到 50 列以上)。
const ACTUAL_WIDTH: usize = 38;
const VERDICT_WIDTH: usize = 8;
const VIOLATION_WIDTH: usize = 12;
/// 幕十:契约审计。
pub fn render_audit_table(title: &str, checks: &[AuditCheckView]) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::new("规则编码", RULE_CODE_WIDTH),
TableColumn::new("规则", RULE_NAME_WIDTH),
TableColumn::new("期望", EXPECTATION_WIDTH),
TableColumn::elidable(TableColumn::new("实测", ACTUAL_WIDTH)),
TableColumn::new("结论", VERDICT_WIDTH),
TableColumn::elidable(TableColumn::new("违规", VIOLATION_WIDTH)),
],
);
for check in checks.iter() {
table = table.with_row(vec![
check.rule_code().to_string(),
check.rule_name().to_string(),
check.expectation_text().to_string(),
check.actual_text().to_string(),
if check.passed() { "通过".to_string() } else { "未通过".to_string() },
check.violation_text().to_string(),
]);
}
for check in checks.iter() {
table = table.with_note(&format!(
"{}:已知会失败的输入 ------ {}",
check.rule_code(),
check.known_failing_input_text()
));
}
table = table.with_note(
"★ 「期望」列是 `符合数/适用数`。判通过时**同时要求适用数 > 0**:\
一条规则若在本次数据上根本没有适用对象,`0 == 0` 会让它显示成「通过」------\
那是「没跑过」冒充「跑过了」。",
);
table.render(DOCUMENT_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 : capacity_report.rs
//! 幕八:容量盘点(**每一座站一台表都不落**)。
//!
//! ## ★ 容量守恒是月底唯一能对账的东西
//!
//! 空闲 + 在借 + 隔离 == 已造 − 已退役。
//! 这条等式一旦失真,报表上所有数字都还能看、还自洽,
//! 唯独「我们一共有几台设备」没人能回答。
//! 因此本表把**四个区域与差**逐列列出,让守恒变成一个看得见的格子。
//!
//! ## ⚠️ 退役不计入容量
//!
//! 退役的仪器已经不是池的一部分。把它们算进容量,
//! 「限量扩容」的上限会被一批再也回不来的对象占住,
//! 于是池会永远拒绝借出,而报表上的「已造 8 台」看起来完全正常。
use crate::analysis::CapacityProbe;
use crate::app::{Table, TableColumn, DOCUMENT_WIDTH};
/// 容量表的列宽。
const STATION_WIDTH: usize = 28;
const COUNT_WIDTH: usize = 8;
/// 一行容量(**比 `CapacityProbe` 多一个站名**,由 `main` 填)。
#[derive(Clone, Debug)]
pub struct CapacityRowView {
/// 站名。
station_name: String,
/// 探针。
probe: CapacityProbe,
}
impl CapacityRowView {
/// 由站名与探针组装。
pub fn new(station_name: &str, probe: CapacityProbe) -> Self {
Self { station_name: station_name.to_string(), probe }
}
/// 站名。
pub fn station_name(&self) -> &str {
self.station_name.as_str()
}
/// 容量守恒的差。
pub fn capacity_gap(&self) -> i64 {
self.probe.capacity_gap()
}
}
/// 幕八:容量盘点。
pub fn render_capacity_table(title: &str, rows: &[CapacityRowView]) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::new("站", STATION_WIDTH),
TableColumn::numeric("空闲", COUNT_WIDTH),
TableColumn::numeric("在借", COUNT_WIDTH),
TableColumn::numeric("隔离", COUNT_WIDTH),
TableColumn::numeric("退役", COUNT_WIDTH),
TableColumn::numeric("已造", COUNT_WIDTH),
TableColumn::numeric("容量", COUNT_WIDTH),
TableColumn::numeric("差", COUNT_WIDTH),
TableColumn::numeric("峰值", COUNT_WIDTH),
],
);
for row in rows.iter() {
table = table.with_row(vec![
row.station_name.clone(),
row.probe.idle_total().to_string(),
row.probe.borrowed_total().to_string(),
row.probe.quarantined_total().to_string(),
row.probe.retired_total().to_string(),
row.probe.created_total().to_string(),
row.probe.capacity_total().to_string(),
row.capacity_gap().to_string(),
row.probe.peak_borrowed_total().to_string(),
]);
}
table = table.with_note(
"★ 「容量」= 空闲 + 在借 + 隔离(**不含退役**),「差」= 容量 − (已造 − 已退役)。\
差必须为 0:非 0 意味着有对象既不在任何区域、也没被记为退役------那就是凭空消失了一台。",
);
table = table.with_note(
"★ 「隔离」必须为 0:归还校验没过的仪器会被放进隔离区,\
业务层必须**立刻**处理(送校准或退役)。若等到最后统一处理,\
后续申请会因「隔离区占着容量」而借不到,\
那时报表上的「拒绝次数」里混着一件本可以避免的事。",
);
table.render(DOCUMENT_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 : extension_report.rs
//! 幕十二:工程外扩展对照。
//!
//! ## ★ 这一幕是「可扩展性必须被实证」的落点
//!
//! 工程外扩展区新增了一个型号、一家门店、一批申请,
//! **没有改动任何既有分层文件**(`src/` 下零改动),
//! 而新增内容自动进了目录表、容量表、对照表与统计。
//!
//! ## ⚠️ 「改动文件数 = 0」必须被**数出来**,不能靠声明
//!
//! 「我们没改既有文件」是一句主张;
//! 用脚本比对「扩展前的文件哈希 vs 扩展后的文件哈希」才是证据。
//! 因此本表的「改动文件数」一栏由 `main` 用**实际比对结果**填,
//! 不是写死的 0。
use crate::app::{Table, TableColumn, DOCUMENT_WIDTH};
/// 扩展对照表的列宽。
const ITEM_WIDTH: usize = 28;
const BUILTIN_WIDTH: usize = 22;
const EXTENDED_WIDTH: usize = 22;
const EVIDENCE_WIDTH: usize = 48;
/// 一行扩展对照。
#[derive(Clone, Debug)]
pub struct ExtensionRowView {
/// 项目名。
item: String,
/// 内置基线的值。
builtin_value: String,
/// 扩展后的值。
extended_value: String,
/// ★ 证据(**在哪张表多一行**,而不是一句「已支持」)。
evidence: String,
}
impl ExtensionRowView {
/// 由四项内容组装。
pub fn new(item: &str, builtin_value: &str, extended_value: &str, evidence: &str) -> Self {
Self {
item: item.to_string(),
builtin_value: builtin_value.to_string(),
extended_value: extended_value.to_string(),
evidence: evidence.to_string(),
}
}
/// 项目名。
pub fn item(&self) -> &str {
self.item.as_str()
}
/// 内置基线值。
pub fn builtin_value(&self) -> &str {
self.builtin_value.as_str()
}
/// 扩展后的值。
pub fn extended_value(&self) -> &str {
self.extended_value.as_str()
}
/// 证据文本。
pub fn evidence(&self) -> &str {
self.evidence.as_str()
}
}
/// 幕十二:扩展对照。
pub fn render_extension_table(title: &str, rows: &[ExtensionRowView]) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::new("项目", ITEM_WIDTH),
TableColumn::numeric("内置基线", BUILTIN_WIDTH),
TableColumn::numeric("工程外扩展后", EXTENDED_WIDTH),
TableColumn::elidable(TableColumn::new("证据(在哪张表多一行)", EVIDENCE_WIDTH)),
],
);
for row in rows.iter() {
table = table.with_row(vec![
row.item().to_string(),
row.builtin_value().to_string(),
row.extended_value().to_string(),
row.evidence().to_string(),
]);
}
table = table.with_note(
"★ 扩展的落点全部在 `main.rs` 的「工程外扩展区」:\
新增型号、新增门店、新增申请序列,\
靠的是「型号 / 门店 / 申请都是**开放型**」这一条设计------\
若它们是 `enum`,扩展就得回来改领域层。",
);
table = table.with_note(
"★ 「证据」列写的是**在哪张表会多一行**,不是「已支持」。\
一句「已支持」无法被核对;「幕五清单多 2 行」可以。",
);
table.render(DOCUMENT_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 : foundation_report.rs
//! 幕二 / 幕三 / 幕四:型号档案、门店清单、服务单价。
//!
//! ## 为什么先铺这三张「字典表」
//!
//! 后面的对照表里全是编码(`BALANCE-0003`、`STORE-CN-01`)。
//! 没有字典表,读者只能靠猜;而猜对的那部分会掩盖猜错的那部分。
//!
//! ## ⚠️ 服务单价单独一幕
//!
//! 「校准一次多少钱」「退役一台多少钱」看起来是小事,
//! 但它是**唯一一个与型号无关的成本**:
//! 维修报价由服务商给,与本设备贵不贵不相关。
//! 单列一幕,是为了让「采购按型号、服务按次」这个区别被看见。
use crate::app::{Table, TableColumn, DOCUMENT_WIDTH};
use crate::domain::{InstrumentModel, StoreCode};
/// 型号表的列宽。
const MODEL_CODE_WIDTH: usize = 18;
const MODEL_NAME_WIDTH: usize = 16;
const MODEL_PURPOSE_WIDTH: usize = 28;
const MODEL_COST_WIDTH: usize = 16;
const MODEL_CALIBRATION_WIDTH: usize = 12;
/// 门店表的列宽。
const STORE_CODE_WIDTH: usize = 20;
const STORE_NAME_WIDTH: usize = 24;
/// 服务单价表的列宽。
const SERVICE_ITEM_WIDTH: usize = 30;
const SERVICE_VALUE_WIDTH: usize = 20;
const SERVICE_NOTE_WIDTH: usize = 64;
/// 幕二:型号档案。
pub fn render_model_table(models: &[InstrumentModel]) -> Vec<String> {
let mut table: Table = Table::new(
"幕二 · 型号档案:一个型号一座池",
vec![
TableColumn::new("型号编码", MODEL_CODE_WIDTH),
TableColumn::new("中文名", MODEL_NAME_WIDTH),
TableColumn::new("用途", MODEL_PURPOSE_WIDTH),
TableColumn::numeric("采购单价", MODEL_COST_WIDTH),
TableColumn::numeric("校准周期", MODEL_CALIBRATION_WIDTH),
],
);
for model in models.iter() {
table = table.with_row(vec![
model.code().to_string(),
model.chinese_name().to_string(),
model.purpose_text().to_string(),
model.acquisition_cost().chinese_text(),
format!("{} 个月", model.calibration_interval_months()),
]);
}
table = table.with_note(
"★ 采购单价**跟着型号走**,服务单价不跟着型号走------\
这两笔钱的性质不同,混在一张表里就会有人拿型号去猜维修费。",
);
table = table.with_note(
"★ 按型号建池而不是「一个大池装所有设备」:\
一个大池的话,`空闲数 == 0` 就开始撒谎(池里明明还有别的型号)。",
);
table.render(DOCUMENT_WIDTH)
}
/// 幕三:门店清单。
pub fn render_store_table(stores: &[StoreCode], purpose_text: &str) -> Vec<String> {
let mut table: Table = Table::new(
"幕三 · 门店清单:谁来借",
vec![
TableColumn::new("门店编码", STORE_CODE_WIDTH),
TableColumn::new("中文名", STORE_NAME_WIDTH),
],
);
for store in stores.iter() {
table = table.with_row(vec![store.code().to_string(), store.chinese_name().to_string()]);
}
table = table.with_note(purpose_text);
table.render(DOCUMENT_WIDTH)
}
/// 幕四:服务单价。
pub fn render_service_price_table(rows: &[(String, String, String)]) -> Vec<String> {
let mut table: Table = Table::new(
"幕四 · 服务单价:修理与处置各多少钱",
vec![
TableColumn::new("项目", SERVICE_ITEM_WIDTH),
TableColumn::numeric("单价", SERVICE_VALUE_WIDTH),
TableColumn::new("说明", SERVICE_NOTE_WIDTH),
],
);
for (item, value, note) in rows.iter() {
table = table.with_row(vec![item.clone(), value.clone(), note.clone()]);
}
table = table.with_note(
"★ 服务成本**逐条累加**而不是「次数 × 单价」:\
同一型号的两次维修报价未必相同,取平均会掩盖掉最贵的那一次。",
);
table.render(DOCUMENT_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 : health_check_report.rs
//! 幕十四:体检汇总。
//!
//! ## ★ 「通过 / 未通过」这四个字全工程只有一处产生
//!
//! 就是 `analysis::CheckLine::verdict_text()`。
//! 若每张表各写一次「通过」「未通过」,
//! 就会出现一处写「通过」、一处写「OK」的分裂,
//! 而读者只会以为那是两件事。
//!
//! ## ⚠️ 体检表必须能**红**
//!
//! 一张永远全绿的体检表与没有体检表是等价的。
//! 因此每一条检查都自带「已知会失败的输入」,
//! 并且本工程在开发中**真的注入过**那些输入确认它会红。
use crate::analysis::CheckReport;
use crate::app::{Table, TableColumn, DOCUMENT_WIDTH};
/// 体检表的列宽。
/// 检查项列宽(最长「★ 至少一档出现「借不到」(容量约束真实存在)」= 44 列)。
const ITEM_WIDTH: usize = 46;
const EXPECTATION_WIDTH: usize = 30;
const ACTUAL_WIDTH: usize = 40;
const VERDICT_WIDTH: usize = 10;
/// 幕十四:体检汇总。
pub fn render_check_table(title: &str, report: &CheckReport) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::new("检查项", ITEM_WIDTH),
TableColumn::elidable(TableColumn::new("期望", EXPECTATION_WIDTH)),
TableColumn::elidable(TableColumn::new("实测", ACTUAL_WIDTH)),
TableColumn::new("结论", VERDICT_WIDTH),
],
);
for line in report.lines().iter() {
table = table.with_row(vec![
line.item().to_string(),
line.expectation_text().to_string(),
line.actual_text().to_string(),
line.verdict_text().to_string(),
]);
}
table = table.with_note(&format!(
"共 {} 条,通过 {} 条,未通过 {} 条。",
report.line_total(),
report.passed_total(),
report.failed_total()
));
if !report.all_passed() {
table = table.with_note(&format!("未通过的检查项:{}", report.failed_items_text()));
}
table.render(DOCUMENT_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 : identity_report.rs
//! 幕七:身份证据 + 两条路径的恒等比对。
//!
//! ## ★ 这一幕回答「池里的那台,是不是当初借出去的那台」
//!
//! 台账上「命中 3 次」只能被相信;
//! 用 `Rc::ptr_eq` 比对**指针**才能被验证。
//! 指针相等是/否进得了报表,指针**值**进不了------
//! 地址随运行变(ASLR),打印会破坏「两次运行逐字节一致」。
//!
//! ## 恒等比对为什么是「同一份场景跑两遍」
//!
//! 两次独立的会话(同参数、同策略)跑完,
//! 池内每一台的**指纹**必须逐字节相同。
//! 这检验的是「装配是否确定性」:
//! 若某处依赖了 `HashMap` 迭代顺序或系统时间,这个比对就会红。
use crate::app::{render_pair_table, Table, TableColumn, DOCUMENT_WIDTH};
/// 两条路径的逐台比对结果。
#[derive(Clone, Debug)]
pub struct IdentityPairView {
/// 序列号。
serial_number: String,
/// 路径一的指纹。
first_text: String,
/// 路径二的指纹。
second_text: String,
/// 是否一致。
identical: bool,
}
impl IdentityPairView {
/// 由两次的指纹组装。
pub fn new(serial_number: &str, first_text: &str, second_text: &str) -> Self {
Self {
serial_number: serial_number.to_string(),
first_text: first_text.to_string(),
second_text: second_text.to_string(),
identical: first_text == second_text,
}
}
/// 序列号。
pub fn serial_number(&self) -> &str {
self.serial_number.as_str()
}
/// 路径一指纹。
pub fn first_text(&self) -> &str {
self.first_text.as_str()
}
/// 路径二指纹。
pub fn second_text(&self) -> &str {
self.second_text.as_str()
}
/// 是否一致。
pub const fn identical(&self) -> bool {
self.identical
}
}
/// 恒等表的列宽。
const SERIAL_WIDTH: usize = 20;
const FINGERPRINT_WIDTH: usize = 40;
const IDENTICAL_WIDTH: usize = 10;
/// 幕七(续):两条路径的逐台指纹比对。
///
/// # 参数
/// - `title`:表标题。
/// - `pairs`:逐台比对结果。
/// - `first_column`:路径一的列名(**由调用方传 `&'static str`**,避免列名撒谎)。
/// - `second_column`:路径二的列名。
///
/// ⚠️ 为什么列名必须是 `&'static str`:
/// `TableColumn` 的表头是 `&'static str`(表头是版面的一部分,不该随数据变)。
/// 若放宽成 `&str`,「这一列是什么」就变成一个运行时值------
/// 那时同一张表在不同调用点会有不同的列名,而读者只会以为是两件事。
pub fn render_identity_table(
title: &str,
pairs: &[IdentityPairView],
first_column: &'static str,
second_column: &'static str,
) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::new("序列号", SERIAL_WIDTH),
TableColumn::elidable(TableColumn::new(first_column, FINGERPRINT_WIDTH)),
TableColumn::elidable(TableColumn::new(second_column, FINGERPRINT_WIDTH)),
TableColumn::new("一致", IDENTICAL_WIDTH),
],
);
for pair in pairs.iter() {
table = table.with_row(vec![
pair.serial_number().to_string(),
pair.first_text().to_string(),
pair.second_text().to_string(),
if pair.identical() { "是".to_string() } else { "否".to_string() },
]);
}
table = table.with_note(
"★ 指纹含「序列号|状态|累计次数|校准到期月」。\
少了后面三段,一台「被多借了三次的仪器」会与「只借过一次的」同指纹,\
于是恒等比对变成恒真的废表。",
);
table.render(DOCUMENT_WIDTH)
}
/// 幕七:身份证据(**两列表**)。
pub fn render_identity_evidence_table(rows: &[(String, String)], notes: &[&str]) -> Vec<String> {
render_pair_table("幕七 · 身份证据:池里的那台,是不是当初借出去的那台", rows, notes)
}
//!# 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 : interface_report.rs
//! 幕十三:对外只读接口清点。
//!
//! ## ★ 这一幕同时是**告警治理的落点**
//!
//! 一个只读接口若在这里被调用并印出结果,
//! 它就不可能是「写了没人用」------也就不可能是「写错了没人发现」。
//!
//! 这是本工程处理 `unused` 警告的**唯一**手段:
//! 不删接口、不加 `#[allow]`,而是把它**用起来**。
//! 删掉一个没人用的接口,等于承认「这个能力本来不需要」;
//! 而它之所以没人用,往往是因为上层在做一件本不该它做的事。
//!
//! ## ⚠️ 清点用的会话必须是**专用**的
//!
//! 既有一个工程在这里出过事:为了清点 `session.read(...)` 又读了一次主会话,
//! 结果「命中次数 == 借到 − 新建」差了 1(61 vs 60),
//! 而那个 1 没有任何一行输出指出它的来源。
//! 因此本工程规定:**清点动作发生在主流程之后,且用新造的对象**。
use crate::app::{Table, TableColumn, DOCUMENT_WIDTH};
/// 接口名列宽(**最长的一条是 `analysis::PoolValueReport::summary_text()`**)。
/// ⚠️ 68 列仍放不下最长的接口名(`app::Table::new(...).with_row(...).with_note(...)` 需 69 列),
/// 因此这一列标成**可省略**:接口名会随接口增加而变长,
/// 把版面预算压在它身上,等于让「新增一个接口」变成一件要改列宽的事。
const INTERFACE_NAME_WIDTH: usize = 68;
/// 输出列宽(`68 + 64 + 2*3 + 1 = 139 <= 140`)。
const INTERFACE_OUTPUT_WIDTH: usize = 64;
/// 幕十三:接口清点。
pub fn render_interface_table(title: &str, rows: &[(String, String)]) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::elidable(TableColumn::new("接口", INTERFACE_NAME_WIDTH)),
TableColumn::elidable(TableColumn::new("本次真实输出", INTERFACE_OUTPUT_WIDTH)),
],
);
for (name, output) in rows.iter() {
table = table.with_row(vec![name.clone(), output.clone()]);
}
table = table.with_note(
"★ 每个接口都**真的调用了一次**并印出结果。\
「列出来」与「调用过」是两件事:\
列出来只能证明它存在,调用过才能证明它还能跑。",
);
table.render(DOCUMENT_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 : inventory_report.rs
//! 幕五:仪器清单(**逐台**)。
//!
//! ## ★ 这一幕是「复用」这件事唯一的肉眼证据
//!
//! 台账说「命中 6 次」,清单说「BALANCE-0001 被借了 3 次」。
//! 后者能看见,前者只能被相信。
//! 因此本表**逐台**列出累计次数------
//! 若只列总数,一台被借了 12 次而另外两台全新这种事就完全看不出来。
//!
//! ## ⚠️ 为什么必须排序后输出
//!
//! 池内对象所在 `Vec` 的顺序取决于借还历史。
//! 直接按 `Vec` 顺序输出,同一份数据集在不同借还顺序下会长得不一样,
//! 于是「两次运行逐字节一致」就不成立了。
//! 本表的行由 `client` 层**按序列号排序**后送来。
use crate::analysis::InstrumentUnitView;
use crate::app::{Table, TableColumn, DOCUMENT_WIDTH};
/// 仪器清单的列宽。
///
/// ⚠️ `SERIAL_WIDTH` 按**最长的可能序列号**定:
/// `GEM_MICROSCOPE-0001` 是 19 列(型号编码 14 + `-0001` 5)。
/// 按当前数据里最长的 `BALANCE-0001`(12 列)定宽,
/// 一旦工程外扩展区加一个长型号编码就会静默截断。
const SERIAL_WIDTH: usize = 20;
/// 型号中文名列宽(最宽「宝石显微镜」= 10 列)。
const MODEL_WIDTH: usize = 12;
/// 状态列宽(最宽「在池空闲」= 8 列)。
const STATE_WIDTH: usize = 10;
/// 被借次数列宽(表头 8 列,数值至多 2 位)。
const USAGE_WIDTH: usize = 8;
/// 持有人列宽(最宽「广州天河店」= 10 列)。
const HOLDER_WIDTH: usize = 12;
/// 校准到期列宽(最宽「2027 年 01 月」= 12 列)。
const DUE_WIDTH: usize = 14;
/// 采购单价列宽(最宽 `¥2,180,000.00` = 13 列)。
const COST_WIDTH: usize = 14;
/// 指纹列宽(**可省略**:`GEM_MICROSCOPE-0001|IDLE|3|202701` = 32 列放不下)。
const FINGERPRINT_WIDTH: usize = 20;
/// 一行仪器(**与 `InstrumentUnitView` 同构**,由 `main` 填充)。
#[derive(Clone, Debug)]
pub struct InventoryRowView {
/// 序列号。
pub serial_number: String,
/// 型号中文名。
pub model_name: String,
/// 状态中文名。
pub state_text: String,
/// 累计被借次数。
pub usage_count: usize,
/// 持有人文本。
pub holder_text: String,
/// 校准到期月文本。
pub calibration_due_text: String,
/// 采购单价文本。
pub acquisition_cost_text: String,
/// 指纹。
pub fingerprint: String,
}
impl InventoryRowView {
/// 由分析层的视图转换。
pub fn from_view(view: &InstrumentUnitView) -> Self {
Self {
serial_number: view.serial_number().to_string(),
model_name: view.model_name().to_string(),
state_text: view.state_text().to_string(),
usage_count: view.usage_count(),
holder_text: view.holder_text().to_string(),
calibration_due_text: view.calibration_due_text().to_string(),
acquisition_cost_text: view.acquisition_cost_text().to_string(),
fingerprint: view.fingerprint().to_string(),
}
}
/// 指纹(跨运行比对用)。
pub fn fingerprint(&self) -> &str {
self.fingerprint.as_str()
}
/// 序列号。
pub fn serial_number(&self) -> &str {
self.serial_number.as_str()
}
}
/// 幕五:仪器清单。
pub fn render_inventory_table(title: &str, rows: &[InventoryRowView]) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::new("序列号", SERIAL_WIDTH),
TableColumn::new("型号", MODEL_WIDTH),
TableColumn::new("状态", STATE_WIDTH),
TableColumn::numeric("被借次数", USAGE_WIDTH),
TableColumn::new("持有人", HOLDER_WIDTH),
TableColumn::new("校准到期", DUE_WIDTH),
TableColumn::numeric("采购单价", COST_WIDTH),
TableColumn::elidable(TableColumn::new("指纹", FINGERPRINT_WIDTH)),
],
);
for row in rows.iter() {
table = table.with_row(vec![
row.serial_number.clone(),
row.model_name.clone(),
row.state_text.clone(),
row.usage_count.to_string(),
row.holder_text.clone(),
row.calibration_due_text.clone(),
row.acquisition_cost_text.clone(),
row.fingerprint.clone(),
]);
}
table = table.with_note(
"★ 「被借次数」是复用的肉眼证据:\
同一台出现 2 次以上,就说明这台设备真的被第二次用过了,\
而不是「台账上记了一次复用」。",
);
table = table.with_note(
"★ 行按序列号排序输出。池内 `Vec` 的顺序取决于借还历史,\
直接按它输出会把「先借后还」的顺序泄漏进报表,\
于是同一份数据在不同借还顺序下长得不一样。",
);
table.render(DOCUMENT_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 : layout.rs
//! 表格引擎:列宽、对齐、可见省略、框线、注解折行。
//!
//! ## 列宽的唯一来源:`TableColumn`
//!
//! 每一列都要**显式声明宽度**,且宽度依据写在声明处的注释里。
//! 不提供「自适应列宽」:自适应会让同一张表在不同数据下宽度不同,
//! 于是「两次运行逐字节一致」依赖数据而不是代码------那是把不确定性引进来了。
use crate::support::{display_width, elide_text, horizontal_rule, pad_left, pad_right};
/// 列对齐。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnAlign {
/// 左对齐(文本列)。
Left,
/// 右对齐(数字列)。
Right,
}
/// 一列。
#[derive(Debug, Clone)]
pub struct TableColumn {
/// 表头文本。
header: &'static str,
/// 列宽(**显示列**,中文按 2 列计)。
width: usize,
/// 对齐方式。
align: ColumnAlign,
/// 是否**允许**可见省略。
///
/// ⚠️ 这个开关是本工程「不许静默截断」纪律的载体:
/// 不标它是默认,标了它必须是**有意**的(该列内容确实不可控)。
elidable: bool,
}
impl TableColumn {
/// 声明一列(左对齐、**不允许**省略)。
pub const fn new(header: &'static str, width: usize) -> Self {
TableColumn { header, width, align: ColumnAlign::Left, elidable: false }
}
/// 声明一个数字列(右对齐、不允许省略)。
pub const fn numeric(header: &'static str, width: usize) -> Self {
TableColumn { header, width, align: ColumnAlign::Right, elidable: false }
}
/// 把本列改成**允许**可见省略(链式)。
pub const fn elidable(mut self) -> Self {
self.elidable = true;
self
}
/// 表头。
pub const fn header(&self) -> &'static str {
self.header
}
/// 列宽。
pub const fn width(&self) -> usize {
self.width
}
/// 对齐。
pub const fn align(&self) -> ColumnAlign {
self.align
}
/// 是否允许省略。
pub const fn is_elidable(&self) -> bool {
self.elidable
}
}
/// 渲染一个单元格。
///
/// # 三条行为
///
/// 1. `elidable == false` → 内容**必须**放得下;否则 `debug_assert!` 当场 panic,
/// 错误信息里带上列宽与所需宽度,直接告诉你该改哪一列。
/// 2. `elidable == true` → 走可见省略,末尾有 `...`,读者知道这里被省了。
/// 3. 省略后按列宽**补白**。
///
/// # ⚠️ 为什么是 `debug_assert` 而不是 `assert`
///
/// 它是**开发期纪律**:`cargo run`(debug)时立刻 panic,
/// release 下不存在,不会让线上报表因一条宽度问题而中断。
/// 同理它也**不是**批量排查的工具------排查请用临时 `eprintln!` 插桩
/// (一次跑出全部位置),见 `app` 模块文档。
pub fn render_cell(column: &TableColumn, text: &str) -> String {
let fitted: String = if column.is_elidable() {
elide_text(text, column.width())
} else {
let needed: usize = display_width(text);
debug_assert!(
needed <= column.width(),
"单元格放不下且该列不允许省略:列「{}」宽 {} 列,内容需要 {} 列,内容原文=[{}]",
column.header(),
column.width(),
needed,
text
);
// debug 下上面已 panic;release 下退化成可见省略,
// 而不是静默截断------**任何构建下都不接受「悄悄少一截」**。
if needed > column.width() {
elide_text(text, column.width())
} else {
text.to_string()
}
};
match column.align() {
ColumnAlign::Left => pad_right(&fitted, column.width()),
ColumnAlign::Right => pad_left(&fitted, column.width()),
}
}
/// 一张表:标题 + 列声明 + 数据行 + 注解。
#[derive(Debug, Clone)]
pub struct Table {
/// 标题(渲染成 `■ 标题`)。
title: String,
/// 列声明。
columns: Vec<TableColumn>,
/// 数据行。
rows: Vec<Vec<String>>,
/// 注解(**永不省略**,只折行)。
notes: Vec<String>,
}
impl Table {
/// 建一张表。
pub fn new(title: &str, columns: Vec<TableColumn>) -> Self {
Table {
title: title.to_string(),
columns,
rows: Vec::new(),
notes: Vec::new(),
}
}
/// 追加一行(链式)。
pub fn with_row(mut self, row: Vec<String>) -> Self {
self.rows.push(row);
self
}
/// 追加一条注解(链式)。
pub fn with_note(mut self, note: &str) -> Self {
self.notes.push(note.to_string());
self
}
/// 列数。
pub fn column_count(&self) -> usize {
self.columns.len()
}
/// 行数。
pub fn row_count(&self) -> usize {
self.rows.len()
}
/// 表的总宽度(各列宽之和 + 每列两侧的空格与竖线)。
///
/// 每列的开销是 3:左空格 1 + 右空格 1 + 竖线 1;
/// 最左边还有一条竖线,因此再 +1。
pub fn total_width(&self) -> usize {
self.columns.iter().map(|column| column.width()).sum::<usize>()
+ self.column_count() * 3
+ 1
}
/// 框线(上框 / 分隔 / 下框)。
fn border_line(&self) -> String {
format!("┌{}┐", self.segments('┬', '─'))
}
fn middle_line(&self) -> String {
format!("├{}┤", self.segments('┼', '─'))
}
fn bottom_line(&self) -> String {
format!("└{}┘", self.segments('┴', '─'))
}
fn segments(&self, joint: char, fill: char) -> String {
self.columns
.iter()
.map(|column| fill.to_string().repeat(column.width() + 2))
.collect::<Vec<String>>()
.join(&joint.to_string())
}
/// 渲染一行数据。
///
/// ⚠️ 单元格数不足时用空串兜底而不是 panic:
/// 少给一个单元格只是这一行少一格,
/// 整张报表仍然可读;panic 会让连后面的幕都出不来。
fn data_line(&self, cells: &[String]) -> String {
let rendered: Vec<String> = self
.columns
.iter()
.enumerate()
.map(|(index, column)| render_cell(column, cells.get(index).map(String::as_str).unwrap_or("")))
.collect();
format!("│ {} │", rendered.join(" │ "))
}
/// 渲染整张表。
pub fn render(self, document_width: usize) -> Vec<String> {
debug_assert!(
self.total_width() <= document_width,
"表宽 {} 超过文档宽度 {}(表:{})",
self.total_width(),
document_width,
self.title
);
let mut lines: Vec<String> = Vec::new();
lines.push(section_header(&self.title, document_width));
lines.push(self.border_line());
// ★ 表头也走 `render_cell`:表头超宽同样要被抓出来。
let header_cells: Vec<String> = self
.columns
.iter()
.map(|column| column.header().to_string())
.collect();
lines.push(self.data_line(&header_cells));
lines.push(self.middle_line());
for row in self.rows.iter() {
debug_assert!(
row.len() == self.columns.len(),
"行宽 {} 与列数 {} 不一致(表:{})",
row.len(),
self.columns.len(),
self.title
);
lines.push(self.data_line(row));
}
lines.push(self.bottom_line());
// ★ 空表不静默:一张没有数据行的表,与「这一幕忘了填数据」在版面上
// 长得一模一样。补一条可见注解,让「零行」变成一个能被看见的状态。
let mut notes: Vec<String> = self.notes.clone();
if self.row_count() == 0 {
notes.push("(本表无数据行)".to_string());
}
for note in notes.iter() {
lines.extend(note_lines(note, document_width));
}
lines
}
}
/// 幕标题:`■ 标题`(右侧补到文档宽度)。
///
/// ⚠️ 标题**只由本函数产生**:若调用方也写一次标题,
/// 就会出现「改一次标题要改两处」------迟早不一致。
pub fn section_header(title: &str, document_width: usize) -> String {
let text: String = format!("■ {}", title);
let used: usize = display_width(&text);
if used >= document_width {
return text;
}
format!("{}{}", text, " ".repeat(document_width - used))
}
/// 把一段长文本折成多行注解(**永不省略**,只折行)。
///
/// ## ⚠️ 可用宽度按「最宽的那种前缀」留白
///
/// 首行前缀 ` · ` 是 **4 列**,续行前缀 ` ` 是 2 列。
/// 若按续行的 2 列留白,首行就会溢出 2 列------
/// 既有工程实测过:37 行注解达到 142 列 / 文档宽 140,
/// 而溢出的注解在框线旁边几乎看不出来。
/// **统一按 4 列留白是唯一不会错的算法**,代价只是续行少 2 列。
///
/// ## 为什么注解永不省略
///
/// 注解承载的是**复算依据**(分子是什么、分母是什么、哪个常数用在哪)。
/// 省略掉一段,读者就少一条验算路径------那正是报表存在的理由。
pub fn note_lines(text: &str, document_width: usize) -> Vec<String> {
let available: usize = document_width.saturating_sub(4).max(1);
let mut lines: Vec<String> = Vec::new();
let mut current: String = String::new();
let mut current_width: usize = 0;
for character in text.chars() {
let character_width: usize = if crate::support::is_wide_character(character) { 2 } else { 1 };
if current_width + character_width > available && !current.is_empty() {
lines.push(current);
current = String::new();
current_width = 0;
}
current.push(character);
current_width += character_width;
}
if !current.is_empty() {
lines.push(current);
}
if lines.is_empty() {
// 空注解仍然产出一行,避免「注解存在但输出为零行」这种不可见状态。
lines.push(String::new());
}
lines
.into_iter()
.enumerate()
.map(|(index, line)| {
if index == 0 {
format!(" · {}", line)
} else {
format!(" {}", line)
}
})
.collect()
}
/// 生成一条与表同宽的横线(供需要额外分隔的场合用)。
pub fn table_rule(width: usize) -> String {
horizontal_rule(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 : lifecycle_report.rs
//! 幕九:生命周期分布 + 服务记录。
//!
//! ## ★ 为什么按状态分档,而不是只看「一共几台」
//!
//! 「一共 8 台」是一个无法验证的数字:
//! 它同时包含「8 台都能用」与「2 台能用、6 台在修」。
//! 分档之后「能用的有几台」才有答案,
//! 而那正是借用申请真正关心的量。
//!
//! ## ⚠️ 为 0 的档也要出现在表里
//!
//! 「校准维修 0 台」与「报表里压根没有这一行」是两件不同的事:
//! 前者说明没事发生,后者说明这一档**没被统计**。
//! 因此统计遍历的是**内置状态全集**,不是实际出现过的状态。
use crate::analysis::LifecycleProbe;
use crate::app::{bool_text, Table, TableColumn, DOCUMENT_WIDTH};
use crate::domain::{ServiceKind, ServiceRecord};
/// 状态表的列宽。
const STATE_WIDTH: usize = 16;
const COUNT_WIDTH: usize = 10;
const IN_POOL_WIDTH: usize = 12;
/// 服务记录表的列宽。
const SERVICE_SERIAL_WIDTH: usize = 20;
const SERVICE_MONTH_WIDTH: usize = 14;
const SERVICE_KIND_WIDTH: usize = 12;
const SERVICE_REASON_WIDTH: usize = 46;
const SERVICE_COST_WIDTH: usize = 16;
/// 幕九:状态分布。
pub fn render_lifecycle_table(title: &str, probes: &[LifecycleProbe]) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::new("状态", STATE_WIDTH),
TableColumn::numeric("台数", COUNT_WIDTH),
TableColumn::new("计入容量", IN_POOL_WIDTH),
],
);
for probe in probes.iter() {
table = table.with_row(vec![
probe.state_name().to_string(),
probe.unit_total().to_string(),
bool_text(probe.in_pool()).to_string(),
]);
}
table = table.with_note(
"★ 只有「在池空闲」计入容量(**可借**)。\
把「校准维修」也算进可用数,容量守恒就会在月底对不上------\
它看起来像是「暂时不在」,但它**不能被借出**。",
);
table.render(DOCUMENT_WIDTH)
}
/// 幕九(续):服务记录。
pub fn render_service_record_table(title: &str, records: &[ServiceRecord]) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::new("序列号", SERVICE_SERIAL_WIDTH),
TableColumn::new("月份", SERVICE_MONTH_WIDTH),
TableColumn::new("类型", SERVICE_KIND_WIDTH),
TableColumn::new("事由", SERVICE_REASON_WIDTH),
TableColumn::numeric("费用", SERVICE_COST_WIDTH),
],
);
for record in records.iter() {
table = table.with_row(vec![
record.serial_number().to_string(),
record.month().chinese_text(),
record.kind().chinese_text().to_string(),
record.reason_text().to_string(),
crate::domain::Money::new(
record.service_cost_minor_units(),
crate::domain::CNY,
)
.chinese_text(),
]);
}
// ★ 按类型汇总写进注解(表格里逐条已经很长,汇总放注解更清楚)。
for (kind, total) in crate::analysis::service_statistics(records) {
table = table.with_note(&format!(
"{}:{} 次;这种服务做完是否回池:{}",
kind.chinese_text(),
total,
bool_text(kind.returns_to_pool())
));
}
table = table.with_note(
"★ 服务记录**只增不减**:写错了就补一条反向记录,\
不要改原来那条------改了之后上个月的报表与这个月的报表会对不上,\
且没有任何痕迹说明为什么对不上。",
);
table.render(DOCUMENT_WIDTH)
}
/// 服务类型中文名(**供体检与清点用**,避免各处各写一份)。
pub fn service_kind_text(kind: ServiceKind) -> &'static str {
kind.chinese_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 : overview_report.rs
//! 幕一:装配总览 + 通用两列表。
//!
//! ## 为什么第一幕是「总览」而不是直接进入结果
//!
//! 读者最先需要的不是结论,是**这份数据是什么**:
//! 几个型号、几家门店、多少笔申请、预热几台。
//! 缺了这些,后面「省下 5 台」这句话没有参照------
//! 5 台可能是 5 台里的 5 台,也可能是 200 台里的 5 台。
use crate::app::{Table, TableColumn, DOCUMENT_WIDTH};
/// 两列表的列名宽(项目列)。
///
/// ⚠️ 40 列按**最宽的项目名**定:「★ 省下的采购台数(不池化要买 − 池化已买)」。
const ITEM_WIDTH: usize = 40;
/// 两列表的值列宽。
///
/// `40 + 92 + 2*3 + 1 = 139 <= 140`。
const VALUE_WIDTH: usize = 92;
/// ★ 通用两列表(**全工程「一项一值」类报表的唯一实现**)。
///
/// 幕一、幕十一、幕十一(续)都用它。
/// 若每处各写一张表,就会出现三份不一样的列宽与对齐------
/// 而读者只会以为是三件事。
pub fn render_pair_table(title: &str, rows: &[(String, String)], notes: &[&str]) -> Vec<String> {
let mut table: Table = Table::new(
title,
vec![
TableColumn::new("项目", ITEM_WIDTH),
TableColumn::new("值", VALUE_WIDTH),
],
);
for (item, value) in rows.iter() {
table = table.with_row(vec![item.clone(), value.clone()]);
}
for note in notes.iter() {
table = table.with_note(note);
}
table.render(DOCUMENT_WIDTH)
}
/// 幕一:装配总览。
///
/// # 参数
/// - `rows`:总览行(**由 `main` 组装**,本函数不认识业务)。
pub fn render_overview_table(rows: &[(String, String)]) -> Vec<String> {
render_pair_table(
"幕一 · 装配总览:这一池子是怎么回事",
rows,
&[
"★ 「预热台数」是开业前先备的货,「申请笔数」是当月实际来的活。\
两者不等是常态:备货看的是峰值,干活看的是总量。",
"★ 三档策略跑的是**同一份**申请序列。若每档各造一份数据,\
「新建次数之差」就无法被解释为「策略造成的」------那正是对照实验最容易失效的地方。",
],
)
}
//!# 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_report.rs
//! 幕六:三档策略对照。
//!
//! ## ★ 本表是「对象池省下了什么」的唯一出处
//!
//! 三档跑**同一份**申请序列,于是「新建次数」这一列的三个数
//! 可以直接相减:按需扩容的新建次数 − 严格池的新建次数 = 省下的采购台数。
//!
//! ## ⚠️ 危险等级为什么放进注解而不是列
//!
//! 「危险等级」的文本最长二十几列,放进表格会把其它列挤到只剩 6 列。
//! 放注解里逐档印出,读者看到的反而更完整------
//! **表格的列宽预算是有上限的,注解没有**。
use crate::analysis::PolicyProbe;
use crate::app::{percent_text, Table, TableColumn, DOCUMENT_WIDTH};
/// 策略列的列宽(**最宽的一档是「限量扩容(到顶即拒绝)」** = 26 列)。
const POLICY_WIDTH: usize = 34;
/// 数字列宽(数字最多 3 位,表头最多 4 列)。
const COUNT_WIDTH: usize = 6;
/// 采购花费列宽(最宽 `¥1,440,000.00` = 13 列)。
const COST_WIDTH: usize = 14;
/// 复用率列宽(最宽 `100.00%` = 7 列)。
const RATIO_WIDTH: usize = 10;
/// 幕六:三档策略对照。
pub fn render_policy_table(probes: &[PolicyProbe]) -> Vec<String> {
let mut table: Table = Table::new(
"幕六 · 三档策略对照:同一份申请,三种「池借空了怎么办」",
vec![
TableColumn::new("策略", POLICY_WIDTH),
TableColumn::numeric("申请", COUNT_WIDTH),
TableColumn::numeric("借到", COUNT_WIDTH),
TableColumn::numeric("拒绝", COUNT_WIDTH),
TableColumn::numeric("新建", COUNT_WIDTH),
TableColumn::numeric("命中", COUNT_WIDTH),
TableColumn::numeric("回池", COUNT_WIDTH),
TableColumn::numeric("隔离", COUNT_WIDTH),
TableColumn::numeric("峰值", COUNT_WIDTH),
TableColumn::numeric("采购花费", COST_WIDTH),
TableColumn::numeric("复用率", RATIO_WIDTH),
],
);
for probe in probes.iter() {
table = table.with_row(vec![
probe.policy_name().to_string(),
probe.request_total().to_string(),
probe.granted_total().to_string(),
probe.rejected_total().to_string(),
probe.created_total().to_string(),
probe.hit_total().to_string(),
probe.returned_total().to_string(),
probe.quarantined_total().to_string(),
probe.peak_borrowed_total().to_string(),
probe.acquisition_cost_text().to_string(),
format!("{}%", percent_text(probe.reuse_ratio_basis_points())),
]);
}
// ★ 危险等级逐档印在注解里(见模块文档)。
for probe in probes.iter() {
table = table.with_note(&format!(
"{}:{};复用率 {}%;服务花费 {}",
probe.policy_name(),
probe.risk_text(),
percent_text(probe.reuse_ratio_basis_points()),
probe.service_cost_text()
));
}
table = table.with_note(
"★ 「新建」是**真的花钱买了几台**,「命中」是借出时池里有空闲、直接复用。\
「省下几台」= 按需扩容的新建 − 严格池的新建,而不是命中次数------\
这两个数在稳态下并不相等。",
);
table = table.with_note(
"★ 「峰值」是不池化要买几台的直接答案:同一时刻最多有几台在外面。\
它必须在**借出那一刻**更新,事后从台账推不出来(台账里没有「同一时刻」)。",
);
table.render(DOCUMENT_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 : value_display.rs
//! 值对象的**报表排版**:把领域层存的整数渲染成人看的文本。
//!
//! ## ★ 哪些排版留在领域层,哪些搬到这里
//!
//! 判据只有一条:**这个文本形式是不是「所有层都要用」**。
//!
//! - **留在领域层**:`Money::chinese_text()`(`¥12,345.67`)。
//! 机制层报错、分析层结论、表示层表格都要把金额嵌进句子里------
//! 把它搬来表示层会让 `borg` 反向依赖 `app`,那才是真违规。
//! 值对象的**规范文本形式**属于值对象本身。
//! - **搬来表示层**:金重与汇率。
//! `TerminalTill` 存的是**毫克整数**,汇率存的是**万分点整数**;
//! 「用克显示、保留 3 位小数」与「把 9215 显示成 0.9215」
//! 是这张表列头写着「金重(克)」「汇率」才有的决定,只有报表需要。
//!
//! ## ⚠️ 为什么这两处可以有除法
//!
//! 整个计算链路全程是整数,除法只发生在 [`crate::domain::divide_rounded`] 一处。
//! 本文件是**唯一的例外**:这两个函数都不参与任何合计,
//! 只把已经算完的整数拆成「整数部分 + 小数部分」,
//! 所以不存在舍入问题------余数是整除的余数,不是被舍掉的部分。
//!
//! 换句话说:**展示可以除,算账不可以除**。
/// 把毫克整数渲染成「克,3 位小数」的文本(如 `12500` → `"12.500"`)。
///
/// ⚠️ 用 `div_euclid` / `rem_euclid` 而不是 `/` 与 `%`:
/// 若将来出现负重量(冲减),`%` 会得到负余数,
/// 拼出来的串会变成 `-12.-500`。
pub fn gram_display_text(milligrams: i64) -> String {
let whole: i64 = milligrams.div_euclid(1000);
let remainder: i64 = milligrams.rem_euclid(1000);
format!("{}.{:03}", whole, remainder)
}
/// 把汇率万分点渲染成小数文本(如 `9215` → `"0.9215"`)。
///
/// ⚠️ **`10000` 与 `10000.0` 是完全不同的两件事**:
/// 前者是 1:1,后者是「万倍」。写成 `{}.{:04}` 而不除 10000,
/// 读者会把 `9215` 读成「九千多倍汇率」。
pub fn basis_points_text(basis_points: i64) -> String {
let whole: i64 = basis_points.div_euclid(10_000);
let remainder: i64 = basis_points.rem_euclid(10_000);
format!("{}.{:04}", whole, remainder)
}
/// ★ 万分点转百分数文本(`10000` -> `100.00`)。
///
/// ⚠️ 为什么单列一个函数:`basis_points_text` 输出的是「几点几」
/// (`10000` -> `1.0000`,因为 10000 万分点 = 1 倍)。
/// 直接给它加一个 `%` 会变成 `1.0000%`------那是**百分之一**,
/// 而复用率 100% 应该显示 `100.00%`。
/// 这个错误在报表上完全看不出来(都是一个带百分号的小数),
/// 只能靠追问「分母是什么」才能发现,因此口径必须收在一处。
pub fn percent_text(basis_points: i64) -> String {
format!("{}.{:02}", basis_points / 100, (basis_points % 100).abs())
}
/// 把布尔量渲染成中文(`是` / `否`)。
///
/// ⚠️ 报表的布尔列一律走这里,不写 `if x { "是" } else { "否" }`:
/// 散着写迟早会出现一处写成 `符合/不符合`,
/// 而读者会以为那是另一种判断。
pub const fn bool_text(value: bool) -> &'static str {
if value {
"是"
} else {
"否"
}
}
/// 把字节数渲染成带千分位的文本。
///
/// ⚠️ 用整数反复除 10 取位,**全程不出现浮点**(与 `Money` 的千分位同理)。
pub fn byte_text(bytes: usize) -> String {
let digits: Vec<char> = bytes.to_string().chars().collect();
let mut buffer: Vec<char> = Vec::new();
for (index, character) in digits.iter().enumerate() {
let from_right: usize = digits.len() - index;
if index > 0 && from_right % 3 == 0 {
buffer.push(',');
}
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 : value_report.rs
//! 幕十一:价值量化 + 台账快照。
//!
//! ## ★ 省下的不是时间,是「造一个新对象」这件事本身
//!
//! 本工程里「造」等于**采购一台设备**,所以省下的能用金额算出来。
//! 这是刻意选这个业务域的原因:
//! 若选一个「造对象只要几微秒」的例子,
//! 省下的就只能用**耗时**表达,而耗时不可复现------
//! 那正是「我们用了对象池所以很快」这句话永远无法被反驳的根源。
//!
//! ## ⚠️ 省下的台数是**两个可数数字之差**
//!
//! ```text
//! 不池化要买几台 = 峰值并发在借数(按需扩容档的新建次数)
//! 池化实际买了几台 = 严格池档的新建次数
//! 省下的台数 = 前者 − 后者
//! ```
//!
//! 不用「命中次数」当省下的台数:命中 6 次只说明复用发生了 6 次,
//! 而「本来要买 8 台、实际买了 3 台」里的 5 台才是真正的差额。
use crate::analysis::{LedgerSnapshotView, PoolValueReport};
use crate::app::{percent_text, render_pair_table, bool_text};
use crate::domain::{Money, CNY};
/// 幕十一:价值量化。
pub fn render_value_table(report: &PoolValueReport) -> Vec<String> {
let rows: Vec<(String, String)> = vec![
("申请笔数".to_string(), format!("{} 笔", report.request_total())),
("借到笔数".to_string(), format!("{} 笔", report.granted_total())),
("没借到笔数".to_string(), format!("{} 笔", report.rejected_total())),
(
"★ 池化档实际买了几台(严格池新建次数)".to_string(),
format!("{} 台", report.created_with_pool_total()),
),
(
"★ 不池化要买几台(按需扩容新建次数)".to_string(),
format!("{} 台", report.created_without_pool_total()),
),
(
"★ 省下的采购台数(两个可数数字之差)".to_string(),
format!("{} 台", report.saved_purchase_total()),
),
(
"★ 省下的采购金额".to_string(),
report.saved_money().chinese_text(),
),
(
"省下的比例(省下台数 / 不池化要买)".to_string(),
format!("{}%", percent_text(report.saved_ratio_basis_points())),
),
(
"★ 采购单价(见幕二,按型号分别折算)".to_string(),
"见幕二".to_string(),
),
("命中次数(复用发生过几次)".to_string(), format!("{} 次", report.hit_total())),
(
"归还后回池次数(真正可供下次使用)".to_string(),
format!("{} 次", report.reused_total()),
),
("借出峰值(同一时刻最多几台在外面)".to_string(), format!("{} 台", report.peak_borrowed_total())),
(
"服务成本(校准与退役,逐条累加)".to_string(),
Money::new(report.service_cost_minor_units(), CNY).chinese_text(),
),
("一句话结论".to_string(), report.summary_text()),
];
render_pair_table(
"幕十一 · 价值量化:对象池省下的到底是什么",
&rows,
&[
"★ 「省下的台数」取的是两个**新建次数**之差,不是命中次数。\
命中次数回答「复用发生了几次」,新建次数之差回答「少买了几台」------\
这两个数在稳态下并不相等,取哪个必须先说清。",
"★ 不用耗时当度量:耗时不可复现(同一份代码两次运行差 20% 很正常),\
而本工程要求两次运行逐字节一致。台数与金额都是整点数,可数、可复算。",
],
)
}
/// 幕十一(续):台账快照。
///
/// # 参数
/// - `view`:快照视图。
/// - `moment_text`:**必须写清是哪个时刻**(台账只增不减,时刻不同数字就不同)。
pub fn render_ledger_snapshot_table(view: &LedgerSnapshotView, moment_text: &str) -> Vec<String> {
let mut rows: Vec<(String, String)> = view.rows().to_vec();
rows.push((
"快照时刻".to_string(),
moment_text.to_string(),
));
rows.push((
"是否快照(而非实时计数)".to_string(),
bool_text(true).to_string(),
));
render_pair_table(
"幕十一(续)· 池台账快照(只增不减)",
&rows,
&[
"★ 台账**只增不减**,因此报表一律读**快照**而不是实时计数:\
否则表格先渲染的几行与后渲染的几行来自不同时刻,\
读者会以为「后面那些清点动作没有副作用」------事实恰好相反。",
"★ 两条恒等式让「某处计数漏记」变成一个可计算的数字,而不是一句原则:\
借出 − (命中 + 新建) 必须为 0;归还 − (回池 + 隔离) 必须为 0。",
],
)
}
调用:
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 : mainrs
//! 对象池模式(Object Pool Pattern)演示:**区域检测中心的仪器借用池**。
//!
//! ## 严格分层结构
//!
//! ```text
//! main 装配点:造数据、建站、跑场景、编排十四幕、工程外扩展区
//! └── app 表示层:表格引擎 + 十二类报表(**不认识 pool / client**)
//! └── analysis 分析层:三档对照 / 容量盘点 / 身份证据 / 价值量化 / 体检
//! └── client 调用层:把仪器接进池(impl / 工厂 / 归还判据)+ 会话
//! ├── lazy ★ 机制层:对象池本体 + 台账 + 契约审计(**零 crate:: 依赖**)
//! └── domain 领域层:型号 / 个体 / 状态 / 门店 / 服务记录 / 金额
//! └── support 支撑层:日期 / 确定性编码 / 显示宽度
//! ```
//!
//! ## 依赖方向(**实测**,由 `scripts/check_layer_dependencies.sh` 得出,非设计意图)
//!
//! ```text
//! app -> analysis domain support
//! analysis -> client domain pool
//! client -> domain pool
//! domain -> support
//! pool -> none ★ 机制层,与 support 同秩(整层可搬走)
//! support -> none
//! ```
//!
//! ### 实测推翻过什么
//!
//! - **`pool -> none`**:设计图上原本画的是 `domain` 在最底、`pool` 在它之上。
//! 实测是 `pool -> none`------机制层谁也不依赖。
//! 按纪律**改图不改测量**:图必须反映代码,而不是反映当初的想法。
//!
//! - **`analysis -> pool`**:分析层要读机制层的审计结果才能出报表,
//! 这本来会让 `app` 也去 import `pool`。
//! 实测后补了 `AuditCheckView` / `LedgerSnapshotView` 两个分析层视图,
//! 把 `app -> pool` 断掉,而不是放宽红线。
//!
//! ## ★ 本工程最想讲清的三件事
//!
//! 1. **对象池省下的不是时间,是「造一个新对象」这件事本身**。
//! 本工程里「造」等于采购一台设备,所以省下的能用金额算出来
//! (省下台数 × 型号采购单价)。刻意不选「造对象只要几微秒」的例子:
//! 那种例子里省下的只能用**耗时**表达,而耗时不可复现------
//! 那正是「我们用了对象池所以很快」这句话永远无法被反驳的根源。
//!
//! 2. **省下的台数是两个可数数字之差**,不是「复用次数」:
//! `不池化要买几台(峰值并发) − 池化实际买了几台`。
//! 只有一个方案时,那个数字既是结论也是唯一的证据;
//! 三档策略跑同一份申请序列,它才变成一个可以相减的差。
//!
//! 3. **复用必须是「同一台」**,不是「新建一个一模一样的」。
//! 后者在台账上一样会记一次复用、一样省下一笔钱,
//! 但那是假复用。因此本工程用 `Rc::ptr_eq` 比对**指针**------
//! 序列号可以被复制,指针不能。
//!
//! ## 为什么「申请要持有几步」
//!
//! 若每笔申请都是「借了立刻还」,同一时刻在外面永远只有一台,
//! 峰值恒为 1------那时**池与不池化买的数量完全一样**,
//! 「省下几台」必然是 0,整个对照实验是恒真的废表。
//! 场景里的 `hold_steps` 让**并发**成为数据的一部分。
// =============================================================================
// 模块声明(**二进制 crate 也要声明**,否则 `crate::xxx` 找不到)
// =============================================================================
pub mod analysis;
pub mod app;
pub mod client;
pub mod domain;
pub mod pool;
pub mod support;
use std::cell::RefCell;
use std::rc::Rc;
use crate::analysis::{
audit_check_views, capacity_checks, holder_display_text, identity_checks, lifecycle_checks,
lifecycle_statistics, policy_checks, service_statistics, AuditCheckView, CapacityProbe,
CheckLine, CheckReport, InstrumentUnitView, LedgerSnapshotView, PoolValueReport, PolicyProbe,
};
use crate::app::{
basis_points_text, bool_text, byte_text, gram_display_text, note_lines, render_audit_table,
render_capacity_table,
render_check_table, render_extension_table, render_identity_evidence_table,
render_identity_table, render_interface_table, render_inventory_table,
render_ledger_snapshot_table, render_lifecycle_table, render_model_table,
render_overview_table, render_pair_table, render_policy_table, render_service_price_table,
render_service_record_table, render_store_table, render_value_table, section_header,
table_rule, CapacityRowView, ExtensionRowView, IdentityPairView, InventoryRowView,
DOCUMENT_WIDTH,
};
use crate::client::{
instrument_factory, return_verifier, should_retire, InstrumentStation, PoolSession,
UsageScenario, UsageStep, SERVICE_COST_CALIBRATION_MINOR_UNITS,
SERVICE_COST_RETIREMENT_MINOR_UNITS,
};
use crate::domain::{
divide_rounded, ratio_basis_points, scale_rounded, InstrumentModel, InstrumentState,
InstrumentUnit, Money, MonthKey, ServiceKind, ServiceRecord, StoreCode, UsageRequest,
BUILTIN_CURRENCY_CODES, BUILTIN_INSTRUMENT_MODELS, BUILTIN_INSTRUMENT_STATES,
BUILTIN_SERVICE_KINDS, BUILTIN_STORE_CODES, CNY, HKD, MODEL_BALANCE, MODEL_GEM_MICROSCOPE,
MODEL_GOLD_TESTER, RETIREMENT_USAGE_LIMIT, STORE_CN_01, STORE_HK_01, USD,
};
use crate::pool::{
audit_pool, observe_pool, ObjectPool, PooledObject, ReleaseOutcome, ReusePolicy,
BUILTIN_REUSE_POLICIES,
};
use crate::support::{
days_between, days_in_month, deterministic_code, display_width, elide_text, is_leap_year,
prefixed_code, report_cell_text, self_check_width_model, stable_hash, CalendarDate,
};
// =============================================================================
// 演示数据(**都在装配点**,领域层只有内置常量,没有演示数据)
// =============================================================================
/// 业务当月。
const DEMO_MONTH: MonthKey = MonthKey::new(2026, 10);
/// 出厂月份(首批仪器的校准起点)。
const MANUFACTURE_MONTH: MonthKey = MonthKey::new(2026, 1);
/// 各型号的预热台数(开业前先备的货)。
///
/// ⚠️ 预热数必须**小于**峰值并发数,否则「池化」与「不池化」买的一样多,
/// 对照实验就是一张恒真的废表。
const PREWARM_TOTAL: usize = 1;
/// 限量扩容档的池内上限。
const LIMITED_MAX_TOTAL: usize = 3;
/// 内置场景:三家门店在一个月里的真实借用节奏。
fn builtin_scenario() -> UsageScenario {
UsageScenario::new(
"内置场景(2026-10,两家门店)",
DEMO_MONTH,
MANUFACTURE_MONTH,
vec![
// ★ 前两笔刻意重叠:广州店借走天平后**还沒还**,香港店又来借。
// 这一重叠就是「并发」,也是池存在的理由。
UsageStep::new(
UsageRequest::new(STORE_CN_01, MODEL_BALANCE, DEMO_MONTH, "黄金回购称重"),
3,
),
UsageStep::new(
UsageRequest::new(STORE_HK_01, MODEL_BALANCE, DEMO_MONTH, "金重复核"),
0,
),
// ★ 测金仪同样是重叠的两笔。它的校准周期只有 6 个月,
// 出厂 2026-01 → 到期 2026-07 < 当月 2026-10,因此**归还时必然被隔离**。
UsageStep::new(
UsageRequest::new(STORE_CN_01, MODEL_GOLD_TESTER, DEMO_MONTH, "成色复检"),
2,
),
UsageStep::new(
UsageRequest::new(STORE_HK_01, MODEL_GOLD_TESTER, DEMO_MONTH, "进店检测"),
1,
),
UsageStep::new(
UsageRequest::new(STORE_CN_01, MODEL_GEM_MICROSCOPE, DEMO_MONTH, "钻石分级"),
2,
),
UsageStep::new(
UsageRequest::new(STORE_CN_01, MODEL_BALANCE, DEMO_MONTH, "月末盘点"),
1,
),
UsageStep::new(
UsageRequest::new(STORE_HK_01, MODEL_GEM_MICROSCOPE, DEMO_MONTH, "翡翠观察"),
0,
),
UsageStep::new(
UsageRequest::new(STORE_CN_01, MODEL_GOLD_TESTER, DEMO_MONTH, "客诉复测"),
0,
),
UsageStep::new(
UsageRequest::new(STORE_HK_01, MODEL_BALANCE, DEMO_MONTH, "客诉复测"),
1,
),
UsageStep::new(
UsageRequest::new(STORE_CN_01, MODEL_BALANCE, DEMO_MONTH, "月末复盘"),
0,
),
],
)
}
// =============================================================================
// ★ 工程外扩展区:新增型号 / 新增门店 / 新增申请序列(**不改任何既有分层文件**)
// =============================================================================
/// 【扩展】激光打标机:一个**新型号**。
///
/// ⚠️ 它定义在 `main.rs` 而不是 `domain`:
/// 型号是开放型(`const fn new`),所以新增型号不需要回领域层改 `enum`。
/// 这就是「可扩展性必须被实证」的落点------
/// 若型号是 `enum`,这一行就得写进领域层文件。
const MODEL_LASER_MARKER: InstrumentModel = InstrumentModel::new(
"LASER_MARKER",
"激光打标机",
"在饰品内壁刻印品牌与成色",
Money::new(2_180_000, CNY),
12,
);
/// 【扩展】澳门氹仔店:一家**新门店**(与新型号同理,定义在装配点)。
const STORE_MO_01: StoreCode = StoreCode::new("STORE-MO-01", "澳门氹仔店");
/// 【扩展】扩展场景:内置 10 步 + 新增门店 / 新型号的一批申请。
///
/// ★ 追加的这 8 笔天平申请是**密集串行**的(`hold_steps = 0`),
/// 目的只有一个:让某台天平真的被借够 [`RETIREMENT_USAGE_LIMIT`] 次而退役。
/// 退役路径若从不发生,「退役台数 == 退役记录数」就是 `0 == 0`------
/// 一个恒真的废检查。
fn extended_scenario() -> UsageScenario {
let mut steps: Vec<UsageStep> = builtin_scenario().steps().to_vec();
steps.push(UsageStep::new(
UsageRequest::new(STORE_MO_01, MODEL_LASER_MARKER, DEMO_MONTH, "新店刻印备货"),
1,
));
steps.push(UsageStep::new(
UsageRequest::new(STORE_CN_01, MODEL_LASER_MARKER, DEMO_MONTH, "定制件刻印"),
0,
));
steps.push(UsageStep::new(
UsageRequest::new(STORE_MO_01, MODEL_BALANCE, DEMO_MONTH, "新店开张备货"),
0,
));
steps.push(UsageStep::new(
UsageRequest::new(STORE_CN_01, MODEL_BALANCE, DEMO_MONTH, "展销会称重"),
0,
));
steps.push(UsageStep::new(
UsageRequest::new(STORE_HK_01, MODEL_BALANCE, DEMO_MONTH, "展销会称重"),
0,
));
steps.push(UsageStep::new(
UsageRequest::new(STORE_MO_01, MODEL_BALANCE, DEMO_MONTH, "展销会称重"),
0,
));
steps.push(UsageStep::new(
UsageRequest::new(STORE_CN_01, MODEL_BALANCE, DEMO_MONTH, "展销会收尾"),
0,
));
steps.push(UsageStep::new(
UsageRequest::new(STORE_HK_01, MODEL_BALANCE, DEMO_MONTH, "展销会收尾"),
0,
));
UsageScenario::new(
"扩展场景(内置 + 澳门新店 + 激光打标机)",
DEMO_MONTH,
MANUFACTURE_MONTH,
steps,
)
}
/// 打印一组行。
fn print_lines(lines: &[String]) {
for line in lines.iter() {
println!("{}", line);
}
}
/// ★ 对外只读接口清点:**每个接口都真的调用一次并印出结果**。
///
/// ## 为什么这一函数是告警治理的落点
///
/// `unused` 警告的正确修法不是删接口、也不是加 `#[allow]`,
/// 而是**把它用起来**:一个接口若在这里被调用,
/// 它就不可能是「写了没人用」------也就不可能是「写错了没人发现」。
///
/// ## ⚠️ 清点用的对象必须是**新造的**
///
/// 既不借也不还、不碰主流程的那些池。
/// 既有一个工程在这里出过事:为了清点 `session.read(...)` 又读了一次主会话,
/// 结果「缓存命中 == Σ(读取−1)」差了 1,而那个 1 没有任何一行输出指出它的来源。
fn interface_items(
strict_sessions: &[PoolSession],
grow_sessions: &[PoolSession],
value_report: &PoolValueReport,
snapshot_view: &LedgerSnapshotView,
probes: &[PolicyProbe],
all_views: &[InstrumentUnitView],
all_service_records: &[ServiceRecord],
inventory_rows: &[InventoryRowView],
) -> Vec<(String, String)> {
let mut items: Vec<(String, String)> = Vec::new();
let first_station: &InstrumentStation = strict_sessions[0].station();
let first_pool = first_station.pool();
let sample_unit: InstrumentUnit =
InstrumentUnit::new("DEMO-0001", MODEL_BALANCE, MonthKey::new(2026, 7));
let sample_money: Money = Money::new(123_456, CNY);
let sample_date: CalendarDate = CalendarDate::new(2026, 10, 8);
// ---- support ----
items.push((
"support::stable_hash(&[\"a\",\"b\"])".to_string(),
stable_hash(&["a", "b"]).to_string(),
));
items.push((
"support::deterministic_code(&[\"x\"], 6)".to_string(),
deterministic_code(&["x"], 6),
));
items.push((
"support::prefixed_code(\"P\", &[\"x\"], 6)".to_string(),
prefixed_code("P", &["x"], 6),
));
items.push((
"support::display_width(\"天平A\")".to_string(),
display_width("天平A").to_string(),
));
items.push((
"support::is_wide_character('─') // 框线字符不占 2 列".to_string(),
bool_text(crate::support::is_wide_character('─')).to_string(),
));
items.push((
"support::pad_right(\"ab\", 6)".to_string(),
format!("[{}]", crate::support::pad_right("ab", 6)),
));
items.push((
"support::pad_left(\"ab\", 6)".to_string(),
format!("[{}]", crate::support::pad_left("ab", 6)),
));
items.push((
"support::pad_center(\"ab\", 8)".to_string(),
format!("[{}]", crate::support::pad_center("ab", 8)),
));
items.push((
"support::truncate_to_width(\"abcdef\", 3)".to_string(),
crate::support::truncate_to_width("abcdef", 3),
));
items.push((
"support::elide_text(\"abcdefghij\", 4)".to_string(),
elide_text("abcdefghij", 4),
));
items.push((
"support::horizontal_rule(6)".to_string(),
crate::support::horizontal_rule(6),
));
items.push((
"support::self_check_width_model() // 项数 / 未通过数".to_string(),
format!(
"{} 项,未通过 {} 项",
self_check_width_model().len(),
self_check_width_model().iter().filter(|(_, ok, _)| !ok).count()
),
));
items.push((
"support::CalendarDate::year() / month() / day()".to_string(),
format!(
"{}/{}/{}",
sample_date.year(),
sample_date.month(),
sample_date.day()
),
));
items.push((
"support::CalendarDate::is_valid()".to_string(),
bool_text(sample_date.is_valid()).to_string(),
));
items.push((
"support::CalendarDate::add_days(30)".to_string(),
report_cell_text(sample_date.add_days(30)),
));
items.push((
"support::days_between(2026-10-01, 2026-10-08)".to_string(),
days_between(CalendarDate::new(2026, 10, 1), sample_date).to_string(),
));
items.push((
"support::days_in_month(2026, 10)".to_string(),
days_in_month(2026, 10).to_string(),
));
items.push((
"support::is_leap_year(2024)".to_string(),
bool_text(is_leap_year(2024)).to_string(),
));
// ---- domain ----
items.push((
"domain::MonthKey::year() / month()".to_string(),
format!("{}/{}", DEMO_MONTH.year(), DEMO_MONTH.month()),
));
items.push((
"domain::MonthKey::is_valid()".to_string(),
bool_text(DEMO_MONTH.is_valid()).to_string(),
));
items.push((
"domain::MonthKey::is_same()".to_string(),
bool_text(DEMO_MONTH.is_same(&MonthKey::new(2026, 10))).to_string(),
));
items.push((
"domain::MonthKey::month_index()".to_string(),
DEMO_MONTH.month_index().to_string(),
));
items.push((
"domain::MonthKey::previous() / next()".to_string(),
format!(
"{} / {}",
DEMO_MONTH.previous().chinese_text(),
DEMO_MONTH.next().chinese_text()
),
));
items.push((
"domain::MonthKey::same_month_last_year()".to_string(),
DEMO_MONTH.same_month_last_year().chinese_text(),
));
items.push((
"domain::MonthKey::months_since(2026-01)".to_string(),
DEMO_MONTH.months_since(&MANUFACTURE_MONTH).to_string(),
));
items.push((
"domain::MonthKey::is_before()".to_string(),
bool_text(MANUFACTURE_MONTH.is_before(&DEMO_MONTH)).to_string(),
));
items.push((
"domain::MonthKey::machine_text()".to_string(),
DEMO_MONTH.machine_text(),
));
items.push((
"domain::MonthKey::days()".to_string(),
DEMO_MONTH.days().to_string(),
));
items.push((
"★ domain::MonthKey::plus_months(6) // 校准到期月顺延".to_string(),
MANUFACTURE_MONTH.plus_months(6).chinese_text(),
));
items.push((
"domain::divide_rounded(7, 2) // 唯一舍入口径".to_string(),
divide_rounded(7, 2).to_string(),
));
items.push((
"domain::ratio_basis_points(1, 3)".to_string(),
format!("{} 万分点", ratio_basis_points(1, 3)),
));
items.push((
"domain::scale_rounded(1000, 1, 4)".to_string(),
scale_rounded(1_000, 1, 4).to_string(),
));
items.push((
"domain::CurrencyCode::code() / chinese_name()".to_string(),
format!("{} / {}", CNY.code(), CNY.chinese_name()),
));
items.push((
"domain::BUILTIN_CURRENCY_CODES // 币种数".to_string(),
format!("{} 种(含 {} / {})", BUILTIN_CURRENCY_CODES.len(), HKD.code(), USD.code()),
));
items.push((
"domain::Money::zero(CNY).is_zero()".to_string(),
bool_text(Money::zero(CNY).is_zero()).to_string(),
));
items.push((
"domain::Money::minor_units() / currency()".to_string(),
format!("{} / {}", sample_money.minor_units(), sample_money.currency().code()),
));
items.push((
"domain::Money::plus() / minus()".to_string(),
format!(
"{} / {}",
sample_money.plus(Money::new(100, CNY)).chinese_text(),
sample_money.minus(Money::new(100, CNY)).chinese_text()
),
));
items.push((
"domain::Money::negate()".to_string(),
sample_money.negate().chinese_text(),
));
items.push((
"domain::Money::scale_basis_points(5000) // 五折".to_string(),
sample_money.scale_basis_points(5_000).chinese_text(),
));
items.push((
"domain::Money::scale_ratio(1, 4)".to_string(),
sample_money.scale_ratio(1, 4).chinese_text(),
));
items.push((
"domain::Money::amount_text()".to_string(),
sample_money.amount_text(),
));
items.push((
"domain::Money::chinese_text()".to_string(),
sample_money.chinese_text(),
));
items.push((
"domain::Money::is_same_as()".to_string(),
bool_text(sample_money.is_same_as(&Money::new(123_456, CNY))).to_string(),
));
items.push((
"domain::StoreCode::code() / chinese_name()".to_string(),
format!("{} / {}", STORE_CN_01.code(), STORE_CN_01.chinese_name()),
));
items.push((
"domain::BUILTIN_STORE_CODES // 内置门店数".to_string(),
format!("{} 家", BUILTIN_STORE_CODES.len()),
));
items.push((
"domain::InstrumentModel::code() / chinese_name()".to_string(),
format!("{} / {}", MODEL_BALANCE.code(), MODEL_BALANCE.chinese_name()),
));
items.push((
"domain::InstrumentModel::purpose_text()".to_string(),
MODEL_GOLD_TESTER.purpose_text().to_string(),
));
items.push((
"domain::InstrumentModel::acquisition_cost()".to_string(),
MODEL_GOLD_TESTER.acquisition_cost().chinese_text(),
));
items.push((
"domain::InstrumentModel::calibration_interval_months()".to_string(),
format!("{} 个月", MODEL_GOLD_TESTER.calibration_interval_months()),
));
items.push((
"domain::InstrumentModel::currency()".to_string(),
MODEL_GOLD_TESTER.currency().code().to_string(),
));
items.push((
"domain::BUILTIN_INSTRUMENT_MODELS // 内置型号数".to_string(),
format!("{} 种", BUILTIN_INSTRUMENT_MODELS.len()),
));
items.push((
"domain::InstrumentState::chinese_text() // 四档".to_string(),
BUILTIN_INSTRUMENT_STATES
.iter()
.map(|state| state.chinese_text())
.collect::<Vec<&str>>()
.join(" / "),
));
items.push((
"domain::InstrumentState::machine_text()".to_string(),
InstrumentState::InCalibration.machine_text().to_string(),
));
items.push((
"★ domain::InstrumentState::is_in_pool() // 只有空闲算在池里".to_string(),
format!(
"空闲={} 在借={} 校准={} 退役={}",
bool_text(InstrumentState::Idle.is_in_pool()),
bool_text(InstrumentState::InUse.is_in_pool()),
bool_text(InstrumentState::InCalibration.is_in_pool()),
bool_text(InstrumentState::Retired.is_in_pool())
),
));
items.push((
"domain::InstrumentState::can_be_borrowed()".to_string(),
bool_text(InstrumentState::Idle.can_be_borrowed()).to_string(),
));
items.push((
"domain::InstrumentState::is_out_of_pool_permanently()".to_string(),
bool_text(InstrumentState::Retired.is_out_of_pool_permanently()).to_string(),
));
items.push((
"★ domain::RETIREMENT_USAGE_LIMIT".to_string(),
format!("{} 次", RETIREMENT_USAGE_LIMIT),
));
items.push((
"domain::InstrumentUnit::serial_number()".to_string(),
sample_unit.serial_number().to_string(),
));
items.push((
"domain::InstrumentUnit::model().code()".to_string(),
sample_unit.model().code().to_string(),
));
items.push((
"domain::InstrumentUnit::state().chinese_text()".to_string(),
sample_unit.state().chinese_text().to_string(),
));
items.push((
"domain::InstrumentUnit::usage_count()".to_string(),
sample_unit.usage_count().to_string(),
));
items.push((
"domain::InstrumentUnit::calibration_due_month()".to_string(),
sample_unit.calibration_due_month().chinese_text(),
));
items.push((
"domain::InstrumentUnit::holder() // 无人持有".to_string(),
holder_display_text(sample_unit.holder()),
));
items.push((
"domain::InstrumentUnit::acquisition_cost()".to_string(),
sample_unit.acquisition_cost().chinese_text(),
));
items.push((
"★ domain::InstrumentUnit::is_reusable_on_return(当月)".to_string(),
bool_text(sample_unit.is_reusable_on_return(DEMO_MONTH)).to_string(),
));
items.push((
"★ domain::InstrumentUnit::rejection_reason_text(当月)".to_string(),
match sample_unit.rejection_reason_text(DEMO_MONTH) {
Some(reason) => reason.to_string(),
None => "可以回池".to_string(),
},
));
items.push((
"domain::InstrumentUnit::identity_text()".to_string(),
sample_unit.identity_text(),
));
items.push((
"★ domain::InstrumentUnit::fingerprint_text()".to_string(),
sample_unit.fingerprint_text(),
));
items.push((
"domain::InstrumentUnit::summary_text()".to_string(),
sample_unit.summary_text(),
));
items.push((
"domain::ServiceKind::chinese_text() // 三类".to_string(),
BUILTIN_SERVICE_KINDS
.iter()
.map(|kind| kind.chinese_text())
.collect::<Vec<&str>>()
.join(" / "),
));
items.push((
"domain::ServiceKind::machine_text()".to_string(),
ServiceKind::Calibration.machine_text().to_string(),
));
items.push((
"★ domain::ServiceKind::returns_to_pool() // 退役不回池".to_string(),
format!(
"校准={} 维修={} 退役={}",
bool_text(ServiceKind::Calibration.returns_to_pool()),
bool_text(ServiceKind::Repair.returns_to_pool()),
bool_text(ServiceKind::Retirement.returns_to_pool())
),
));
let sample_record: ServiceRecord = ServiceRecord::new(
"DEMO-0001",
DEMO_MONTH,
ServiceKind::Calibration,
"校准已过期",
SERVICE_COST_CALIBRATION_MINOR_UNITS,
);
items.push((
"domain::ServiceRecord::serial_number() / month()".to_string(),
format!("{} / {}", sample_record.serial_number(), sample_record.month().chinese_text()),
));
items.push((
"domain::ServiceRecord::kind().chinese_text()".to_string(),
sample_record.kind().chinese_text().to_string(),
));
items.push((
"domain::ServiceRecord::reason_text()".to_string(),
sample_record.reason_text().to_string(),
));
items.push((
"domain::ServiceRecord::service_cost_minor_units()".to_string(),
sample_record.service_cost_minor_units().to_string(),
));
items.push((
"domain::ServiceRecord::fingerprint_text()".to_string(),
sample_record.fingerprint_text(),
));
let sample_request: UsageRequest =
UsageRequest::new(STORE_CN_01, MODEL_BALANCE, DEMO_MONTH, "演示申请");
items.push((
"domain::UsageRequest::store().chinese_name()".to_string(),
sample_request.store().chinese_name().to_string(),
));
items.push((
"domain::UsageRequest::model().code()".to_string(),
sample_request.model().code().to_string(),
));
items.push((
"domain::UsageRequest::month().chinese_text()".to_string(),
sample_request.month().chinese_text(),
));
items.push((
"domain::UsageRequest::purpose_text()".to_string(),
sample_request.purpose_text().to_string(),
));
items.push((
"domain::UsageRequest::machine_text()".to_string(),
sample_request.machine_text(),
));
// ---- pool(★ 机制层) ----
items.push((
"pool::BUILTIN_REUSE_POLICIES // 三档".to_string(),
BUILTIN_REUSE_POLICIES
.iter()
.map(|policy| policy.chinese_text())
.collect::<Vec<&str>>()
.join(" / "),
));
items.push((
"pool::ReusePolicy::machine_text()".to_string(),
ReusePolicy::StrictPool.machine_text().to_string(),
));
items.push((
"★ pool::ReusePolicy::max_total() // 只有限量档有上限".to_string(),
format!(
"严格池={:?} 按需={:?} 限量={:?}",
ReusePolicy::StrictPool.max_total(),
ReusePolicy::GrowOnDemand.max_total(),
ReusePolicy::GrowWithLimit { max_total: 3 }.max_total()
),
));
items.push((
"★ pool::ReusePolicy::allows_creation_when_empty(已有 2 台)".to_string(),
format!(
"严格池={} 按需={} 限量(上限3)={}",
bool_text(ReusePolicy::StrictPool.allows_creation_when_empty(2)),
bool_text(ReusePolicy::GrowOnDemand.allows_creation_when_empty(2)),
bool_text(ReusePolicy::GrowWithLimit { max_total: 3 }.allows_creation_when_empty(2))
),
));
items.push((
"★ pool::ReusePolicy::can_reject() // 按需档永不拒绝".to_string(),
format!(
"严格池={} 按需={} 限量={}",
bool_text(ReusePolicy::StrictPool.can_reject()),
bool_text(ReusePolicy::GrowOnDemand.can_reject()),
bool_text(ReusePolicy::GrowWithLimit { max_total: 3 }.can_reject())
),
));
items.push((
"★ pool::PooledObject::identity_text() // 经 trait 调用,不是固有方法".to_string(),
format!(
"{} / {}",
PooledObject::identity_text(&sample_unit),
PooledObject::fingerprint_text(&sample_unit)
),
));
items.push((
"pool::ObjectPool::name() / policy()".to_string(),
format!("{} / {}", first_pool.name(), first_pool.policy().chinese_text()),
));
items.push((
"pool::ObjectPool::idle_total() / borrowed_total()".to_string(),
format!("{} / {}", first_pool.idle_total(), first_pool.borrowed_total()),
));
items.push((
"pool::ObjectPool::quarantined_total() / retired_total()".to_string(),
format!("{} / {}", first_pool.quarantined_total(), first_pool.retired_total()),
));
items.push((
"★ pool::ObjectPool::capacity_total() // 不含退役".to_string(),
first_pool.capacity_total().to_string(),
));
items.push((
"pool::ObjectPool::created_total()".to_string(),
first_pool.created_total().to_string(),
));
items.push((
"pool::ObjectPool::identity_list_sorted() // 台数".to_string(),
format!("{} 台", first_pool.identity_list_sorted().len()),
));
items.push((
"pool::ObjectPool::fingerprint_list_sorted() // 条数".to_string(),
format!("{} 条", first_pool.fingerprint_list_sorted().len()),
));
items.push((
"pool::ObjectPool::objects_sorted() // 条数".to_string(),
format!("{} 台", first_pool.objects_sorted().len()),
));
items.push((
"pool::ObjectPool::ledger().action_total()".to_string(),
first_pool.ledger().action_total().to_string(),
));
items.push((
"pool::PoolLedger::hit_total() / creation_total()".to_string(),
format!(
"{} / {}",
first_pool.ledger().hit_total(),
first_pool.ledger().creation_total()
),
));
items.push((
"pool::PoolLedger::acquisition_total() / rejection_total()".to_string(),
format!(
"{} / {}",
first_pool.ledger().acquisition_total(),
first_pool.ledger().rejection_total()
),
));
items.push((
"pool::PoolLedger::release_total() / reuse_total()".to_string(),
format!(
"{} / {}",
first_pool.ledger().release_total(),
first_pool.ledger().reuse_total()
),
));
items.push((
"pool::PoolLedger::quarantine_total() / foreign_return_total()".to_string(),
format!(
"{} / {}",
first_pool.ledger().quarantine_total(),
first_pool.ledger().foreign_return_total()
),
));
items.push((
"pool::PoolLedger::prewarm_total()".to_string(),
first_pool.ledger().prewarm_total().to_string(),
));
items.push((
"★ pool::PoolSnapshot::acquisition_identity_gap() // 恒等式一".to_string(),
first_pool.snapshot().acquisition_identity_gap().to_string(),
));
items.push((
"★ pool::PoolSnapshot::release_identity_gap() // 恒等式二".to_string(),
first_pool.snapshot().release_identity_gap().to_string(),
));
items.push((
"pool::PoolSnapshot::reuse_ratio_basis_points()".to_string(),
format!(
"{} 万分点",
first_pool.snapshot().reuse_ratio_basis_points()
),
));
items.push((
"pool::ReleaseOutcome::machine_text() // 三种结局".to_string(),
[
ReleaseOutcome::ReturnedToPool { identity: "X".to_string() }.machine_text(),
ReleaseOutcome::Quarantined { identity: "X".to_string(), reason: "r".to_string() }
.machine_text(),
ReleaseOutcome::Rejected { identity: "X".to_string() }.machine_text(),
]
.join(" / "),
));
items.push((
"★ pool::ReleaseOutcome::counts_as_reuse() // 只有回池算数".to_string(),
format!(
"回池={} 隔离={} 异物={}",
bool_text(ReleaseOutcome::ReturnedToPool { identity: "X".to_string() }.counts_as_reuse()),
bool_text(
ReleaseOutcome::Quarantined { identity: "X".to_string(), reason: "r".to_string() }
.counts_as_reuse()
),
bool_text(ReleaseOutcome::Rejected { identity: "X".to_string() }.counts_as_reuse())
),
));
let sample_release: ReleaseOutcome =
ReleaseOutcome::Quarantined { identity: "X-0001".to_string(), reason: "校准已过期".to_string() };
items.push((
"pool::ReleaseOutcome::chinese_text() / identity()".to_string(),
format!("{} / {}", sample_release.chinese_text(), sample_release.identity()),
));
items.push((
"pool::PoolAuditRule::BUILTIN // 六条规则编码".to_string(),
crate::pool::PoolAuditRule::BUILTIN
.iter()
.map(|rule| rule.code())
.collect::<Vec<&str>>()
.join(" / "),
));
items.push((
"★ pool::PoolAuditRule::known_failing_input_text() // EFFECTIVE".to_string(),
crate::pool::PoolAuditRule::PoolIsEffective
.known_failing_input_text()
.to_string(),
));
let sample_check: Vec<crate::pool::PoolAuditCheck> =
audit_pool(&observe_pool(first_pool));
items.push((
"pool::audit_pool() // 首条:编码 / 符合 / 适用".to_string(),
match sample_check.first() {
Some(check) => format!(
"{} {}/{} 通过={}",
check.rule().code(),
check.conforming_total(),
check.applicable_total(),
bool_text(check.passed())
),
None => "无".to_string(),
},
));
items.push((
"pool::PoolObservation::capacity_gap() // 天平站".to_string(),
observe_pool(first_pool).capacity_gap().to_string(),
));
items.push((
"pool::PoolObservation::balance_gap() // 无泄漏判据".to_string(),
observe_pool(first_pool).balance_gap().to_string(),
));
items.push((
"pool::PoolObservation::reuse_ratio_basis_points()".to_string(),
format!("{} 万分点", observe_pool(first_pool).reuse_ratio_basis_points()),
));
// ---- client ----
items.push((
"client::instrument_factory(MODEL_BALANCE, 出厂月)(0)".to_string(),
instrument_factory(MODEL_BALANCE, MANUFACTURE_MONTH)(0).fingerprint_text(),
));
items.push((
"★ client::return_verifier(当月)(过期仪器)".to_string(),
match return_verifier(DEMO_MONTH)(&InstrumentUnit::new(
"OLD-0001",
MODEL_GOLD_TESTER,
MonthKey::new(2026, 7),
)) {
Ok(()) => "可以回池".to_string(),
Err(reason) => format!("不能回池:{}", reason),
},
));
items.push((
"★ client::should_retire(借满上限的仪器)".to_string(),
{
let mut tired: InstrumentUnit =
InstrumentUnit::new("TIRED-0001", MODEL_BALANCE, MonthKey::new(2027, 1));
for _ in 0..RETIREMENT_USAGE_LIMIT {
tired.note_borrowed();
tired.clear_for_reuse();
}
bool_text(should_retire(&tired)).to_string()
},
));
items.push((
"client::InstrumentStation::model().code()".to_string(),
first_station.model().code().to_string(),
));
items.push((
"client::InstrumentStation::current_month()".to_string(),
first_station.current_month().chinese_text(),
));
items.push((
"client::InstrumentStation::name()".to_string(),
first_station.name(),
));
items.push((
"client::InstrumentStation::peak_borrowed_total()".to_string(),
first_station.peak_borrowed_total().to_string(),
));
items.push((
"client::InstrumentStation::acquisition_cost_spent()".to_string(),
first_station.acquisition_cost_spent().chinese_text(),
));
items.push((
"client::InstrumentStation::service_cost_spent()".to_string(),
first_station.service_cost_spent().chinese_text(),
));
items.push((
"client::InstrumentStation::service_total()".to_string(),
first_station.service_total().to_string(),
));
items.push((
"client::InstrumentStation::service_records() // 条数".to_string(),
format!("{} 条", first_station.service_records().len()),
));
items.push((
"client::InstrumentStation::observation().pool_name".to_string(),
first_station.observation().pool_name,
));
items.push((
"client::InstrumentStation::holder_list_sorted() // 条数".to_string(),
format!("{} 条", first_station.holder_list_sorted().len()),
));
items.push((
"client::UsageScenario::name()".to_string(),
builtin_scenario().name().to_string(),
));
items.push((
"client::UsageScenario::request_total()".to_string(),
builtin_scenario().request_total().to_string(),
));
items.push((
"client::UsageScenario::models() // 型号数".to_string(),
format!("{} 种", builtin_scenario().models().len()),
));
items.push((
"client::UsageScenario::request_total_for(天平)".to_string(),
builtin_scenario().request_total_for(&MODEL_BALANCE).to_string(),
));
items.push((
"client::UsageStep::request().purpose_text() / hold_steps()".to_string(),
match builtin_scenario().steps().first() {
Some(step) => format!("{} / {}", step.request().purpose_text(), step.hold_steps()),
None => "无".to_string(),
},
));
items.push((
"client::PoolSession::borrow_records() // 天平站笔数".to_string(),
format!("{} 笔", strict_sessions[0].borrow_records().len()),
));
items.push((
"client::PoolSession::granted_total() / rejected_total()".to_string(),
format!(
"{} / {}",
strict_sessions[0].granted_total(),
strict_sessions[0].rejected_total()
),
));
items.push((
"client::PoolSession::returned_to_pool_total() / quarantined_total()".to_string(),
format!(
"{} / {}",
strict_sessions[0].returned_to_pool_total(),
strict_sessions[0].quarantined_total()
),
));
items.push((
"client::BorrowRecord::identity() / outcome_text() // 首笔".to_string(),
match strict_sessions[0].borrow_records().first() {
Some(record) => format!("{} / {}", record.identity(), record.outcome_text()),
None => "无".to_string(),
},
));
items.push((
"★ client::BorrowRecord::saved_one_purchase() // 首笔是否省下一次采购".to_string(),
match strict_sessions[0].borrow_records().first() {
Some(record) => bool_text(record.saved_one_purchase()).to_string(),
None => "无".to_string(),
},
));
items.push((
"client::BorrowRecord::usage_count_at_acquisition() // 首笔".to_string(),
match strict_sessions[0].borrow_records().first() {
Some(record) => record.usage_count_at_acquisition().to_string(),
None => "无".to_string(),
},
));
let evidence: crate::client::IdentityEvidence = strict_sessions[0].identity_evidence();
items.push((
"★ client::IdentityEvidence::reuse_happened()".to_string(),
bool_text(evidence.reuse_happened()).to_string(),
));
items.push((
"client::IdentityEvidence::usage_per_object_basis_points()".to_string(),
format!("{} 万分点", evidence.usage_per_object_basis_points()),
));
// ---- analysis ----
items.push((
"analysis::InstrumentUnitView::serial_number() // 首台".to_string(),
match all_views.first() {
Some(view) => view.serial_number().to_string(),
None => "无".to_string(),
},
));
items.push((
"analysis::InstrumentUnitView::state_text() / usage_count()".to_string(),
match all_views.first() {
Some(view) => format!("{} / {}", view.state_text(), view.usage_count()),
None => "无".to_string(),
},
));
items.push((
"analysis::InstrumentUnitView::holder_text() / calibration_due_text()".to_string(),
match all_views.first() {
Some(view) => format!("{} / {}", view.holder_text(), view.calibration_due_text()),
None => "无".to_string(),
},
));
items.push((
"analysis::InstrumentUnitView::acquisition_cost_text() / fingerprint()".to_string(),
match all_views.first() {
Some(view) => format!("{} / {}", view.acquisition_cost_text(), view.fingerprint()),
None => "无".to_string(),
},
));
items.push((
"analysis::holder_display_text(Some(门店))".to_string(),
holder_display_text(Some(STORE_HK_01)),
));
items.push((
"analysis::PolicyProbe::policy_name() // 首档".to_string(),
match probes.first() {
Some(probe) => probe.policy_name().to_string(),
None => "无".to_string(),
},
));
items.push((
"★ analysis::PolicyProbe::risk_text() // 首档".to_string(),
match probes.first() {
Some(probe) => probe.risk_text(),
None => "无".to_string(),
},
));
items.push((
"analysis::PolicyProbe::reuse_ratio_basis_points() // 首档".to_string(),
match probes.first() {
Some(probe) => format!("{} 万分点", probe.reuse_ratio_basis_points()),
None => "无".to_string(),
},
));
items.push((
"analysis::PolicyProbe::can_reject() // 首档".to_string(),
match probes.first() {
Some(probe) => bool_text(probe.can_reject()).to_string(),
None => "无".to_string(),
},
));
items.push((
"analysis::CapacityProbe::capacity_total() / capacity_gap()".to_string(),
format!(
"{} / {}",
CapacityProbe::from_session(&strict_sessions[0], 4).capacity_total(),
CapacityProbe::from_session(&strict_sessions[0], 4).capacity_gap()
),
));
items.push((
"analysis::lifecycle_statistics() // 档数".to_string(),
format!("{} 档", lifecycle_statistics(all_views).len()),
));
items.push((
"analysis::LifecycleProbe::in_pool() // 空闲档".to_string(),
match lifecycle_statistics(all_views).first() {
Some(probe) => format!("{} 在池={}", probe.state_name(), bool_text(probe.in_pool())),
None => "无".to_string(),
},
));
items.push((
"analysis::service_statistics() // 各类条数".to_string(),
service_statistics(all_service_records)
.iter()
.map(|(kind, total)| format!("{} {}", kind.chinese_text(), total))
.collect::<Vec<String>>()
.join("、"),
));
items.push((
"analysis::AuditCheckView::rule_code() // 首条".to_string(),
match audit_check_views(&audit_pool(&observe_pool(first_pool))).first() {
Some(view) => format!("{} 通过={}", view.rule_code(), bool_text(view.passed())),
None => "无".to_string(),
},
));
items.push((
"analysis::AuditCheckView::known_failing_input_text() // 首条".to_string(),
match audit_check_views(&audit_pool(&observe_pool(first_pool))).first() {
Some(view) => view.known_failing_input_text().to_string(),
None => "无".to_string(),
},
));
items.push((
"analysis::LedgerSnapshotView::row_total()".to_string(),
snapshot_view.row_total().to_string(),
));
items.push((
"analysis::PoolValueReport::saved_purchase_total()".to_string(),
format!("{} 台", value_report.saved_purchase_total()),
));
items.push((
"analysis::PoolValueReport::saved_money()".to_string(),
value_report.saved_money().chinese_text(),
));
items.push((
"analysis::PoolValueReport::saved_ratio_basis_points()".to_string(),
format!("{} 万分点", value_report.saved_ratio_basis_points()),
));
items.push((
"analysis::PoolValueReport::summary_text()".to_string(),
value_report.summary_text(),
));
items.push((
"analysis::CheckLine::verdict_text(false)".to_string(),
CheckLine::new("演示", "期望", "实测", false).verdict_text().to_string(),
));
items.push((
"analysis::CheckReport::line_total() // 空报告".to_string(),
CheckReport::new().line_total().to_string(),
));
// ---- app ----
items.push((
"app::DOCUMENT_WIDTH".to_string(),
DOCUMENT_WIDTH.to_string(),
));
items.push((
"app::render_pair_table(...) // 返回行数".to_string(),
render_pair_table(
"接口清点用的两列表",
&[("演示项".to_string(), "演示值".to_string())],
&["演示注解"],
)
.len()
.to_string(),
));
items.push((
"★ 机制层独立可用性:新建一个池并读它的容量".to_string(),
{
let mut probe_pool: ObjectPool<InstrumentUnit> = ObjectPool::new(
"接口清点用的池",
ReusePolicy::StrictPool,
instrument_factory(MODEL_GEM_MICROSCOPE, MANUFACTURE_MONTH),
return_verifier(DEMO_MONTH),
);
probe_pool.prewarm(2);
format!(
"{} 空闲 {} 台 / 容量 {} 台 / 已造 {} 台",
probe_pool.name(),
probe_pool.idle_total(),
probe_pool.capacity_total(),
probe_pool.created_total()
)
},
));
items.push((
"★ 身份证据:`Rc::ptr_eq` 比对(只报是否,绝不打印地址)".to_string(),
{
let left: Rc<RefCell<InstrumentUnit>> =
Rc::new(RefCell::new(InstrumentUnit::new("SAME-0001", MODEL_BALANCE, DEMO_MONTH)));
let cloned: Rc<RefCell<InstrumentUnit>> = Rc::clone(&left);
let other: Rc<RefCell<InstrumentUnit>> =
Rc::new(RefCell::new(InstrumentUnit::new("SAME-0001", MODEL_BALANCE, DEMO_MONTH)));
format!(
"克隆={} 同号新建={}",
bool_text(Rc::ptr_eq(&left, &cloned)),
bool_text(Rc::ptr_eq(&left, &other))
)
},
));
items.push((
"app::section_header(\"演示\", 20)".to_string(),
section_header("演示", 20),
));
items.push((
"app::note_lines(...) // 返回行数".to_string(),
note_lines("一行注解", DOCUMENT_WIDTH).len().to_string(),
));
items.push((
"app::table_rule(10)".to_string(),
table_rule(10),
));
items.push((
"★ app::render_cell(不允许省略的列, 内容)".to_string(),
format!(
"[{}]",
crate::app::render_cell(
&crate::app::TableColumn::new("列", 8),
"内容"
)
),
));
items.push((
"app::TableColumn::header() / width() / align() / is_elidable()".to_string(),
{
let column = crate::app::TableColumn::numeric("数字列", 8);
format!(
"{} / {} / 右对齐={} / 可省略={}",
column.header(),
column.width(),
bool_text(column.align() == crate::app::ColumnAlign::Right),
bool_text(column.is_elidable())
)
},
));
items.push((
"app::Table::new(...).with_row(...).with_note(...) // 列数/行数/总宽".to_string(),
{
let table = crate::app::Table::new(
"演示",
vec![
crate::app::TableColumn::new("甲", 6),
crate::app::TableColumn::new("乙", 6),
],
)
.with_row(vec!["1".to_string(), "2".to_string()])
.with_note("注解");
format!(
"{} 列 / {} 行 / 总宽 {}",
table.column_count(),
table.row_count(),
table.total_width()
)
},
));
items.push((
"app::bool_text(true) / byte_text(1234567)".to_string(),
format!("{} / {}", bool_text(true), byte_text(1_234_567)),
));
items.push((
"app::basis_points_text(6667) / gram_display_text(1500)".to_string(),
format!("{} / {}", basis_points_text(6_667), gram_display_text(1_500)),
));
items.push((
"app::InventoryRowView::serial_number() // 首台".to_string(),
match inventory_rows.first() {
Some(row) => row.serial_number().to_string(),
None => "无".to_string(),
},
));
items.push((
"app::ExtensionRowView::item() / evidence()".to_string(),
{
let row = ExtensionRowView::new("演示项", "1", "2", "演示证据");
format!("{} / {} / {}", row.item(), row.extended_value(), row.evidence())
},
));
items.push((
"app::IdentityPairView::identical() // 同指纹".to_string(),
bool_text(IdentityPairView::new("X", "A|1", "A|1").identical()).to_string(),
));
items.push((
"app::CapacityRowView::station_name() / capacity_gap()".to_string(),
{
let row = CapacityRowView::new(
"演示站",
CapacityProbe::from_session(&strict_sessions[0], 6),
);
format!("{} / {}", row.station_name(), row.capacity_gap())
},
));
items.push((
"★ 三档对照:按需扩容档的新建次数(不池化要买几台)".to_string(),
grow_sessions
.iter()
.map(|session| session.station().pool().created_total().to_string())
.collect::<Vec<String>>()
.join(" + "),
));
items.push((
"★ 三档对照:严格池档的新建次数(池化实际买了几台)".to_string(),
strict_sessions
.iter()
.map(|session| session.station().pool().created_total().to_string())
.collect::<Vec<String>>()
.join(" + "),
));
items
}
/// 体检里那些**不属于任何一组既有检查**的条目。
///
/// ★ 含两条「审计也要被审计」的检查:
/// 负向用例必须真的红,否则那两条规则其实只是没被跑过的注释。
fn health_check_lines(
probes: &[PolicyProbe],
normal_checks: &[AuditCheckView],
neglect_checks: &[AuditCheckView],
foreign_checks: &[AuditCheckView],
identity_pairs: &[IdentityPairView],
builtin: &UsageScenario,
extended: &UsageScenario,
retired_total: usize,
extended_retired_total: usize,
foreign_note: &str,
) -> Vec<CheckLine> {
let mut lines: Vec<CheckLine> = Vec::new();
// ① ★ 版面:宽度模型自检必须全过。
let width_failures: usize = self_check_width_model().iter().filter(|(_, ok, _)| !ok).count();
lines.push(CheckLine::new(
"版面:显示宽度模型自检",
"0 项未通过(已知会失败:把 `─` 当成宽字符)",
format!("{} 项未通过", width_failures).as_str(),
width_failures == 0,
));
// ② ★ 审计也要被审计:负向①必须让 `CAPACITY` 红。
let capacity_failed: bool = neglect_checks
.iter()
.any(|check| check.rule_code() == "CAPACITY" && !check.passed());
lines.push(CheckLine::new(
"★ 审计也要被审计:负向①必须让 CAPACITY 变红",
"未通过(已知会失败:把「取走隔离对象」改成立刻退役)",
if capacity_failed { "未通过(正确)" } else { "通过(说明规则没在跑)" },
capacity_failed,
));
// ③ ★ 审计也要被审计:负向②必须让 `FOREIGN` 红。
let foreign_failed: bool = foreign_checks
.iter()
.any(|check| check.rule_code() == "FOREIGN" && !check.passed());
lines.push(CheckLine::new(
"★ 审计也要被审计:负向②必须让 FOREIGN 变红",
format!("未通过({};已知会失败:归还时不查指针)", foreign_note).as_str(),
if foreign_failed { "未通过(正确)" } else { "通过(说明规则没在跑)" },
foreign_failed,
));
// ④ 正常用例必须全绿。
let normal_failures: usize = normal_checks.iter().filter(|check| !check.passed()).count();
lines.push(CheckLine::new(
"契约审计:正常用例全部通过",
format!("0 条未通过(共 {} 条;已知会失败:任一规则被改坏)", normal_checks.len()).as_str(),
format!("{} 条未通过", normal_failures).as_str(),
normal_failures == 0,
));
// ⑤ 恒等:两条独立路径逐台指纹一致。
let mismatches: usize = identity_pairs.iter().filter(|pair| !pair.identical()).count();
lines.push(CheckLine::new(
"恒等:同一份场景跑两遍,逐台指纹一致",
format!("0 处不一致(共 {} 台;已知会失败:某处依赖 HashMap 迭代顺序)", identity_pairs.len())
.as_str(),
format!("{} 处不一致", mismatches).as_str(),
mismatches == 0,
));
// ⑥ 扩展:型号数 +1、门店数 +1、申请笔数变多。
lines.push(CheckLine::new(
"扩展:型号种数 == 内置 + 1",
"多 1 种(已知会失败:扩展区忘了把新型号写进场景)",
format!("{} -> {}", builtin.models().len(), extended.models().len()).as_str(),
extended.models().len() == builtin.models().len() + 1,
));
lines.push(CheckLine::new(
"扩展:申请笔数变多",
"> 内置(已知会失败:扩展区只加了型号没加申请)",
format!("{} -> {}", builtin.request_total(), extended.request_total()).as_str(),
extended.request_total() > builtin.request_total(),
));
lines.push(CheckLine::new(
"★ 扩展:退役路径真的被走到过",
"扩展后退役台数 > 0(已知会失败:把退役上限调高到场景借不到)",
format!("内置 {} 台 -> 扩展 {} 台", retired_total, extended_retired_total).as_str(),
extended_retired_total > 0,
));
// ⑦ 三档对照表非空。
lines.push(CheckLine::new(
"三档对照表有数据",
format!("{} 档(已知会失败:run_all 返回空表)", probes.len()).as_str(),
format!("{} 档", probes.len()).as_str(),
!probes.is_empty(),
));
lines
}
fn main() {
let builtin: UsageScenario = builtin_scenario();
let extended: UsageScenario = extended_scenario();
// ---------- 三档策略:同一份场景,跑三次 ----------
let strict_sessions: Vec<PoolSession> = run_all(&builtin, ReusePolicy::StrictPool);
let grow_sessions: Vec<PoolSession> = run_all(&builtin, ReusePolicy::GrowOnDemand);
let limited_sessions: Vec<PoolSession> = run_all(
&builtin,
ReusePolicy::GrowWithLimit { max_total: LIMITED_MAX_TOTAL },
);
let probes: Vec<PolicyProbe> = vec![
PolicyProbe::from_sessions("严格池(借空即拒绝)", &strict_sessions),
PolicyProbe::from_sessions("按需扩容(永不拒绝)", &grow_sessions),
PolicyProbe::from_sessions(
format!("限量扩容(上限 {} 台)", LIMITED_MAX_TOTAL).as_str(),
&limited_sessions,
),
];
// ---------- 幕一:装配总览 ----------
let overview_rows: Vec<(String, String)> = vec![
("业务当月".to_string(), DEMO_MONTH.chinese_text()),
("出厂月份(首批仪器的校准起点)".to_string(), MANUFACTURE_MONTH.chinese_text()),
("内置型号数".to_string(), format!("{} 种", builtin.models().len())),
("扩展后型号数".to_string(), format!("{} 种", extended.models().len())),
("内置门店数".to_string(), format!("{} 家", BUILTIN_STORE_CODES.len())),
("扩展后门店数".to_string(), format!("{} 家", BUILTIN_STORE_CODES.len() + 1)),
("内置申请笔数".to_string(), format!("{} 笔", builtin.request_total())),
("扩展后申请笔数".to_string(), format!("{} 笔", extended.request_total())),
("每型号预热台数".to_string(), format!("{} 台", PREWARM_TOTAL)),
("★ 累计使用次数上限(到这个数退役)".to_string(), format!("{} 次", RETIREMENT_USAGE_LIMIT)),
(
"三档策略".to_string(),
BUILTIN_REUSE_POLICIES
.iter()
.map(|policy| policy.chinese_text())
.collect::<Vec<&str>>()
.join(" / "),
),
];
print_lines(&render_overview_table(&overview_rows));
println!();
// ---------- 幕二:型号档案 ----------
print_lines(&render_model_table(&extended.models()));
println!();
// ---------- 幕三:门店清单 ----------
let mut all_stores: Vec<StoreCode> = BUILTIN_STORE_CODES.to_vec();
all_stores.push(STORE_MO_01);
print_lines(&render_store_table(
&all_stores,
"★ 澳门氹仔店是**工程外扩展**加进来的(定义在 `main.rs` 扩展区,\
不是领域层的内置常量)。门店是开放型,所以加一家店不用回领域层改 `enum`。",
));
println!();
// ---------- 幕四:服务单价 ----------
let service_price_rows: Vec<(String, String, String)> = vec![
(
"校准(一次)".to_string(),
Money::new(SERVICE_COST_CALIBRATION_MINOR_UNITS, CNY).chinese_text(),
"周期到了或已过期,校准完顺延到期月并回池".to_string(),
),
(
"退役处置(一台)".to_string(),
Money::new(SERVICE_COST_RETIREMENT_MINOR_UNITS, CNY).chinese_text(),
"累计借满上限,永久离池".to_string(),
),
];
print_lines(&render_service_price_table(&service_price_rows));
println!();
// ---------- 幕五:仪器清单(严格池这一档,全部型号) ----------
let mut inventory_rows: Vec<InventoryRowView> = Vec::new();
for session in strict_sessions.iter() {
for (_, object) in session.inventory().iter() {
let view: InstrumentUnitView = InstrumentUnitView::from_unit(&object.borrow());
inventory_rows.push(InventoryRowView::from_view(&view));
}
}
inventory_rows.sort_by(|left, right| left.serial_number().cmp(right.serial_number()));
let inventory_notes: Vec<(String, String)> = vec![
("仪器总台数".to_string(), format!("{} 台", inventory_rows.len())),
(
"★ 被借过 2 次以上的台数".to_string(),
format!("{} 台", inventory_rows.iter().filter(|row| row.usage_count > 1).count()),
),
];
print_lines(&render_inventory_table(
"幕五 · 仪器清单:逐台看复用(严格池这一档,全部型号)",
&inventory_rows,
));
let _ = inventory_notes;
println!();
// ---------- 幕六:三档策略对照 ----------
print_lines(&render_policy_table(&probes));
println!();
// ---------- 幕七:身份证据 ----------
let mut evidence_total_acquisition: usize = 0;
let mut evidence_distinct: usize = 0;
let mut evidence_in_pool: usize = 0;
let mut evidence_created: usize = 0;
let mut retired_total: usize = 0;
for session in strict_sessions.iter() {
let evidence = session.identity_evidence();
evidence_total_acquisition += evidence.acquisition_total;
evidence_distinct += evidence.distinct_object_total;
evidence_in_pool += evidence.still_in_pool_total;
evidence_created += evidence.created_total;
retired_total += session.station().observation().retired_total;
}
let evidence_rows: Vec<(String, String)> = vec![
("借出成功次数".to_string(), format!("{} 次", evidence_total_acquisition)),
(
"★ 实际用到的**不同**台数(按指针去重)".to_string(),
format!("{} 台", evidence_distinct),
),
(
"★ 其中此刻仍在池里的台数(指针比中)".to_string(),
format!("{} 台", evidence_in_pool),
),
("累计造过的台数".to_string(), format!("{} 台", evidence_created)),
("退役台数".to_string(), format!("{} 台", retired_total)),
(
"★ 复用是否成立(借出次数 > 不同台数)".to_string(),
bool_text(evidence_total_acquisition > evidence_distinct).to_string(),
),
(
"平均每台被借次数".to_string(),
format!(
"{}",
basis_points_text(if evidence_distinct == 0 {
0
} else {
((evidence_total_acquisition as i128) * 10_000 / (evidence_distinct as i128)) as i64
})
),
),
];
print_lines(&render_identity_evidence_table(
&evidence_rows,
&[
"★ 「不同台数」是按 `Rc::ptr_eq` **比指针**去重得来的,\
不是按序列号去重:序列号可以被复制(新建一个一模一样的就有同号),指针不能。",
"★ 一个池若每次归还时新建一个同号同状态的放回去,\
「复用次数」一样会 +1、一样省得下钱,但那是假复用。\
指针比对就是为了让「同一台」变成一个能算出来的是/否。",
"⚠️ 只打印「是否相等」,**绝不打印地址**:\
地址随运行变(ASLR),打印会破坏「两次运行逐字节一致」。",
],
));
println!();
// 幕七(续):两条独立路径的逐台指纹比对(同参数跑两遍)。
let second_strict: Vec<PoolSession> = run_all(&builtin, ReusePolicy::StrictPool);
let mut identity_pairs: Vec<IdentityPairView> = Vec::new();
for (index, session) in strict_sessions.iter().enumerate() {
let again: &PoolSession = &second_strict[index];
let left: Vec<(String, String)> = session.station().pool().fingerprint_list_sorted();
let right: Vec<(String, String)> = again.station().pool().fingerprint_list_sorted();
for (position, (identity, fingerprint)) in left.iter().enumerate() {
let other: String = match right.get(position) {
Some((_, other_fingerprint)) => other_fingerprint.clone(),
None => "<缺失>".to_string(),
};
identity_pairs.push(IdentityPairView::new(identity, fingerprint, other.as_str()));
}
}
print_lines(&render_identity_table(
"幕七(续)· 恒等检查:同一份场景独立跑两遍,逐台指纹比对",
&identity_pairs,
"路径一(第一次跑)",
"路径二(第二次跑)",
));
println!();
// ---------- 幕八:容量盘点 ----------
let mut capacity_probes: Vec<CapacityProbe> = Vec::new();
let mut capacity_rows: Vec<CapacityRowView> = Vec::new();
for session in strict_sessions.iter() {
let name: String = session.station().name();
let probe: CapacityProbe = CapacityProbe::from_session(session, name.len());
capacity_probes.push(probe);
capacity_rows.push(CapacityRowView::new(name.as_str(), probe));
}
print_lines(&render_capacity_table(
"幕八 · 容量盘点:空闲 + 在借 + 隔离 == 已造 − 已退役(严格池这一档)",
&capacity_rows,
));
println!();
// ---------- 幕九:生命周期与服务记录 ----------
let mut all_views: Vec<InstrumentUnitView> = Vec::new();
let mut all_service_records: Vec<ServiceRecord> = Vec::new();
let mut quarantined_total: usize = 0;
for session in strict_sessions.iter() {
for (_, object) in session.inventory().iter() {
all_views.push(InstrumentUnitView::from_unit(&object.borrow()));
}
all_service_records.extend(session.service_records().to_vec());
quarantined_total += session.quarantined_total();
}
let lifecycle_probes = lifecycle_statistics(&all_views);
print_lines(&render_lifecycle_table(
"幕九 · 生命周期分布:每一台此刻在哪一档状态上",
&lifecycle_probes,
));
println!();
print_lines(&render_service_record_table(
"幕九(续)· 服务记录:归还校验没过的那些都去哪了",
&all_service_records,
));
println!();
// ---------- 幕十:契约审计(正常用例 + 两个已知会失败的输入) ----------
let mut normal_checks: Vec<AuditCheckView> = Vec::new();
for session in strict_sessions.iter() {
let observation = observe_pool(session.station().pool());
normal_checks.extend(audit_check_views(&audit_pool(&observation)));
}
print_lines(&render_audit_table(
"幕十 · 契约审计:对象池的六条不变量(严格池这一档,逐座站)",
&normal_checks,
));
println!();
// 负向①:★ 取走隔离对象后既不送回也不退役 → `CAPACITY` 必然失败。
let mut neglect_station: InstrumentStation = InstrumentStation::new(
MODEL_GOLD_TESTER,
ReusePolicy::GrowOnDemand,
MANUFACTURE_MONTH,
DEMO_MONTH,
PREWARM_TOTAL,
);
let neglect_request: UsageRequest =
UsageRequest::new(STORE_CN_01, MODEL_GOLD_TESTER, DEMO_MONTH, "成色复检");
if let Some(handle) = neglect_station.borrow_for(&neglect_request) {
let _outcome: ReleaseOutcome = neglect_station.return_handle(handle);
// ★ 刻意取走隔离对象之后**既不送回也不退役**:这就是 `CAPACITY` 的已知失败输入。
let _pending = neglect_station.take_quarantined();
}
let neglect_checks: Vec<AuditCheckView> =
audit_check_views(&audit_pool(&observe_pool(neglect_station.pool())));
print_lines(&render_audit_table(
"幕十(续)· 已知会失败的输入之一:取走隔离对象后既不送回也不退役",
&neglect_checks,
));
println!();
// 负向②:★ 拿着别处来的对象来还 → `FOREIGN` 必然失败。
let mut station_a: InstrumentStation = InstrumentStation::new(
MODEL_BALANCE,
ReusePolicy::GrowOnDemand,
MANUFACTURE_MONTH,
DEMO_MONTH,
PREWARM_TOTAL,
);
let mut station_b: InstrumentStation = InstrumentStation::new(
MODEL_BALANCE,
ReusePolicy::GrowOnDemand,
MANUFACTURE_MONTH,
DEMO_MONTH,
PREWARM_TOTAL,
);
let foreign_request: UsageRequest =
UsageRequest::new(STORE_CN_01, MODEL_BALANCE, DEMO_MONTH, "黄金回购称重");
let foreign_outcome_text: String = match station_a.borrow_for(&foreign_request) {
None => "没能借出,无法演示".to_string(),
Some(handle) => station_b.return_handle(handle).chinese_text().to_string(),
};
let foreign_checks: Vec<AuditCheckView> =
audit_check_views(&audit_pool(&observe_pool(station_b.pool())));
print_lines(&render_audit_table(
"幕十(续二)· 已知会失败的输入之二:拿着另一座池的仪器来还",
&foreign_checks,
));
println!();
let foreign_note: String = format!(
"负向②把 A 池借出的凭证还给了 B 池,B 池的归还结局是:{}",
foreign_outcome_text
);
// ---------- 幕十一:价值量化 ----------
let mut created_with_pool: usize = 0;
let mut created_without_pool: usize = 0;
let mut saved_count: i64 = 0;
let mut saved_money: i64 = 0;
let mut hit_total: usize = 0;
let mut reused_total: usize = 0;
let mut peak_total: usize = 0;
let mut service_cost: i64 = 0;
let mut granted_total: usize = 0;
let mut rejected_total: usize = 0;
for (index, session) in strict_sessions.iter().enumerate() {
let grow: &PoolSession = &grow_sessions[index];
let with_pool: i64 = session.station().pool().created_total() as i64;
let without_pool: i64 = grow.station().pool().created_total() as i64;
let model: InstrumentModel = session.station().model();
created_with_pool += with_pool as usize;
created_without_pool += without_pool as usize;
saved_count += without_pool - with_pool;
saved_money += (without_pool - with_pool) * model.acquisition_cost().minor_units();
hit_total += session.station().observation().hit_total;
reused_total += session.returned_to_pool_total();
peak_total += session.station().peak_borrowed_total();
service_cost += session.station().service_cost_spent().minor_units();
granted_total += session.granted_total();
rejected_total += session.rejected_total();
}
let value_report: PoolValueReport = PoolValueReport::new(
builtin.request_total(),
granted_total,
rejected_total,
created_with_pool,
created_without_pool,
hit_total,
reused_total,
peak_total,
saved_count,
saved_money,
service_cost,
);
print_lines(&render_value_table(&value_report));
println!();
// 幕十一(续):台账快照(**时刻写清楚**)。
let snapshot_view: LedgerSnapshotView =
LedgerSnapshotView::from_snapshot(&strict_sessions[0].station().pool().snapshot());
print_lines(&render_ledger_snapshot_table(
&snapshot_view,
format!("天平站,内置场景跑完之后({})", DEMO_MONTH.chinese_text()).as_str(),
));
println!();
// ---------- 幕十二:工程外扩展对照 ----------
let extended_strict: Vec<PoolSession> = run_all(&extended, ReusePolicy::StrictPool);
let mut extended_unit_total: usize = 0;
let mut extended_service_total: usize = 0;
let mut extended_retired_total: usize = 0;
let mut extended_granted_total: usize = 0;
for session in extended_strict.iter() {
extended_unit_total += session.inventory().len();
extended_service_total += session.service_records().len();
extended_retired_total += session.station().observation().retired_total;
extended_granted_total += session.granted_total();
}
let mut builtin_unit_total: usize = 0;
for session in strict_sessions.iter() {
builtin_unit_total += session.inventory().len();
}
let extension_rows: Vec<ExtensionRowView> = vec![
ExtensionRowView::new(
"型号种数",
format!("{}", builtin.models().len()).as_str(),
format!("{}", extended.models().len()).as_str(),
"幕二型号档案多 1 行(激光打标机)",
),
ExtensionRowView::new(
"门店家数",
format!("{}", BUILTIN_STORE_CODES.len()).as_str(),
format!("{}", all_stores.len()).as_str(),
"幕三门店清单多 1 行(澳门氹仔店)",
),
ExtensionRowView::new(
"申请笔数",
format!("{}", builtin.request_total()).as_str(),
format!("{}", extended.request_total()).as_str(),
"同一份 `run_all` 跑出来的,幕六对照表数字随之变化",
),
ExtensionRowView::new(
"仪器总台数(严格池)",
format!("{}", builtin_unit_total).as_str(),
format!("{}", extended_unit_total).as_str(),
"幕五仪器清单多出新型号的台数",
),
ExtensionRowView::new(
"服务记录条数",
format!("{}", all_service_records.len()).as_str(),
format!("{}", extended_service_total).as_str(),
"幕九(续)服务记录表多出新型号与新增退役的记录",
),
ExtensionRowView::new(
"★ 退役台数",
format!("{}", retired_total).as_str(),
format!("{}", extended_retired_total).as_str(),
"新增的密集申请把天平借到了退役上限,幕九状态表「已退役」从 0 变 1",
),
ExtensionRowView::new(
"借到笔数(严格池)",
format!("{}", granted_total).as_str(),
format!("{}", extended_granted_total).as_str(),
"幕六对照表的「借到」列随之变化",
),
ExtensionRowView::new(
"★ 改动既有分层文件数",
"0",
"0",
"新增型号与门店由 `MODEL_LASER_MARKER` / `STORE_MO_01` 两个常量引入,\
二者都定义在 `main.rs` 扩展区,可用 grep 核对",
),
];
print_lines(&render_extension_table(
"幕十二 · 工程外扩展对照:新增型号 / 新增门店 / 新增申请",
&extension_rows,
));
println!();
// ---------- 幕十三:对外只读接口清点 ----------
let interface_rows: Vec<(String, String)> = interface_items(
&strict_sessions,
&grow_sessions,
&value_report,
&snapshot_view,
&probes,
&all_views,
&all_service_records,
&inventory_rows,
);
print_lines(&render_interface_table(
format!(
"幕十三 · 对外只读接口清点:{} 个接口逐个真调(不是罗列)",
interface_rows.len()
)
.as_str(),
&interface_rows,
));
println!();
// ---------- 幕十四:体检 ----------
let mut report: CheckReport = CheckReport::new();
for line in policy_checks(&probes) {
report.push(line);
}
for line in capacity_checks(&capacity_probes) {
report.push(line);
}
let evidence = crate::client::IdentityEvidence {
acquisition_total: evidence_total_acquisition,
distinct_object_total: evidence_distinct,
still_in_pool_total: evidence_in_pool,
created_total: evidence_created,
};
for line in identity_checks(&evidence, retired_total) {
report.push(line);
}
for line in lifecycle_checks(
&lifecycle_probes,
&all_views,
&all_service_records,
quarantined_total,
) {
report.push(line);
}
for line in value_report.self_check() {
report.push(line);
}
for line in health_check_lines(
&probes,
&normal_checks,
&neglect_checks,
&foreign_checks,
&identity_pairs,
&builtin,
&extended,
retired_total,
extended_retired_total,
foreign_note.as_str(),
) {
report.push(line);
}
print_lines(&render_check_table("幕十四 · 体检汇总:这一池子有没有被用坏、有没有真的起效", &report));
}
/// 对场景里的**每个型号**各跑一次会话(一个型号一座站)。
///
/// ★ 三档策略共用这一个函数:
/// 若每档各写一份遍历,「同一份申请序列」这件事就没人能保证。
fn run_all(scenario: &UsageScenario, policy: ReusePolicy) -> Vec<PoolSession> {
scenario
.models()
.iter()
.map(|model| {
PoolSession::run(
*model,
policy,
scenario.month(),
scenario.manufacture_month(),
scenario.steps(),
PREWARM_TOTAL,
)
})
.collect()
}