rust
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : borg_audit.rs
//! ★ 契约审计:「句柄共享成功」不等于「共享正确」。
//!
//! ## 四条规则
//!
//! | 规则 | 核对什么 | 为什么必须单独核 |
//! |---|---|---|
//! | `HANDLE_COUNT_CONSERVED` | 句柄条数 == 期望 | 少一个说明有人没挂上,多一个说明有人重复挂 |
//! | `SINGLE_SHARED_STATE` | 所有句柄指向同一份状态 | 博格的定义;不成立就不是博格 |
//! | `HANDLES_ARE_DISTINCT` | 句柄两两不是同一个对象 | ★ 若这条不成立,那是**单例**不是博格 |
//! | `CONTENT_CONSISTENT` | 所有句柄读到相同内容指纹 | 状态共享了但内容被各自改坏时,只有这条例外能抓出来 |
//!
//! ## ⚠️ 恒真的检查没有价值
//!
//! `HANDLES_ARE_DISTINCT` 与 `SINGLE_SHARED_STATE` 必须**同时**核:
//! 只核前者,快照组(各自一份状态)也全部通过;
//! 只核后者,单例也通过。**两条合起来才是博格的定义。**
//!
//! 此外,审计自身也必须被审计:本工程在 `main.rs` 里构造一组
//! **已知应当失败**的句柄(混入快照句柄),并断言失败条数 ≥ 1。
use crate::borg::BorgHandle;
/// 一条审计规则的执行结果。
#[derive(Debug, Clone)]
pub struct BorgAuditRule {
/// 规则编码。
rule_code: &'static str,
/// 期望(人能读)。
expectation_text: String,
/// 实测(人能读)。
actual_text: String,
/// 是否通过。
passed: bool,
}
impl BorgAuditRule {
/// 构造一条结果。
pub fn new(rule_code: &'static str, expectation_text: String, actual_text: String, passed: bool) -> Self {
BorgAuditRule { rule_code, expectation_text, actual_text, passed }
}
/// 规则编码。
pub const fn rule_code(&self) -> &'static str {
self.rule_code
}
/// 期望。
pub fn expectation_text(&self) -> &str {
&self.expectation_text
}
/// 实测。
pub fn actual_text(&self) -> &str {
&self.actual_text
}
/// 是否通过。
pub const fn passed(&self) -> bool {
self.passed
}
/// 结论文本(`通过` / `不通过`)。
pub const fn verdict_text(&self) -> &'static str {
if self.passed {
"通过"
} else {
"不通过"
}
}
}
/// 一组审计结果。
#[derive(Debug, Clone)]
pub struct BorgAudit {
/// 逐条规则的结果(**顺序固定**:与规则声明顺序同序)。
rules: Vec<BorgAuditRule>,
}
impl BorgAudit {
/// 构造。
pub fn new(rules: Vec<BorgAuditRule>) -> Self {
BorgAudit { rules }
}
/// 逐条结果。
pub fn rules(&self) -> &[BorgAuditRule] {
&self.rules
}
/// 通过条数。
pub fn passed_total(&self) -> usize {
self.rules.iter().filter(|rule| rule.passed()).count()
}
/// 不通过条数。
pub fn failed_total(&self) -> usize {
self.rules.iter().filter(|rule| !rule.passed()).count()
}
/// 是否全部通过。
pub fn all_passed(&self) -> bool {
self.failed_total() == 0
}
/// 不通过的规则编码(逗号分隔;全通过时为 `无`)。
pub fn failed_codes_text(&self) -> String {
let mut codes: Vec<&'static str> = Vec::new();
for rule in self.rules.iter() {
if !rule.passed() {
codes.push(rule.rule_code());
}
}
if codes.is_empty() {
"无".to_string()
} else {
codes.join("+")
}
}
}
/// 对一组句柄执行四条规则。
///
/// - `handles`:待核的句柄(**引用切片**:句柄本身不可复制,报表持有的是引用);
/// - `expected_handle_total`:期望的句柄条数;
/// - `fingerprint_of`:从载荷取出**内容指纹**的函数(由调用方给出,
/// 机制层不认识载荷,也就无从自己算指纹)。
pub fn audit_borg_handles<S: 'static>(
handles: &[&BorgHandle<S>],
expected_handle_total: usize,
fingerprint_of: &dyn Fn(&S) -> String,
) -> BorgAudit {
let mut rules: Vec<BorgAuditRule> = Vec::new();
// 规则一:句柄条数守恒。
rules.push(BorgAuditRule::new(
"HANDLE_COUNT_CONSERVED",
format!("{} 个", expected_handle_total),
format!("{} 个", handles.len()),
handles.len() == expected_handle_total,
));
// 规则二:所有句柄指向同一份状态。
let mut single_state: bool = true;
for index in 1..handles.len() {
if !handles[0].is_same_state(handles[index]) {
single_state = false;
break;
}
}
rules.push(BorgAuditRule::new(
"SINGLE_SHARED_STATE",
"所有句柄指向同一份状态".to_string(),
if single_state { "同一份".to_string() } else { "存在不同份".to_string() },
single_state,
));
// 规则三:句柄两两不是同一个对象(★ 这条不成立就是单例,不是博格)。
let mut all_distinct: bool = true;
for left in 0..handles.len() {
for right in (left + 1)..handles.len() {
if handles[left].is_same_handle(handles[right]) {
all_distinct = false;
}
}
}
rules.push(BorgAuditRule::new(
"HANDLES_ARE_DISTINCT",
"句柄两两不同(是博格不是单例)".to_string(),
if all_distinct { "两两不同".to_string() } else { "存在同一句柄".to_string() },
all_distinct,
));
// 规则四:所有句柄读到相同内容指纹。
let mut fingerprints: Vec<String> = Vec::new();
for handle in handles.iter() {
fingerprints.push(handle.read(|state| fingerprint_of(state)));
}
let mut content_consistent: bool = true;
for index in 1..fingerprints.len() {
if fingerprints[0] != fingerprints[index] {
content_consistent = false;
break;
}
}
let distinct_fingerprint_total: usize = {
let mut unique: Vec<String> = Vec::new();
for item in fingerprints.iter() {
if !unique.contains(item) {
unique.push(item.clone());
}
}
unique.len()
};
rules.push(BorgAuditRule::new(
"CONTENT_CONSISTENT",
"内容指纹 1 种".to_string(),
format!("内容指纹 {} 种", distinct_fingerprint_total),
content_consistent,
));
BorgAudit::new(rules)
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : shared_state.rs
//! ★ 博格机制的核心:**一份共享状态 + 任意多个句柄**。
//!
//! ## 博格与单例的分界线(本工程最重要的一句话)
//!
//! - **单例**管控的是「实例能不能有多个」:第二个 `install` 被拒,`Err`。
//! - **博格**管控的是「状态有几份」:**不限制句柄数量**,只保证状态份数 = 1。
//!
//! 因此 [`BorgSharedState::spawn_handle`] 的接收者是 `&'static self`------
//! 这不是随手写的签名,而是把「只有活到程序结束的状态才配发句柄」
//! 这条约束交给类型系统:一个临时状态根本传不进来。
//!
//! ## ★ 为什么用 `RwLock` 而不是 `RefCell`
//!
//! `static` 要求 `Sync`,而 `RefCell<T>` **不是** `Sync`(它把借用检查挪到了运行期,
//! 因此无法安全跨线程共享)。要住进 `static` 就必须用同步原语。
//! 也就是说:**在别的语言里要靠纪律保证「共享状态不会并发写坏」,
//! 在这里由类型系统保证**------这条与单例工程「不需要 `unsafe`」是同一类结论,
//! 值得写在显著位置,它是选 Rust 的理由。
//!
//! ## ⚠️ 锁中毒(poison)不静默恢复
//!
//! `RwLock` 在持锁线程 panic 后会「中毒」。本实现取回数据继续服务,
//! 但把次数记进 `poison_recoveries` 并纳入台账------
//! **恢复是可观测的数,不是悄悄吞掉**。
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::RwLock;
/// 共享状态槽位。
///
/// `S` 是**载荷类型**(本工程是 `domain::BusinessContextState`)。
/// 本类型**完全不认识** `S` 里有什么------这是机制层能复用的前提。
pub struct BorgSharedState<S> {
/// 载荷(内部可变)。
value: RwLock<S>,
/// ★ 已发出的句柄总数(`&'static self` 才能发,故它与「活着的句柄数」是两回事)。
handle_total: AtomicUsize,
/// 变更次数(**只增不减**):博格「一处改处处见」的代价凭证。
mutation_total: AtomicUsize,
/// 读取次数(**只增不减**)。
read_total: AtomicUsize,
/// 锁中毒后恢复的次数。正常为 0;**非 0 就是事故**,必须进报表。
poison_recoveries: AtomicUsize,
}
impl<S> BorgSharedState<S> {
/// 造一份共享状态。
pub fn new(initial: S) -> Self {
BorgSharedState {
value: RwLock::new(initial),
handle_total: AtomicUsize::new(0),
mutation_total: AtomicUsize::new(0),
read_total: AtomicUsize::new(0),
poison_recoveries: AtomicUsize::new(0),
}
}
/// ★ 发一个句柄。
///
/// 接收者是 `&'static self`:**只有 `'static` 的状态能发句柄**。
/// 于是「用一个临时状态造句柄、句柄比状态活得久」这个动作
/// 在类型层就不存在------不需要注释提醒,也不需要运行期检查。
pub fn spawn_handle(&'static self) -> BorgHandle<S> {
// fetch_add 返回的是「加之前的值」,故 +1 才是序号。
let sequence: u64 = self.handle_total.fetch_add(1, Ordering::SeqCst) as u64 + 1;
BorgHandle { state: self, handle_sequence: sequence }
}
/// 读一次(闭包形式,避免要求 `S: Clone`)。
///
/// ⚠️ 用闭包 `FnOnce(&S) -> T` 而不是 `fn read(&self) -> S`:
/// 后者要 `S: Clone`,等于给每一种载荷都强加一个约束,
/// 而「读一个字段」根本不需要克隆整份状态。
pub fn read<T>(&self, reader: impl FnOnce(&S) -> T) -> T {
self.read_total.fetch_add(1, Ordering::SeqCst);
let guard = match self.value.read() {
Ok(guard) => guard,
Err(poisoned) => {
// 中毒只说明「某个持写锁的线程 panic 了」,数据仍在;
// 但必须留下痕迹,否则这种事故永远不会被发现。
self.poison_recoveries.fetch_add(1, Ordering::SeqCst);
poisoned.into_inner()
}
};
reader(&guard)
}
/// 改一次(`&self` 即可,不需要 `&mut`------这正是「共享可变」的形态)。
pub fn mutate(&self, mutation: impl FnOnce(&mut S)) {
self.mutation_total.fetch_add(1, Ordering::SeqCst);
let mut guard = match self.value.write() {
Ok(guard) => guard,
Err(poisoned) => {
self.poison_recoveries.fetch_add(1, Ordering::SeqCst);
poisoned.into_inner()
}
};
mutation(&mut guard);
}
/// 已发出的句柄总数。
pub fn handle_total(&self) -> usize {
self.handle_total.load(Ordering::SeqCst)
}
/// 变更次数。
pub fn mutation_total(&self) -> usize {
self.mutation_total.load(Ordering::SeqCst)
}
/// 读取次数。
pub fn read_total(&self) -> usize {
self.read_total.load(Ordering::SeqCst)
}
/// 锁中毒恢复次数。
pub fn poison_recoveries(&self) -> usize {
self.poison_recoveries.load(Ordering::SeqCst)
}
}
/// 句柄:**指向共享状态的一个入口**。
///
/// 句柄自己几乎不占状态(一个引用 + 一个序号),
/// 这正是博格与「每台终端各持一份完整状态」的成本差别所在。
///
/// ⚠️ `S: 'static` 不是随手加的约束:句柄持有 `&'static BorgSharedState<S>`,
/// 若 `S` 里可能装着短命的引用,那么「句柄活得比数据久」就又变得可能。
/// 加上它之后,这条约束由编译器保证,不需要写进注释让人自觉。
pub struct BorgHandle<S: 'static> {
/// 指向的共享状态(`'static` 由 `spawn_handle` 的接收者保证)。
state: &'static BorgSharedState<S>,
/// 句柄序号(**只增不减**,从 1 开始)。
handle_sequence: u64,
}
impl<S: 'static> BorgHandle<S> {
/// 句柄序号。
pub const fn handle_sequence(&self) -> u64 {
self.handle_sequence
}
/// 指向的共享状态。
pub const fn shared_state(&self) -> &'static BorgSharedState<S> {
self.state
}
/// 读一次(转发到共享状态)。
pub fn read<T>(&self, reader: impl FnOnce(&S) -> T) -> T {
self.state.read(reader)
}
/// 改一次(转发到共享状态)。
///
/// ★ 注意接收者是 `&self`:改共享状态**不需要独占句柄**。
/// 这正是博格与「每个实例各持一份」最直观的差别。
pub fn mutate(&self, mutation: impl FnOnce(&mut S)) {
self.state.mutate(mutation)
}
/// ★ 判「是不是同一个句柄」:**用 `ptr::eq`,绝不打印地址**。
///
/// 地址在两次运行之间会变(ASLR),打印它会让
/// 「两次运行逐字节一致」当场失效。要印的是 `handle_sequence()`。
pub fn is_same_handle(&self, other: &BorgHandle<S>) -> bool {
std::ptr::eq(self as *const BorgHandle<S>, other as *const BorgHandle<S>)
}
/// ★★ 判「是不是指向同一份共享状态」------**博格的核心判据**。
///
/// 真博格:句柄两两**不同**(`is_same_handle` = false)
/// 但状态**相同**(`is_same_state` = true)。
/// 这一对相反的结果就是博格的定义,也是它与单例、与普通多实例的分界。
pub fn is_same_state(&self, other: &BorgHandle<S>) -> bool {
std::ptr::eq(self.state, other.state)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : state_census.rs
//! 共享状态台账:**快照**。
//!
//! ## ★ 为什么台账必须做成「快照」而不是实时读数
//!
//! 本层的计数(句柄数 / 变更数 / 读数)**只增不减**。
//! 因此「在报表渲染过程中读一次」与「在装配完成后读一次」会得到不同的数------
//! 若报表各处各自去读,`handle_total` 会在同一份输出里前后不一致,
//! 而「两次运行逐字节一致」也就守不住了。
//!
//! 对策:在**确定的时刻**取一次快照,报表一律印快照,并注明快照时刻。
//! 这是单例工程「台账在两个时刻各印一次」那条经验的直接沿用。
use crate::borg::BorgSharedState;
/// 共享状态台账快照。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateCensus {
/// 已发出的句柄总数。
handle_total: usize,
/// 报表实际持有的句柄数(**与 `handle_total` 可能不同**:
/// 中间若有人取过句柄又丢弃,计数仍在,句柄没了)。
observed_handle_total: usize,
/// 变更次数。
mutation_total: usize,
/// 读取次数。
read_total: usize,
/// 锁中毒恢复次数(正常应为 0)。
poison_recoveries: usize,
/// ★ 状态份数。
state_copy_total: usize,
}
impl StateCensus {
/// 在给定时刻取一次快照。
///
/// - `state`:共享状态;
/// - `observed_handle_total`:调用方此刻实际持有的句柄数。
///
/// ⚠️ 两个句柄数**分开记**而不是只记一个:
/// 只记「发出过几个」会掩盖「有几个还在用」,
/// 只记「还有几个」会让「曾经发过多少」无法追溯。
pub fn capture<S>(state: &BorgSharedState<S>, observed_handle_total: usize) -> Self {
StateCensus {
handle_total: state.handle_total(),
observed_handle_total,
mutation_total: state.mutation_total(),
read_total: state.read_total(),
poison_recoveries: state.poison_recoveries(),
// ★ 机制层在**结构上**保证只有一份:句柄只持有引用,
// 没有任何第二条路径能造出第二份状态。这个 1 是结构的结论,不是约定。
state_copy_total: 1,
}
}
/// 已发出的句柄总数。
pub const fn handle_total(&self) -> usize {
self.handle_total
}
/// 实际持有的句柄数。
pub const fn observed_handle_total(&self) -> usize {
self.observed_handle_total
}
/// 变更次数。
pub const fn mutation_total(&self) -> usize {
self.mutation_total
}
/// 读取次数。
pub const fn read_total(&self) -> usize {
self.read_total
}
/// 锁中毒恢复次数。
pub const fn poison_recoveries(&self) -> usize {
self.poison_recoveries
}
/// 状态份数。
pub const fn state_copy_total(&self) -> usize {
self.state_copy_total
}
/// 一句话摘要(进报表注解)。
///
/// ⚠️ 摘要里的每个数字都取自快照本身,不在此处重算------
/// 一句与相邻数字矛盾的注解,比没有注解更糟。
pub fn summary_text(&self) -> String {
format!(
"发出句柄 {} 个 / 在用 {} 个 / 状态 {} 份 / 变更 {} 次 / 读取 {} 次 / 锁中毒 {} 次",
self.handle_total,
self.observed_handle_total,
self.state_copy_total,
self.mutation_total,
self.read_total,
self.poison_recoveries
)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : context_carrier.rs
//! ★ 三种「上下文载体」的**统一接口**。
//!
//! ## 为什么要有这个 trait
//!
//! 本工程要回答的不是「博格怎么写」,而是「**怎么证明它是博格**」。
//! 要证明就得有对照:真博格 vs 快照 vs 独立实例。
//! 三者内部机制完全不同(`RwLock` 共享 / `RefCell` 私有 / `RefCell` 私有),
//! 若各写各的方法名,对照实验就得写三套分支,
//! 而「三套分支」正是这类实验最容易写出偏差的地方。
//!
//! 统一成一个 trait 之后,对照实验只有**一份代码**------
//! 于是「三种载体结果不同」必然来自载体本身,不可能来自实验代码。
//!
//! ## ★ 签名一律 `&self`(不是 `&mut self`)
//!
//! 真博格改共享状态**不需要独占句柄**,因此它的变更方法是 `&self`。
//! 若 trait 用 `&mut self`,真博格就得自己再加一层内部可变性去迁就接口------
//! 那是把接口的缺陷塞进实现里。内部可变性由**各自**按需解决。
use crate::domain::{BusinessContextState, ShiftCode};
/// 载体种类。
///
/// ★ 这是一个**封闭集合**(三种对照形态是实验设计的一部分,不会扩展),
/// 因此用 `enum` 换编译期穷尽性检查------与 `ShiftCode` / `TradeKind` 同理。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarrierKind {
/// 真博格:多个句柄,一份共享状态。
Shared,
/// ★ 快照陷阱:创建时拷一份,之后与共享状态**再无关系**。
Snapshot,
/// 独立实例:压根不认识共享状态,初值也不同。
Isolated,
}
impl CarrierKind {
/// 中文名。
pub const fn chinese_name(&self) -> &'static str {
match self {
CarrierKind::Shared => "真博格",
CarrierKind::Snapshot => "快照副本",
CarrierKind::Isolated => "独立实例",
}
}
/// 机器编码。
pub const fn code(&self) -> &'static str {
match self {
CarrierKind::Shared => "SHARED",
CarrierKind::Snapshot => "SNAPSHOT",
CarrierKind::Isolated => "ISOLATED",
}
}
/// ★ 判据:改了它之后,**别人**应不应该看到。
///
/// 这是三种载体唯一的本质差别,也是对照实验要看的那一列。
pub const fn propagates_change(&self) -> bool {
match self {
CarrierKind::Shared => true,
CarrierKind::Snapshot => false,
CarrierKind::Isolated => false,
}
}
}
/// 载体全集(**顺序固定**:真博格 → 快照 → 独立)。
pub const BUILTIN_CARRIER_KINDS: [CarrierKind; 3] =
[CarrierKind::Shared, CarrierKind::Snapshot, CarrierKind::Isolated];
/// 上下文载体的统一接口。
pub trait BusinessContextCarrier {
/// 载体种类。
fn carrier_kind(&self) -> CarrierKind;
/// 载体标签(进报表第一列)。
fn label_text(&self) -> String;
/// 读一份当前状态的**副本**。
///
/// ⚠️ 返回值必然是副本(快照)------这是免不了的:
/// Rust 不允许把借用跨过可变点传出来。
/// 关键区别在于**这个副本是不是唯一的真相**:
/// 真博格的副本只是「此刻的读数」,快照的副本就是它全部的世界。
fn current_state(&self) -> BusinessContextState;
/// 交接班:换班次 + 换操作员。
fn apply_shift_handover(&self, shift: ShiftCode, operator_name: &str);
/// 内容指纹。
fn content_fingerprint(&self) -> String;
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : isolated_context.rs
//! 独立实例:**压根不认识共享状态**的对照组。
//!
//! ## 为什么还要有这一档
//!
//! 快照档证明的是「初值相同也不等于共享」。
//! 这一档证明的是另一件事:**不共享的形态里,有一种是一眼就能看出来的**。
//!
//! 有了三档,判别才有层次:
//!
//! | | 初值是否一致 | 改后是否跟随 | 危险程度 |
//! |---|---|---|---|
//! | 真博格 | 一致 | **跟随** | ---(正确) |
//! | 快照副本 | 一致 | 不跟随 | ★★ 最高(看起来像博格) |
//! | 独立实例 | **不一致** | 不跟随 | 低(一眼可见) |
//!
//! 若只有两档(真博格 vs 独立实例),「初值一致」就会被误当成共享的充分条件,
//! 快照这档缺陷就永远不会被测出来。
use std::cell::RefCell;
use crate::client::{BusinessContextCarrier, CarrierKind};
use crate::domain::{BusinessContextState, ShiftCode};
/// 独立实例:自己持有一份完整状态。
pub struct IsolatedContext {
/// 载体标签。
label_text: String,
/// 自己那份状态(与共享状态**无任何关系**)。
own_state: RefCell<BusinessContextState>,
}
impl IsolatedContext {
/// 造一个独立实例。
///
/// ⚠️ 初值由调用方**显式给出**而不是从共享状态拷:
/// 拷贝出来的就不叫独立实例了,那是快照。
/// 本工程在装配点给它一个**不同门店**的初值,
/// 好让「初值不一致」成为这一档可核对的特征。
pub fn new(label_text: String, initial_state: BusinessContextState) -> Self {
IsolatedContext { label_text, own_state: RefCell::new(initial_state) }
}
/// 载体标签(不含任何版本号后缀------它不是从别处拷来的)。
pub fn label_text(&self) -> &str {
&self.label_text
}
}
impl BusinessContextCarrier for IsolatedContext {
fn carrier_kind(&self) -> CarrierKind {
CarrierKind::Isolated
}
fn label_text(&self) -> String {
self.label_text.clone()
}
fn current_state(&self) -> BusinessContextState {
self.own_state.borrow().clone()
}
fn apply_shift_handover(&self, shift: ShiftCode, operator_name: &str) {
self.own_state.borrow_mut().hand_over(shift, operator_name.to_string());
}
fn content_fingerprint(&self) -> String {
self.own_state.borrow().content_fingerprint()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : snapshot_context.rs
//! ★★ 快照副本:**伪装成博格的独立副本**(本工程最危险的对照)。
//!
//! ## 为什么它比「独立实例」危险得多
//!
//! 独立实例从初值起就与别人不同,一眼看得出「这不是共享」。
//! 快照副本则是**创建时拷一份**------初值与共享状态**完全相同**,
//! 于是创建完的那一刻,它的指纹、它的每个字段都与真博格一模一样。
//!
//! 只有**共享状态后来被改过**之后,两者才分叉:
//! - 真博格:跟着变;
//! - 快照:停在旧值。
//!
//! 而「停���旧值」在报表上表现为「这台机器今天的汇率是旧的」------
//! 收港币时按旧汇率折算,报表完全正常,只是钱错了。
//! **这正是静默降级最典型的形态**。
//!
//! ## ★ 判别它的唯一可靠指标
//!
//! 「初值一致」不足以证明共享,**必须看改过之后是否还一致**。
//! 因此本工程的对照实验一律跑三步:建 → 改 → 再看,
//! 只跑前两步的实验会把快照判成博格。
use std::cell::RefCell;
use crate::borg::BorgSharedState;
use crate::client::{BusinessContextCarrier, CarrierKind};
use crate::domain::{BusinessContextState, ShiftCode};
/// 快照副本:创建时拷一份,此后与共享状态再无关系。
pub struct SnapshotContext {
/// 载体标签。
label_text: String,
/// 冻结在副本里的状态(`RefCell` 是为了让 trait 的 `&self` 签名成立)。
frozen_state: RefCell<BusinessContextState>,
/// ★ 取自共享状态的那一刻的版本号(**诊断用**:它与共享状态当前版本号
/// 的差,就是「这份副本落后了多少」)。
captured_revision: u64,
}
impl SnapshotContext {
/// 从共享状态拷一份出来。
///
/// ⚠️ 刻意**不留**指向共享状态的引用:
/// 留了就不是快照了,那是「缓存 + 回源」,属于另一种设计(而且更难推理)。
pub fn capture_from(
label_text: String,
shared_state: &BorgSharedState<BusinessContextState>,
) -> Self {
let snapshot: BusinessContextState = shared_state.read(|state| state.clone());
let captured_revision: u64 = snapshot.revision();
SnapshotContext { label_text, frozen_state: RefCell::new(snapshot), captured_revision }
}
/// 载体标签。
pub fn label_text(&self) -> &str {
&self.label_text
}
/// 拷取时的版本号。
pub const fn captured_revision(&self) -> u64 {
self.captured_revision
}
}
impl BusinessContextCarrier for SnapshotContext {
fn carrier_kind(&self) -> CarrierKind {
CarrierKind::Snapshot
}
fn label_text(&self) -> String {
format!("{}(副本 rev{})", self.label_text, self.captured_revision)
}
fn current_state(&self) -> BusinessContextState {
self.frozen_state.borrow().clone()
}
fn apply_shift_handover(&self, shift: ShiftCode, operator_name: &str) {
// ★ 只改自己那份:共享状态一动不动。这正是它与真博格的全部差别。
self.frozen_state
.borrow_mut()
.hand_over(shift, operator_name.to_string());
}
fn content_fingerprint(&self) -> String {
self.frozen_state.borrow().content_fingerprint()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : store_service.rs
//! 门店服务:**营业动作的入口**。
//!
//! ## ★ 为什么这些动作要收在一个服务里,而不是让终端各自改共享状态
//!
//! 博格的共享状态**任何句柄都能改**------这是它的价值,也是它的代价。
//! 若每台终端都能随手 `mutate`,那么「今天汇率被谁改的」就无从追溯。
//!
//! 收进服务之后,改动的**动作**是有限的一组方法,
//! 而改动留下的**痕迹**(`mutation_total` 与 `revision`)仍在机制层------
//! 于是「能改」与「能查被改了几次」两件事同时成立。
//!
//! ## ⚠️ 服务持有的是「共享状态」而不是「某一台终端的句柄」
//!
//! 交接班是**全店的事**,不是某台机器的事。
//! 若服务持有某台终端的句柄,那么「一号机交接班」会变成
//! 「通过一号机的句柄改共享状态」------语义上没差别,
//! 但读者会以为这件事只与一号机有关。
use crate::borg::BorgSharedState;
use crate::domain::{BusinessContextState, CurrencyCode, ShiftCode, StoreCode};
/// 门店服务。
pub struct StoreService {
/// 共享营业上下文(`'static`:服务不拥有它,只是持有引用)。
shared_state: &'static BorgSharedState<BusinessContextState>,
}
impl StoreService {
/// 构造。
pub fn new(shared_state: &'static BorgSharedState<BusinessContextState>) -> Self {
StoreService { shared_state }
}
/// 共享状态。
pub const fn shared_state(&self) -> &'static BorgSharedState<BusinessContextState> {
self.shared_state
}
/// 交接班:换班次 + 换操作员(**一次变更**)。
pub fn hand_over(&self, shift: ShiftCode, operator_name: &str) {
self.shared_state.mutate(|state| state.hand_over(shift, operator_name.to_string()));
}
/// 刷新某币种汇率。
///
/// ★ 把「没找到这个币种」如实返回:
/// 悄悄新增一条会让「币种清单」与「汇率表」两处口径漂移,
/// 而这种漂移只在跨币种折算时才显形。
pub fn refresh_exchange_rate(&self, currency: CurrencyCode, basis_points: i64) -> bool {
let mut applied: bool = false;
self.shared_state
.mutate(|state| applied = state.refresh_exchange_rate(currency, basis_points));
applied
}
/// 切换门店(移动柜台 / 跨店支援)。
pub fn switch_store(&self, store_code: StoreCode) {
self.shared_state.mutate(|state| state.set_store_code(store_code));
}
/// 换促销档期。
pub fn advance_promotion(&self) {
self.shared_state.mutate(|state| state.advance_promotion_version());
}
/// 当前状态的一行描述(**不**取快照,是实时读数)。
///
/// ⚠️ 文档里必须写明它是实时读数:它与台账快照可能不一致,
/// 而这正是「只增不减的量要取快照」那条纪律的反面教材。
pub fn describe_text(&self) -> String {
self.shared_state.read(|state| state.compact_text())
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : terminal_context.rs
//! ★ 真博格的句柄:**一台收银终端一个句柄,共享同一份营业上下文**。
//!
//! ## ★★ 博格的边界:什么进共享状态,什么留在句柄上
//!
//! - **进共享状态**(所有终端必须看到同一个值):
//! 今天在哪家店、什么营业日、什么班次、谁值班、汇率多少、税率多少、促销第几版。
//! 一号机交接班之后,二三四号机必须立刻看到新班次------这就是博格的全部价值。
//! - **留在句柄上**(只与这台机器有关):
//! `terminal_code`。这台机器是几号机,不随共享状态变化,
//! 也不该被别的机器改掉。
//! - **压根不进本类型**(只与这台机器有关且可变):
//! 钱箱 `TerminalTill`。一号机收的钱被二号机读到,账立刻就错。
//!
//! 判据一句话:**所有句柄必须看到同一个值 → 进共享状态;
//! 只与本句柄有关 → 留在句柄上或另立类型。**
//! 判错了不是「多了个字段」,是账错。
//!
//! ## ⚠️ 严格 Borg 没有「实例私有状态」
//!
//! Python 的 Borg 把 `instance.__dict__` 指向类级 dict,
//! 因此**实例上没有任何私有属性**。本工程给句柄留了 `terminal_code`,
//! 它是**身份**不是状态------`const` 语义、创建后不可变。
//! 若哪天要给终端加「本机的现金结余」,那就不该放进这里,
//! 而该放进 `TerminalTill`(见上)。
use crate::borg::BorgHandle;
use crate::borg::BorgSharedState;
use crate::client::{BusinessContextCarrier, CarrierKind};
use crate::domain::{BusinessContextState, ShiftCode, TerminalCode};
/// 一台终端的营业上下文(**博格句柄**)。
pub struct TerminalContext {
/// 句柄身份:我是几号机(**不可变**,不进共享状态)。
terminal_code: TerminalCode,
/// 指向共享营业上下文的博格句柄。
handle: BorgHandle<BusinessContextState>,
}
impl TerminalContext {
/// 开一台终端:向共享状态要一个句柄。
pub fn new(
terminal_code: TerminalCode,
shared_state: &'static BorgSharedState<BusinessContextState>,
) -> Self {
// ★ 每调一次就多一个句柄;共享状态**不会**因为多开一台机器而多出一份。
let handle: BorgHandle<BusinessContextState> = shared_state.spawn_handle();
TerminalContext { terminal_code, handle }
}
/// 我是几号机。
pub const fn terminal_code(&self) -> TerminalCode {
self.terminal_code
}
/// 底层博格句柄(供契约审计用:审计要拿 `&[BorgHandle<S>]`)。
pub const fn handle(&self) -> &BorgHandle<BusinessContextState> {
&self.handle
}
/// 句柄序号(**可打印的身份号**;不打印地址,地址会因 ASLR 变化)。
pub const fn handle_sequence(&self) -> u64 {
self.handle.handle_sequence()
}
/// 读一次共享状态(闭包形式,不克隆整份状态)。
pub fn read<T>(&self, reader: impl FnOnce(&BusinessContextState) -> T) -> T {
self.handle.read(reader)
}
/// 改一次共享状态(`&self` 即可------这正是「共享可变」的形态)。
pub fn mutate(&self, mutation: impl FnOnce(&mut BusinessContextState)) {
self.handle.mutate(mutation)
}
}
impl BusinessContextCarrier for TerminalContext {
fn carrier_kind(&self) -> CarrierKind {
CarrierKind::Shared
}
fn label_text(&self) -> String {
format!("{}(共享句柄 #{})", self.terminal_code.code(), self.handle_sequence())
}
fn current_state(&self) -> BusinessContextState {
// 克隆一份返回:这是「此刻的读数」,共享状态仍在共享槽位里。
self.read(|state| state.clone())
}
fn apply_shift_handover(&self, shift: ShiftCode, operator_name: &str) {
self.mutate(|state| state.hand_over(shift, operator_name.to_string()));
}
fn content_fingerprint(&self) -> String {
self.read(|state| state.content_fingerprint())
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : terminal_fleet.rs
//! 终端机群:**N 台终端 + N 个钱箱 + 1 份共享营业上下文**。
//!
//! ## ★ 这一张表就是博格的成本模型
//!
//! - 终端 N 台 → **N 个句柄**(`TerminalContext`,每个只占一个引用 + 一个序号);
//! - 钱箱 N 个 → **N 份私有数据**(本来就该各有一份);
//! - 营业上下文 → **1 份**(共享)。
//!
//! 若不共享,营业上下文也要 N 份,而它的内容(汇率、班次、税率)
//! 每台机器都一样------那 N−1 份就是纯粹的内存浪费,
//! 外加一个「改汇率要通知 N 台机器」的同步问题。
//! 博格把「N 份相同内容」压成「1 份 + N 个引用」,
//! 顺带把同步问题变成**不存在**(根本没得同步)。
use crate::borg::BorgHandle;
use crate::borg::BorgSharedState;
use crate::client::TerminalContext;
use crate::domain::{BusinessContextState, TenderLine, TerminalCode, TerminalTill};
/// 机群里的一台终端:一个共享句柄 + 一个私有钱箱。
pub struct FleetMember {
/// 共享营业上下文的句柄。
context: TerminalContext,
/// 本机的钱箱(**私有**,不共享)。
till: TerminalTill,
}
impl FleetMember {
/// 构造。
pub fn new(context: TerminalContext, till: TerminalTill) -> Self {
FleetMember { context, till }
}
/// 营业上下文句柄。
pub const fn context(&self) -> &TerminalContext {
&self.context
}
/// 钱箱(只读)。
pub const fn till(&self) -> &TerminalTill {
&self.till
}
/// 钱箱(可写:记一笔)。
pub fn till_mut(&mut self) -> &mut TerminalTill {
&mut self.till
}
}
/// 终端机群。
pub struct TerminalFleet {
/// 成员清单(**顺序 = 开台顺序**,不排序------开台顺序本身是运营信息)。
members: Vec<FleetMember>,
}
impl TerminalFleet {
/// 开台:为每台终端向共享状态要一个句柄,并配一个空钱箱。
pub fn open(
shared_state: &'static BorgSharedState<BusinessContextState>,
terminal_codes: &[TerminalCode],
) -> Self {
let mut members: Vec<FleetMember> = Vec::new();
for terminal_code in terminal_codes.iter() {
// ★ 每一次 new 都是「多一个句柄」,不是「多一份状态」。
let context: TerminalContext = TerminalContext::new(*terminal_code, shared_state);
let till: TerminalTill = TerminalTill::new(*terminal_code);
members.push(FleetMember::new(context, till));
}
TerminalFleet { members }
}
/// 成员清单。
pub fn members(&self) -> &[FleetMember] {
&self.members
}
/// 台数。
pub fn terminal_total(&self) -> usize {
self.members.len()
}
/// 全部底层博格句柄(供契约审计用)。
pub fn handles(&self) -> Vec<&BorgHandle<BusinessContextState>> {
self.members.iter().map(|member| member.context().handle()).collect()
}
/// 按终端编码找成员。
pub fn member_of(&self, terminal_code: TerminalCode) -> Option<&FleetMember> {
self.members
.iter()
.find(|member| member.context().terminal_code() == terminal_code)
}
/// 按终端编码找成员(可写)。
pub fn member_mut_of(&mut self, terminal_code: TerminalCode) -> Option<&mut FleetMember> {
self.members
.iter_mut()
.find(|member| member.context().terminal_code() == terminal_code)
}
/// 记一笔到指定终端的钱箱。
///
/// ★ 返回 `bool` 而不是静默忽略:
/// 记到一台不存在的机器上若被悄悄接受,那笔钱就从报表里消失了。
pub fn record(&mut self, terminal_code: TerminalCode, line: TenderLine) -> bool {
match self.member_mut_of(terminal_code) {
Some(member) => {
member.till_mut().push(line);
true
}
None => false,
}
}
}
调用:
rust
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:博格模式 Borg 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/9 22:50
//!# User : geovindu
//!# Product : RustRover
//!# Project : BorgPattern
//!# File : main.rs
//! # 博格模式(Borg Pattern)------ 连锁珠宝门店终端共享营业上下文
//!
//! 一家门店开 5 台收银终端。它们必须看到**同一份**营业上下文
//! (今天在哪家店、什么班次、谁值班、汇率多少、税率多少、促销第几版),
//! 但每台机器有**自己的钱箱**。
//! 本工程用博格把「N 份相同的上下文」压成「1 份状态 + N 个句柄」。
//!
//! ## 严格分层结构
//!
//! ```text
//! main 装配点:造共享状态、开机群、记账、编排十一幕、工程外扩展区
//! └── app 表示层:表格引擎 + 十类报表(**不认识 borg / client**)
//! └── analysis 分析层:载体对照 / 审计视图 / 账本 / 价值量化 / 体检
//! ├── client 调用层:三种载体 + 终端机群 + 门店服务
//! │ └── borg ★ 博格机制层:共享状态槽位 + 句柄 + 契约审计
//! └── domain 领域层:营业上下文 / 钱箱 / 金额 / 汇率表 / 币种
//! └── support 支撑层:日期 / 确定性编码 / 显示宽度
//! ```
//!
//! ## 依赖方向(**实测**,由 `scripts/check_layer_dependencies.sh` 得出,非设计意图)
//!
//! ```text
//! app -> analysis domain support
//! analysis -> borg client domain (另直接依赖 borg:要读 BorgAudit / StateCensus)
//! client -> borg domain
//! domain -> support
//! borg -> none ★ 与 support 同秩,两个叶子
//! support -> none
//! ```
//!
//! ★★ 图上必须写 `borg -> none` 而不是把它画在 `domain` 下面。
//! 机制层**不认识载荷**------这是它能被整体搬进别的代码库的前提。
//! 一旦它开始引用 `domain`,上面的实测邻接表立刻变化,
//! 于是「机制层可复用」这句话从结构结论退化成口头承诺。
//!
//! 另有四条**图看不见**的红线,由 `scripts/check_layer_redlines.sh` 实测:
//! 领域层不出现 `Borg` / `RwLock` / `Handle` / `std::sync`;
//! 表示层不出现 `crate::borg` / `crate::client`;
//! 全工程无 `#[allow(...)]` / `unsafe` / `HashMap`;
//! 汇率缺失不得按 1:1 兜底。
//!
//! ## ★★ 博格与单例的分界线(本工程最重要的一句话)
//!
//! - **单例**管控的是「实例能不能有多个」:装第二次被拒。
//! - **博格**管控的是「状态有几份」:**不限制句柄数量**,只保证状态份数 = 1。
//!
//! 因此 [`borg::BorgSharedState::spawn_handle`] 的接收者是 `&'static self`------
//! 「只有活到程序结束的状态才配发句柄」这件事交给类型系统,
//! 不需要写进注释让人自觉。
//!
//! ## ★ 博格的边界:什么进共享状态,什么不进
//!
//! 判据一句话:**所有句柄必须看到同一个值 → 进共享状态;
//! 只与本句柄有关 → 留在句柄上或另立类型。**
//!
//! - 进共享:门店 / 营业日 / 班次 / 值班操作员 / 汇率表 / 税率 / 促销版本;
//! - 留在句柄:`terminal_code`(身份,`const` 语义、创建后不可变);
//! - 另立类型:`TerminalTill` 钱箱(一号机收的钱被二号机读到,账立刻就错)。
//!
//! ## ★ 本工程要实证的五件事
//!
//! 全部由脚本复验,不是口头声明:
//! `bash scripts/check_layer_dependencies.sh src`(依赖方向)
//! 与 `bash scripts/check_layer_redlines.sh src`(红线)。
//!
//! 1. **机制层零依赖**:`src/borg` 里一个 `crate::` 都没有。
//! 机制对载荷一无所知------这是它能被复用的前提。
//! 2. **领域层不认识博格**:`src/domain` 里没有 `Borg` / `RwLock` / `Handle`。
//! 一旦领域层知道「自己会被共享」,博格就从一个结构退化成一个标志位。
//! 3. **表示层不认识机制与调用层**:`src/app` 里没有 `crate::borg` / `crate::client`。
//! 4. **状态只有一份 且 句柄两两不同**:两条**同时**核,缺一条就退化成别的东西
//! (只核前者单例也过;只核后者快照组也过)。
//! 5. **扩展不改既有文件**:第 5 台终端、澳门元、澳门氹仔店、第 4 位操作员
//! 全部写在下面的「工程外扩展区」,不动任何分层文件,并自动进入统计与报表。
//!
//! ## 十一幕
//!
//! | 幕 | 内容 | 回答什么问题 |
//! |---|---|---|
//! 一 | 装配总览 | 后面每个数受哪些参数影响 |
//! 二 | 币种与汇率 | 两条折算路径的共同基线 |
//! 三 | 机群装配 | N 个句柄 + N 个钱箱 + 1 份状态 |
//! 四 | 三档载体对照 | ★ 改一次共享状态,看谁跟着变 |
//! 五 | 契约审计四条 | ★ 状态相同 **且** 句柄不同(含已知失败用例) |
//! 六 | 钱箱账本 | 双路径互盯 + 汇率波动影响额 |
//! 七 | 币种分桶守恒式 | 分桶求和 vs 逐笔求和 |
//! 八 | 价值量化 | N 份与 1 份差多少;台账快照 |
//! 九 | 工程外扩展 | 新增的东西有没有自动进入统计 |
//! 十 | 接口清点 | 对外只读接口逐条真调一遍 |
//! 十一 | 体检 | 所有「应当 == 实测」收在一处 |
mod analysis;
mod app;
mod borg;
mod client;
mod domain;
mod support;
use std::sync::OnceLock;
use crate::analysis::{
audit_rule_view, audit_rule_views, audit_summary_view, AuditRuleView, AuditSummaryView,
BorgValueReport, CarrierProbe, CensusView, CheckLine, CheckReport, SettlementLedger,
TillLedgerEntry,
};
use crate::app::{
basis_points_text, bool_text, byte_text, gram_display_text, note_lines, render_assembly_table,
render_audit_negative_table, render_audit_table, render_carrier_table, render_census_table,
render_check_table, render_currency_bucket_table, render_exchange_table,
render_extension_table, render_fleet_table, render_interface_table, render_till_table,
render_value_table, render_cell, section_header, table_rule, ColumnAlign, ExtensionRowView,
FleetRowView, Table, TableColumn, DOCUMENT_WIDTH, INTERFACE_NAME_WIDTH, INTERFACE_OUTPUT_WIDTH,
};
use crate::borg::{audit_borg_handles, BorgAudit, BorgHandle, BorgSharedState, StateCensus};
use crate::client::{
BusinessContextCarrier, CarrierKind, FleetMember, IsolatedContext, SnapshotContext,
StoreService, TerminalFleet,
};
use crate::domain::{
divide_rounded, next_shift, BusinessContextState, CurrencyCode, ExchangeRateEntry,
ExchangeRateTable, Money, ShiftCode, StoreCode, TenderLine, TerminalCode, TradeKind, CNY, HKD,
STORE_CN_01, STORE_CN_02, STORE_HK_01, TERMINAL_POS_01, TERMINAL_POS_02, TERMINAL_POS_03,
TERMINAL_POS_04, USD, BUILTIN_CURRENCY_CODES, BUILTIN_SHIFT_CODES, BUILTIN_STORE_CODES,
BUILTIN_TERMINAL_CODES, BUILTIN_TRADE_KINDS,
};
use crate::support::{
days_between, days_in_month, deterministic_code, display_width, elide_text, is_leap_year,
pad_center, prefixed_code, report_cell_text, self_check_width_model, stable_hash,
truncate_to_width, CalendarDate,
};
// ==================================================================
// ★★★ 工程外扩展区:以下内容**全部**在 `main.rs` 内声明
// ==================================================================
// 判据:`src/` 下的分层文件一个都没有被修改。
// 新增的东西必须**自动进入统计与报表**,而不是「接进来就算了」。
//
// 1. 第 5 台终端(新增一台机器 → 多一个句柄,不多一份状态);
// 2. 澳门元 MOP(新增一个币种 → 自动进币种分桶,因为分桶币种来自实际流水);
// 3. 澳门氹仔店(新增一家门店 → 自动进「切换门店」动作);
// 4. 第 4 位值班操作员(新增一位值班员 → 自动进交接班动作)。
// ------------------------------------------------------------------
/// 扩展一:第 5 台终端。
///
/// ⚠️ 声明在这里而不是 `domain/terminal_code.rs`:
/// 后者是分层文件,「新增终端不改分层文件」这条验证必须站得住。
/// 能做到是因为 `TerminalCode` 是**开放型** `const fn new` 结构体而不是 `enum`。
const TERMINAL_POS_05: TerminalCode = TerminalCode::new("POS-05", "二层翡翠柜");
/// 扩展二:澳门元(本工程第 4 种币种)。
///
/// ★ 它能自动进分桶,靠的是 `TerminalTill::native_net_by_currency()` 的
/// 币种清单**从实际流水提取**------若从内置清单取,MOP 会表现为
/// 「这个币种今天没收款」,报表完全正常,只是少了一整个币种的账。
const MOP: CurrencyCode = CurrencyCode::new("MOP", "澳门元", "MOP$");
/// 扩展二配套:澳门元对人民币的万分点(1 MOP ≈ 0.8420 CNY)。
const MOP_RATE_BASIS_POINTS: i64 = 8_420;
/// 扩展三:澳门氹仔店。
const STORE_MO_01: StoreCode = StoreCode::new("STORE-MO-01", "澳门氹仔店");
/// 扩展四:第 4 位值班操作员。
const OPERATOR_EXTENDED: &str = "何俊杰";
// ==================================================================
// 共享状态:**全工程唯一的一份**营业上下文
// ==================================================================
/// ★ 共享状态槽位(`'static`)。
///
/// ## 为什么用 `OnceLock` 而不是 `Box::leak`
///
/// 两者都能拿到 `&'static`,但 `OnceLock` 额外保证「**只会初始化一次**」------
/// 而「状态只有一份」正是博格要守的东西。用 `Box::leak` 的话,
/// 「有人又泄漏了第二个」这件事无法被发现;用 `OnceLock`,第二次取
/// 拿到的仍是同一个引用,**结构上**不可能有第二份。
///
/// ⚠️ `get_or_init` 返回 `&'static T`(`static` 的生命周期就是 `'static`),
/// 这正是 `spawn_handle(&'static self)` 需要的接收者类型。
static SHARED_CONTEXT: OnceLock<BorgSharedState<BusinessContextState>> = OnceLock::new();
/// 取共享状态(首次调用时初始化)。
fn shared_context() -> &'static BorgSharedState<BusinessContextState> {
SHARED_CONTEXT.get_or_init(|| {
let rate_entries: Vec<ExchangeRateEntry> = vec![
ExchangeRateEntry::new(CNY, 10_000),
ExchangeRateEntry::new(HKD, 9_215),
ExchangeRateEntry::new(USD, 71_250),
// ★ 扩展区新增的币种:直接进初始汇率表,不需要改任何分层文件。
ExchangeRateEntry::new(MOP, MOP_RATE_BASIS_POINTS),
];
BorgSharedState::new(BusinessContextState::new(
STORE_CN_01,
CalendarDate::new(2026, 10, 8),
ShiftCode::Morning,
"陈美琪".to_string(),
ExchangeRateTable::new(rate_entries),
1_300,
3,
))
})
}
/// 独立实例档的初值:**刻意与共享档不同**。
///
/// ⚠️ 不从共享状态拷------拷出来的就不叫独立实例,那是快照。
/// 给它一个不同门店、不同班次、不同汇率,好让「初值不一致」
/// 成为这一档可核对的特征。
fn isolated_initial_state() -> BusinessContextState {
let rate_entries: Vec<ExchangeRateEntry> = vec![
ExchangeRateEntry::new(CNY, 10_000),
// ★ 与共享档的两个数字都不同,确保内容指纹必然不同。
ExchangeRateEntry::new(HKD, 9_180),
ExchangeRateEntry::new(USD, 71_300),
];
BusinessContextState::new(
STORE_HK_01,
CalendarDate::new(2026, 10, 8),
ShiftCode::Evening,
"黄伟文".to_string(),
ExchangeRateTable::new(rate_entries),
0,
1,
)
}
// ==================================================================
// 编排
// ==================================================================
fn main() {
// ---------- 装配 ----------
let service: StoreService = StoreService::new(shared_context());
let business_date: CalendarDate = CalendarDate::new(2026, 10, 8);
// ★ 终端清单:4 台内置 + 1 台扩展区新增。
let terminal_codes: Vec<TerminalCode> = vec![
TERMINAL_POS_01,
TERMINAL_POS_02,
TERMINAL_POS_03,
TERMINAL_POS_04,
TERMINAL_POS_05,
];
// ---------- 幕一:装配总览 ----------
let assembly_items: Vec<(String, String)> = vec![
("门店".to_string(), STORE_CN_01.display_text()),
(
"营业日".to_string(),
format!(
"{}({})",
business_date.machine_text(),
business_date.weekday_chinese_text()
),
),
(
"班次".to_string(),
format!(
"{} {}",
ShiftCode::Morning.chinese_name(),
ShiftCode::Morning.period_text()
),
),
("值班操作员".to_string(), "陈美琪".to_string()),
("税率(万分点)".to_string(), "1300".to_string()),
("促销版本".to_string(), "v3".to_string()),
(
"记账基准币种".to_string(),
format!("{} {}", CNY.code(), CNY.chinese_name()),
),
("币种数(含扩展 MOP)".to_string(), "4".to_string()),
(
"终端台数(含扩展 POS-05)".to_string(),
terminal_codes.len().to_string(),
),
("共享状态初始版本号".to_string(), "0".to_string()),
(
"机制层依赖".to_string(),
"零(borg 层不出现任何 crate::)".to_string(),
),
("支撑层".to_string(), "日期 / 确定性编码 / 显示宽度".to_string()),
];
print_lines(&render_assembly_table(&assembly_items));
// ---------- 幕二:币种与汇率 ----------
let initial_table: ExchangeRateTable =
shared_context().read(|state| state.exchange_table().clone());
let mut exchange_rows: Vec<(String, String, String, String)> = Vec::new();
for entry in initial_table.entries().iter() {
exchange_rows.push((
entry.currency().code().to_string(),
entry.currency().chinese_name().to_string(),
basis_points_text(entry.basis_points()),
if entry.currency() == MOP {
"★ 工程外扩展区新增的币种".to_string()
} else if entry.currency() == CNY {
"基准币种,恒为 1.0000".to_string()
} else {
"盘中会被刷新(本工程港币会被刷新)".to_string()
},
));
}
print_lines(&render_exchange_table(&exchange_rows));
// ---------- 开台 + 记账 ----------
let mut fleet: TerminalFleet = TerminalFleet::open(shared_context(), &terminal_codes);
assert!(
record_demo_trades(&mut fleet),
"12 笔演示流水必须全部记进钱箱:记到不存在的终端会被拒绝而不是静默丢弃"
);
// ---------- 幕三:机群装配 ----------
let (fleet_rows, context_texts): (Vec<FleetRowView>, Vec<String>) = build_fleet_rows(&fleet);
print_lines(&render_fleet_table(&fleet_rows, &context_texts));
// ---------- 幕四:三档载体对照实验 ----------
// ★ 快照档必须在**任何变更之前**拷取,否则它拷到的就是变更后的值,
// 「初值一致」这一列会变成「否」,快照与独立档就此混为一谈。
let snapshot: SnapshotContext =
SnapshotContext::capture_from("快照副本档".to_string(), shared_context());
let isolated: IsolatedContext =
IsolatedContext::new("独立实例档".to_string(), isolated_initial_state());
let shared_carrier: &dyn BusinessContextCarrier = fleet.members()[0].context();
let carriers: Vec<&dyn BusinessContextCarrier> = vec![shared_carrier, &snapshot, &isolated];
// ★ 快照档拷取时的版本号:此时共享状态还是 revision 0。
// (此后交接班 / 刷汇率 / 切店 / 换促销档期各 +1,
// 于是它与当前版本的差就是「变更过几次」------这个差在幕十被印出来。)
let isolated_label: String = isolated.label_text().to_string();
// ★ 交接班的目标班次由**领域函数**算出来,不在这里写死「晚班」:
// 写死之后,哪天班次全集变了(例如加了第四种班次),
// 这一处的 `Evening` 不会跟着变,实验就悄悄跑在了旧的制度上。
let current_shift: ShiftCode = shared_context().read(|state| state.shift_code());
let next_shift_code: ShiftCode = next_shift(current_shift);
let probes: Vec<CarrierProbe> = crate::analysis::probe_carriers(&carriers, &|| {
// 变更动作:**交接班**(换班次 + 换操作员,一次变更一个 revision)。
// ⚠️ 操作员取自工程外扩展区(第 4 位值班员),
// 于是「扩展区新增的人自动进共享状态」这件事在幕九之外也有实证。
service.hand_over(next_shift_code, OPERATOR_EXTENDED);
});
print_lines(&render_carrier_table(&crate::analysis::carrier_probe_views(&probes)));
// ★★ 附加实证:对**快照档**施加一次交接班,共享状态必须纹丝不动。
//
// 为什么值得单独做一次:上面的对照实验只证明了「快照没有跟随」,
// 那是**被动**的(它没动,可能只是因为没人去动它)。
// 这里**主动**去改它,再确认共享状态不变------
// 「改不动共享状态」与「没被共享状态改」是两件不同的事,两件都要有证据。
let shared_before_snapshot_change: String =
shared_context().read(|state| state.content_fingerprint());
snapshot.apply_shift_handover(ShiftCode::FullDay, "快照档自己的值班员");
let shared_after_snapshot_change: String =
shared_context().read(|state| state.content_fingerprint());
// ---------- 盘中刷新汇率(为双路径制造差额) ----------
let refreshed: bool = service.refresh_exchange_rate(HKD, 9_330);
assert!(refreshed, "港币必须在汇率表里,刷新失败说明装配错了");
// ---------- 切店到扩展区新增的澳门氹仔店 ----------
service.switch_store(STORE_MO_01);
// ---------- 换促销档期(第 4 类营业动作) ----------
// ★ 前三次变更是「交接班 / 刷汇率 / 切店」,这一次是**换促销档期**。
// 四类动作都要「一处改、处处见」;只验三类等于默认第四类成立,
// 而「默认成立」正是这类工程最容易漏掉的地方(第四类往往是后加的)。
service.advance_promotion();
// ---------- 幕五:契约审计四条 ----------
let handles: Vec<&BorgHandle<BusinessContextState>> = fleet.handles();
let audit: BorgAudit =
audit_borg_handles(&handles, fleet.terminal_total(), &|state: &BusinessContextState| {
state.content_fingerprint()
});
let audit_views: Vec<AuditRuleView> = audit_rule_views(&audit);
let audit_summary: AuditSummaryView = audit_summary_view(&audit);
print_lines(&render_audit_table(
&audit_views,
&format!(
"共 {} 条:通过 {} 条,未通过 {} 条(未通过规则:{})。",
audit_summary.rule_total(),
audit_summary.passed_total(),
audit_summary.failed_total(),
audit_summary.failed_codes_text()
),
));
// ★★ 已知应当失败的用例:**另造一份内容完全相同**的共享状态,
// 从它发一个句柄混进真博格的句柄组。
//
// 为什么要让内容完全相同:若内容也不同,`CONTENT_CONSISTENT` 会跟着失败,
// 于是「到底是哪条规则抓出来的」就说不清了。
// 内容一致而仍然被抓出来,才证明 `SINGLE_SHARED_STATE` 这一条**不是摆设**。
let decoy_state: &'static BorgSharedState<BusinessContextState> =
Box::leak(Box::new(BorgSharedState::new(
shared_context().read(|state| state.clone()),
)));
let decoy_handle: BorgHandle<BusinessContextState> = decoy_state.spawn_handle();
let mut mixed_handles: Vec<&BorgHandle<BusinessContextState>> = fleet.handles();
mixed_handles.push(&decoy_handle);
let negative_audit: BorgAudit = audit_borg_handles(
&mixed_handles,
mixed_handles.len(),
&|state: &BusinessContextState| state.content_fingerprint(),
);
print_lines(&render_audit_negative_table(
&audit_rule_views(&negative_audit),
&negative_audit.failed_codes_text(),
));
// ---------- 幕六 / 幕七:钱箱账本 ----------
let closing_table: ExchangeRateTable =
shared_context().read(|state| state.exchange_table().clone());
let ledger: SettlementLedger = SettlementLedger::build(&fleet, &closing_table);
print_lines(&render_till_table(&ledger));
print_lines(&render_currency_bucket_table(
&ledger.native_net_by_currency_total(),
&ledger.native_net_by_currency_direct(),
ledger.closing_table(),
));
// ---------- 幕八:价值量化 + 台账快照 ----------
// ★ 台账在此刻取一次快照;此后(接口清点等)不再重新读取。
let census: StateCensus = StateCensus::capture(shared_context(), fleet.terminal_total());
let census_view: CensusView = crate::analysis::census_view(&census);
let context_bytes: usize = shared_context().read(|state| state.compact_text().len());
let value_report: BorgValueReport = BorgValueReport::measure(
fleet.terminal_total(),
census.handle_total(),
census.mutation_total(),
context_bytes,
census.state_copy_total(),
);
print_lines(&render_value_table(&value_report));
print_lines(&render_census_table(&census_view, "幕八渲染之前(切店与换促销档期之后)"));
// ---------- 幕九:工程外扩展对照 ----------
let current_revision: u64 = shared_context().read(|state| state.revision());
let extension_rows: Vec<ExtensionRowView> = vec![
ExtensionRowView::new(
"第 5 台终端 POS-05".to_string(),
"机群句柄 + 私有钱箱".to_string(),
true,
"幕三多一行(句柄 #5)、幕六多一行(钱箱指纹不同)".to_string(),
),
ExtensionRowView::new(
"澳门元 MOP".to_string(),
"汇率表 + 币种分桶".to_string(),
true,
"幕二多一行、幕七多一行(分桶币种来自实际流水)".to_string(),
),
ExtensionRowView::new(
"澳门氹仔店".to_string(),
"门店清单 + 切换门店动作".to_string(),
true,
"幕三「共享上下文」一列 5 行同时变成澳门氹仔店".to_string(),
),
ExtensionRowView::new(
format!("第 4 位操作员 {}", OPERATOR_EXTENDED),
"交接班 / 值班操作员".to_string(),
true,
format!(
"共享状态版本号已自增到 {}(`StoreService::hand_over` 可再换人)",
current_revision
),
),
];
print_lines(&render_extension_table(&extension_rows));
// ---------- 幕十:接口清点(**真调一遍**) ----------
print_lines(&render_interface_table(&interface_items(
business_date,
&fleet,
&service,
&snapshot,
&ledger,
&value_report,
&census_view,
&probes,
&audit,
&isolated_label,
)));
// ---------- 幕十一:体检 ----------
let health: CheckReport = health_check_lines(
&probes,
&audit,
&negative_audit,
&ledger,
&value_report,
&fleet,
&shared_before_snapshot_change,
&shared_after_snapshot_change,
);
print_lines(&render_check_table(&health));
}
/// 打印一幕(逐行输出)。
fn print_lines(lines: &[String]) {
for line in lines.iter() {
println!("{}", line);
}
}
/// 12 笔演示流水:销售 / 退货 / 回收,人民币 / 港币 / 美元 / 澳门元。
///
/// ★ 每一笔都带上**落笔当时的汇率万分点**:
/// 这是「两条折算路径能互盯」的前提(见 `domain/tender_line.rs`)。
///
/// ⚠️ 返回 `bool` 而不是「记了几笔」:任何一笔记到不存在的终端上都会被拒绝,
/// 而 `TerminalFleet::record` 正是靠返回 `bool` 让这件事显形。
fn record_demo_trades(fleet: &mut TerminalFleet) -> bool {
let mut all_recorded: bool = true;
// 一号机:人民币销售 + 港币销售 + 港币退货。
all_recorded &= fleet.record(
TERMINAL_POS_01,
TenderLine::new(
"T-0001".to_string(),
TradeKind::Sale,
CNY,
Money::new(1_280_000, CNY),
0,
10_000,
),
);
all_recorded &= fleet.record(
TERMINAL_POS_01,
TenderLine::new(
"T-0002".to_string(),
TradeKind::Sale,
HKD,
Money::new(388_000, HKD),
12_500,
9_215,
),
);
all_recorded &= fleet.record(
TERMINAL_POS_01,
TenderLine::new(
"T-0005".to_string(),
TradeKind::Return,
HKD,
Money::new(128_000, HKD),
4_200,
9_215,
),
);
// 二号机:人民币销售 + 美元销售 + 回收旧金。
all_recorded &= fleet.record(
TERMINAL_POS_02,
TenderLine::new(
"T-0003".to_string(),
TradeKind::Sale,
CNY,
Money::new(268_000, CNY),
0,
10_000,
),
);
all_recorded &= fleet.record(
TERMINAL_POS_02,
TenderLine::new(
"T-0006".to_string(),
TradeKind::Sale,
USD,
Money::new(96_000, USD),
7_800,
71_250,
),
);
all_recorded &= fleet.record(
TERMINAL_POS_02,
TenderLine::new(
"T-0007".to_string(),
TradeKind::Buyback,
CNY,
Money::new(455_000, CNY),
23_100,
10_000,
),
);
// 三号机:人民币销售 + 港币销售。
all_recorded &= fleet.record(
TERMINAL_POS_03,
TenderLine::new(
"T-0004".to_string(),
TradeKind::Sale,
CNY,
Money::new(880_000, CNY),
15_600,
10_000,
),
);
all_recorded &= fleet.record(
TERMINAL_POS_03,
TenderLine::new(
"T-0008".to_string(),
TradeKind::Sale,
HKD,
Money::new(216_000, HKD),
9_800,
9_215,
),
);
// 四号机:人民币销售 + 港币销售。
all_recorded &= fleet.record(
TERMINAL_POS_04,
TenderLine::new(
"T-0009".to_string(),
TradeKind::Sale,
CNY,
Money::new(1_560_000, CNY),
0,
10_000,
),
);
all_recorded &= fleet.record(
TERMINAL_POS_04,
TenderLine::new(
"T-0010".to_string(),
TradeKind::Sale,
HKD,
Money::new(74_500, HKD),
3_200,
9_215,
),
);
// ★ 五号机(扩展区新增):澳门元销售 + 人民币销售。
all_recorded &= fleet.record(
TERMINAL_POS_05,
TenderLine::new(
"T-0011".to_string(),
TradeKind::Sale,
MOP,
Money::new(128_000, MOP),
5_400,
MOP_RATE_BASIS_POINTS,
),
);
all_recorded &= fleet.record(
TERMINAL_POS_05,
TenderLine::new(
"T-0012".to_string(),
TradeKind::Sale,
CNY,
Money::new(336_000, CNY),
0,
10_000,
),
);
all_recorded
}
/// 幕三的行视图与「本机读到的共享上下文」文本。
///
/// ★ 两份数据**同序**:`context_texts[index]` 就是 `fleet_rows[index]` 那台机器的读数。
fn build_fleet_rows(fleet: &TerminalFleet) -> (Vec<FleetRowView>, Vec<String>) {
let mut rows: Vec<FleetRowView> = Vec::new();
let mut context_texts: Vec<String> = Vec::new();
for member in fleet.members().iter() {
let till = member.till();
rows.push(FleetRowView::new(
member.context().terminal_code().code().to_string(),
member.context().terminal_code().chinese_name().to_string(),
member.context().handle_sequence().to_string(),
member_line_count(member).to_string(),
till.deal_count().to_string(),
gram_display_text(till.weight_milligrams()),
till.content_fingerprint(),
));
// ★ 这一列是博格的实证:N 台机器读到的一行文本必须**完全相同**。
context_texts.push(member.context().read(|state| state.compact_text()));
}
(rows, context_texts)
}
/// 幕十:对外只读接口**逐条真调一遍**。
///
/// ⚠️ 每个「输出样例」都是真的跑出来的结果,不是手写的示例文本------
/// 手写的样例会在接口改了之后继续印旧值,那比没有样例更糟。
///
/// ## 分组顺序就是依赖顺序
///
/// 支撑 → 领域 → 机制 → 调用 → 分析 → 表示,与 `main.rs` 顶部的分层图同序。
/// 读者顺着往下读,就是顺着依赖方向走了一遍;
/// 哪一组里出现「支撑层接口返回的值不对」,后面所有组都得重看。
fn interface_items(
business_date: CalendarDate,
fleet: &TerminalFleet,
service: &StoreService,
snapshot: &SnapshotContext,
ledger: &SettlementLedger,
value_report: &BorgValueReport,
census_view: &CensusView,
probes: &[CarrierProbe],
audit: &BorgAudit,
isolated_label: &str,
) -> Vec<(String, String)> {
let mut items: Vec<(String, String)> = Vec::new();
// ---- support ----
items.push((
"support::CalendarDate::weekday_chinese_text()".to_string(),
CalendarDate::new(2026, 10, 8).weekday_chinese_text().to_string(),
));
items.push((
"support::is_leap_year(2100) // ★ 用 2100 做探针".to_string(),
bool_text(is_leap_year(2100)).to_string(),
));
items.push((
"support::deterministic_code(&[\"CNY\"], 10)".to_string(),
deterministic_code(&["CNY"], 10),
));
items.push((
"support::display_width(\"港币\")".to_string(),
display_width("港币").to_string(),
));
items.push((
"support::elide_text(\"澳门氹仔店\", 6)".to_string(),
elide_text("澳门氹仔店", 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::stable_hash(&[\"CNY\", \"HKD\"])".to_string(),
stable_hash(&["CNY", "HKD"]).to_string(),
));
items.push((
"support::prefixed_code(\"FP\", &[\"STORE-CN-01\"], 10)".to_string(),
prefixed_code("FP", &[STORE_CN_01.code()], 10),
));
items.push((
"support::pad_center(\"合计\", 10)".to_string(),
format!("[{}]", pad_center("合计", 10)),
));
items.push((
"support::truncate_to_width(\"澳门氹仔店澳门氹仔店\", 10)".to_string(),
truncate_to_width("澳门氹仔店澳门氹仔店", 10),
));
items.push((
"support::days_in_month(2026, 2) / days_in_month(2024, 2)".to_string(),
format!("{} / {}(平年 / 闰年)", days_in_month(2026, 2), days_in_month(2024, 2)),
));
items.push((
"support::days_between(2026-10-08, 2026-10-11)".to_string(),
days_between(business_date, business_date.add_days(3)).to_string(),
));
items.push((
"support::report_cell_text(2026-10-08)".to_string(),
report_cell_text(business_date),
));
// ---- support::CalendarDate(日期值对象的只读口) ----
items.push((
"support::CalendarDate::year() / month() / day()".to_string(),
format!("{} / {} / {}", business_date.year(), business_date.month(), business_date.day()),
));
items.push((
"support::CalendarDate::is_valid()".to_string(),
bool_text(business_date.is_valid()).to_string(),
));
items.push((
"support::CalendarDate::weekday_index() // 0=周一".to_string(),
business_date.weekday_index().to_string(),
));
items.push((
"support::CalendarDate::is_weekend()".to_string(),
bool_text(business_date.is_weekend()).to_string(),
));
items.push((
"support::CalendarDate::chinese_text()".to_string(),
business_date.chinese_text(),
));
items.push((
"support::CalendarDate::chinese_text_with_weekday()".to_string(),
business_date.chinese_text_with_weekday(),
));
items.push((
"support::CalendarDate::add_days(30)".to_string(),
business_date.add_days(30).machine_text(),
));
items.push((
"support::CalendarDate::add_working_days(3)".to_string(),
business_date.add_working_days(3).machine_text(),
));
// ---- domain ----
items.push((
"domain::divide_rounded(388000 * 9330, 10000)".to_string(),
divide_rounded(i128::from(388_000) * i128::from(9_330), 10_000).to_string(),
));
items.push((
"domain::Money::new(128000, CNY).chinese_text()".to_string(),
Money::new(128_000, CNY).chinese_text(),
));
items.push((
"domain::ExchangeRateTable::basis_points_of(HKD) // 收盘".to_string(),
match ledger.closing_table().basis_points_of(HKD) {
Some(points) => basis_points_text(points),
None => "无汇率".to_string(),
},
));
items.push((
"domain::ExchangeRateTable::convert_to_base(MOP$1,280.00)".to_string(),
match ledger.closing_table().convert_to_base(Money::new(128_000, MOP)) {
Some(money) => money.chinese_text(),
None => "不可用".to_string(),
},
));
items.push((
"domain::TenderLine::cny_at_trade_rate() // T-0002 落笔汇率".to_string(),
fleet
.member_of(TERMINAL_POS_01)
.and_then(|member| member.till().lines().get(1))
.map(|line| line.cny_at_trade_rate().chinese_text())
.unwrap_or_else(|| "不可用".to_string()),
));
items.push((
"domain::TerminalTill::native_net_by_currency() // 一号机".to_string(),
fleet
.member_of(TERMINAL_POS_01)
.map(|member| {
member
.till()
.native_net_by_currency()
.iter()
.map(|(currency, money)| format!("{}{}", currency.code(), money.amount_text()))
.collect::<Vec<String>>()
.join(" / ")
})
.unwrap_or_else(|| "不可用".to_string()),
));
items.push((
"domain::BusinessContextState::content_fingerprint()".to_string(),
shared_context().read(|state| state.content_fingerprint()),
));
items.push((
"domain::BusinessContextState::compact_text()".to_string(),
shared_context().read(|state| state.compact_text()),
));
items.push((
"domain::BusinessContextState::revision()".to_string(),
shared_context().read(|state| state.revision()).to_string(),
));
items.push((
"domain::Money::zero(CNY) / is_zero()".to_string(),
format!(
"{} / {}",
Money::zero(CNY).amount_text(),
bool_text(Money::zero(CNY).is_zero())
),
));
items.push((
"domain::Money::plus(¥12.80 + ¥25.00)".to_string(),
Money::new(1_280, CNY).plus(Money::new(2_500, CNY)).chinese_text(),
));
items.push((
"domain::TenderLine::trade_rate_basis_points() // 一号机首笔".to_string(),
fleet
.member_of(TERMINAL_POS_01)
.and_then(|member| member.till().lines().first())
.map(|line| basis_points_text(line.trade_rate_basis_points()))
.unwrap_or_else(|| "不可用".to_string()),
));
items.push((
"domain::BUILTIN_CURRENCY_CODES // 内置币种全集".to_string(),
builtin_currency_text(),
));
items.push((
"domain::BUILTIN_STORE_CODES // 内置门店全集(含未用到的)".to_string(),
builtin_store_text(),
));
items.push((
"domain::STORE_CN_02 // 内置但本场没切过去的门店".to_string(),
format!("{} {}", STORE_CN_02.code(), STORE_CN_02.chinese_name()),
));
items.push((
"domain::BUILTIN_TERMINAL_CODES // 内置终端全集".to_string(),
builtin_terminal_text(),
));
items.push((
"domain::BUILTIN_TRADE_KINDS // 交易种类全集".to_string(),
builtin_trade_kind_text(),
));
items.push((
"domain::BUILTIN_SHIFT_CODES // 班次全集(经营制度,非数据)".to_string(),
builtin_shift_text(),
));
items.push((
"domain::next_shift(早班) // 交接班算下一班,不写死".to_string(),
next_shift(ShiftCode::Morning).chinese_name().to_string(),
));
items.push((
"domain::BusinessContextState::business_date()".to_string(),
shared_context().read(|state| state.business_date().machine_text()),
));
items.push((
"domain::BusinessContextState::shift_code()".to_string(),
shared_context().read(|state| format!("{}({})", state.shift_code().chinese_name(), state.shift_code().period_text())),
));
items.push((
"domain::BusinessContextState::operator_name() // 扩展区第 4 位".to_string(),
shared_context().read(|state| state.operator_name().to_string()),
));
items.push((
"domain::BusinessContextState::tax_rate_basis_points()".to_string(),
basis_points_text(shared_context().read(|state| state.tax_rate_basis_points())),
));
items.push((
"domain::BusinessContextState::promotion_version()".to_string(),
format!("v{}", shared_context().read(|state| state.promotion_version())),
));
// ---- borg ----
items.push((
"borg::BorgSharedState::handle_total()".to_string(),
shared_context().handle_total().to_string(),
));
items.push((
"borg::BorgSharedState::poison_recoveries() // 正常为 0".to_string(),
shared_context().poison_recoveries().to_string(),
));
items.push((
"borg::BorgHandle::handle_sequence() // 一号机".to_string(),
fleet.members()[0].context().handle_sequence().to_string(),
));
items.push((
"borg::BorgHandle::is_same_state(一号机, 二号机) // ★ 博格判据".to_string(),
bool_text(
fleet.members()[0]
.context()
.handle()
.is_same_state(fleet.members()[1].context().handle())
)
.to_string(),
));
items.push((
"borg::BorgHandle::is_same_handle(一号机, 二号机) // ★ 不是单例".to_string(),
bool_text(
fleet.members()[0]
.context()
.handle()
.is_same_handle(fleet.members()[1].context().handle())
)
.to_string(),
));
items.push((
"borg::StateCensus::summary_text() // 快照".to_string(),
census_view.summary_text().to_string(),
));
items.push((
"borg::BorgHandle::shared_state() // ★ 句柄只持有引用".to_string(),
format!(
"同一槽位,已发句柄 {} 个",
fleet.members()[0].context().handle().shared_state().handle_total()
),
));
items.push((
"borg::audit_borg_handles() // 返回 BorgAudit".to_string(),
format!("{} 条规则", audit.rules().len()),
));
items.push((
"borg::BorgAuditRule 首条(经 audit_rule_view 转换)".to_string(),
match audit.rules().first() {
Some(rule) => {
let view: AuditRuleView = audit_rule_view(rule);
format!(
"{} → {}(passed={})",
view.rule_code(),
view.verdict_text(),
bool_text(view.passed())
)
}
None => "无".to_string(),
},
));
// ---- client ----
items.push((
"client::CarrierKind::Shared::propagates_change()".to_string(),
bool_text(CarrierKind::Shared.propagates_change()).to_string(),
));
items.push((
"client::CarrierKind::Snapshot::propagates_change()".to_string(),
bool_text(CarrierKind::Snapshot.propagates_change()).to_string(),
));
items.push((
"client::CarrierKind::Isolated::propagates_change()".to_string(),
bool_text(CarrierKind::Isolated.propagates_change()).to_string(),
));
items.push((
"client::TerminalFleet::terminal_total()".to_string(),
fleet.terminal_total().to_string(),
));
items.push((
"client::TerminalFleet::handles() // 句柄清单长度".to_string(),
fleet.handles().len().to_string(),
));
items.push((
"client::SnapshotContext::label_text()".to_string(),
snapshot.label_text().to_string(),
));
items.push((
"client::SnapshotContext::captured_revision() // ★ 落后多少".to_string(),
{
let current: u64 = shared_context().read(|state| state.revision());
let captured: u64 = snapshot.captured_revision();
format!("{}(当前 revision {},落后 {} 版)", captured, current, current - captured)
},
));
items.push((
"client::StoreService::describe_text() // 实时读数".to_string(),
service.describe_text(),
));
items.push((
"client::StoreService::shared_state() // 服务持有的是共享状态".to_string(),
format!(
"锁中毒恢复 {} 次(正常为 0)",
service.shared_state().poison_recoveries()
),
));
items.push((
"client::BusinessContextCarrier::current_state() // 一号机读数副本".to_string(),
fleet.members()[0].context().current_state().compact_text(),
));
items.push((
"client::IsolatedContext::label_text()".to_string(),
isolated_label.to_string(),
));
// ---- analysis ----
items.push((
"analysis::probe_carriers() // 三步实验".to_string(),
format!("{} 条观测", probes.len()),
));
items.push((
"analysis::carrier_probe_views(probes) // 首条视图".to_string(),
match crate::analysis::carrier_probe_views(probes).first() {
Some(view) => format!(
"{}({})→ {}",
view.kind_code(),
view.label_text(),
view.verdict_text()
),
None => "无".to_string(),
},
));
items.push((
"analysis::CarrierProbe::shared_initial_fingerprint() // 变更前".to_string(),
probes
.first()
.map(|probe| probe.shared_initial_fingerprint().to_string())
.unwrap_or_else(|| "无".to_string()),
));
items.push((
"analysis::CarrierProbe::shared_final_fingerprint() // 变更后".to_string(),
probes
.first()
.map(|probe| probe.shared_final_fingerprint().to_string())
.unwrap_or_else(|| "无".to_string()),
));
items.push((
"analysis::SettlementLedger::till_total()".to_string(),
format!("{} 台", ledger.till_total()),
));
items.push((
"analysis::SettlementLedger::line_total()".to_string(),
ledger.line_total().to_string(),
));
items.push((
"analysis::SettlementLedger::deal_count_total()".to_string(),
ledger.deal_count_total().to_string(),
));
items.push((
"analysis::SettlementLedger::weight_milligrams_total()".to_string(),
gram_display_text(ledger.weight_milligrams_total()),
));
items.push((
"analysis::TillLedgerEntry::path_two_available() // 每台都能收盘折算".to_string(),
bool_text(
!ledger.entries().is_empty()
&& ledger.entries().iter().all(TillLedgerEntry::path_two_available),
)
.to_string(),
));
items.push((
"analysis::SettlementLedger::path_one_cny_total()".to_string(),
ledger.path_one_cny_total().chinese_text(),
));
items.push((
"analysis::SettlementLedger::gap_cny_total() // ★ 波动影响额".to_string(),
match ledger.gap_cny_total() {
Some(money) => money.chinese_text(),
None => "不可用".to_string(),
},
));
items.push((
"analysis::SettlementLedger::currencies()".to_string(),
ledger
.currencies()
.iter()
.map(CurrencyCode::code)
.collect::<Vec<&str>>()
.join("+"),
));
items.push((
"analysis::BorgValueReport::summary_text()".to_string(),
value_report.summary_text(),
));
items.push((
"analysis::BorgValueReport::terminal_total() / context_bytes()".to_string(),
format!("{} 台 / {} 字节", value_report.terminal_total(), value_report.context_bytes()),
));
items.push((
"analysis::BorgValueReport::saved_bytes_total()".to_string(),
format!("{} 字节", byte_text(value_report.saved_bytes_total())),
));
items.push((
"analysis::BorgValueReport::avoided_sync_total()".to_string(),
value_report.avoided_sync_total().to_string(),
));
items.push((
"analysis::CheckReport::failed_items_text() // 价值量化自检".to_string(),
value_report.self_check().failed_items_text(),
));
items.push((
"analysis::CheckReport::line_total() / all_passed()".to_string(),
{
let value_check: CheckReport = value_report.self_check();
format!(
"{} 条 / {}",
value_check.line_total(),
bool_text(value_check.all_passed())
)
},
));
items.push((
"analysis::AuditSummaryView::all_passed() // 契约审计汇总".to_string(),
{
let summary: AuditSummaryView = audit_summary_view(audit);
format!(
"{}({} 条中通过 {} 条)",
bool_text(summary.all_passed()),
summary.rule_total(),
summary.passed_total()
)
},
));
// ---- app ----
items.push((
"app::gram_display_text(12500)".to_string(),
gram_display_text(12_500),
));
items.push(("app::basis_points_text(9330)".to_string(), basis_points_text(9_330)));
items.push(("app::bool_text(true)".to_string(), bool_text(true).to_string()));
items.push(("app::byte_text(1234567)".to_string(), byte_text(1_234_567)));
items.push((
"app::section_header(标题, 文档宽度) // 长度".to_string(),
format!("{} 列", display_width(§ion_header("幕十", DOCUMENT_WIDTH))),
));
items.push((
"app::table_rule(文档宽度) // 长度".to_string(),
format!("{} 列", display_width(&table_rule(DOCUMENT_WIDTH))),
));
items.push((
"app::INTERFACE_NAME_WIDTH + INTERFACE_OUTPUT_WIDTH + 开销".to_string(),
format!(
"{}+{}+7 = {} 列(文档宽度 {})",
INTERFACE_NAME_WIDTH,
INTERFACE_OUTPUT_WIDTH,
INTERFACE_NAME_WIDTH + INTERFACE_OUTPUT_WIDTH + 7,
DOCUMENT_WIDTH
),
));
items
}
/// 内置币种全集的一行文本(`条数:编码 中文名 ...`)。
///
/// ⚠️ 条数由 [`BUILTIN_CURRENCY_CODES`] 自己给出,不写死 `3`:
/// 内置清单将来多一种,这一行不需要有人记得来改。
fn builtin_currency_text() -> String {
let texts: Vec<String> = BUILTIN_CURRENCY_CODES
.iter()
.map(|currency| format!("{} {}", currency.code(), currency.chinese_name()))
.collect();
format!("{} 种:{}", texts.len(), texts.join(" / "))
}
/// 内置门店全集的一行文本。
fn builtin_store_text() -> String {
let texts: Vec<String> = BUILTIN_STORE_CODES
.iter()
.map(|store| format!("{} {}", store.code(), store.chinese_name()))
.collect();
format!("{} 家:{}", texts.len(), texts.join(" / "))
}
/// 内置终端全集的一行文本。
fn builtin_terminal_text() -> String {
let texts: Vec<String> = BUILTIN_TERMINAL_CODES
.iter()
.map(|terminal| format!("{} {}", terminal.code(), terminal.chinese_name()))
.collect();
format!("{} 台:{}", texts.len(), texts.join(" / "))
}
/// 交易种类全集的一行文本。
fn builtin_trade_kind_text() -> String {
let texts: Vec<String> = BUILTIN_TRADE_KINDS
.iter()
.map(|kind| format!("{} {}", kind.code(), kind.chinese_name()))
.collect();
format!("{} 种:{}", texts.len(), texts.join(" / "))
}
/// 班次全集的一行文本(**经营制度**,用 `enum` 换编译期穷尽性检查)。
fn builtin_shift_text() -> String {
let texts: Vec<String> = BUILTIN_SHIFT_CODES
.iter()
.map(|shift| format!("{} {}", shift.chinese_name(), shift.period_text()))
.collect();
format!("{} 种:{}", texts.len(), texts.join(" / "))
}
/// 取某台终端钱箱里的流水笔数(**显式写出 `FleetMember` 类型**)。
///
/// ⚠️ 这里刻意标注类型而不是让编译器推断:`FleetMember` 是调用层对外暴露的
/// 「**一台终端 = 一个共享句柄 + 一个私有钱箱**」这一对偶关系,
/// 写出来读者才看得见------这正是博格边界的落点。
fn member_line_count(member: &FleetMember) -> usize {
member.till().line_count()
}
/// 幕十一(版面部分):**排版引擎自检**。
///
/// ## ★ 为什么体检要检排版
///
/// 前面十幕的全部结论都靠这张版面传达。
/// 版面若会静默截断,那么「结论正确」与「读者看到正确」就是两件事------
/// 而后者无法从数据里看出来。
///
/// ## 「已知会失败的输入」分别是什么
///
/// | 检查 | 会让它失败的输入 |
/// |---|---|
/// | `section_header` 补白 | 有人给标题加了额外前缀却没同步补白 |
/// | `table_rule` 长度 | 有人把框线字符改成宽字符却按 1 列算 |
/// | `render_cell` 右对齐 | 把数字列建成 `TableColumn::new`(左对齐) |
/// | `note_lines` 折行 | 折行按续行 2 列留白(首行会溢出 2 列) |
/// | `Table::row_count` | `with_row` 改成去重追加 |
/// | 接口表两列宽 | 单独改了 `INTERFACE_NAME_WIDTH` 却忘了它是反算出来的 |
fn layout_engine_check_lines() -> Vec<CheckLine> {
let mut lines: Vec<CheckLine> = Vec::new();
// 一:幕标题必须补白到**正好**文档宽度(否则右边缘会参差)。
let header_width: usize = display_width(§ion_header("版面自检", DOCUMENT_WIDTH));
lines.push(CheckLine::new(
"版面:section_header 补白".to_string(),
format!("{} 列", DOCUMENT_WIDTH),
format!("{} 列", header_width),
header_width == DOCUMENT_WIDTH,
));
// 二:横线长度必须等于入参(框线字符若被换成宽字符,这里立刻失败)。
let rule_width: usize = display_width(&table_rule(DOCUMENT_WIDTH));
lines.push(CheckLine::new(
"版面:table_rule 长度".to_string(),
format!("{} 列", DOCUMENT_WIDTH),
format!("{} 列", rule_width),
rule_width == DOCUMENT_WIDTH,
));
// 三:数字列必须**右对齐**,且对齐方式由 `ColumnAlign` 显式给出。
let numeric_column: TableColumn = TableColumn::numeric("自检", 8);
let rendered_cell: String = render_cell(&numeric_column, "42");
let left_padding: usize = rendered_cell.chars().take_while(|character| *character == ' ').count();
lines.push(CheckLine::new(
"版面:render_cell 右对齐".to_string(),
"左补白 6 列".to_string(),
format!(
"左补白 {} 列,align=Right 为 {}",
left_padding,
bool_text(numeric_column.align() == ColumnAlign::Right)
),
left_padding == 6 && numeric_column.align() == ColumnAlign::Right,
));
// 四:注解折行后**每一行**都不能超过文档宽度。
let long_note: String = "★ ".repeat(80);
let wrapped: Vec<String> = note_lines(&long_note, DOCUMENT_WIDTH);
let overflow_total: usize = wrapped
.iter()
.filter(|line| display_width(line) > DOCUMENT_WIDTH)
.count();
lines.push(CheckLine::new(
"版面:note_lines 折行".to_string(),
format!("折成多行,每行 ≤ {} 列", DOCUMENT_WIDTH),
format!("{} 行,超宽 {} 行", wrapped.len(), overflow_total),
wrapped.len() >= 2 && overflow_total == 0,
));
// 五:`row_count()` 必须等于加进去的行数(不含表头)。
let two_rows: Table = Table::new("版面自检", vec![TableColumn::new("列", 6)])
.with_row(vec!["甲".to_string()])
.with_row(vec!["乙".to_string()]);
lines.push(CheckLine::new(
"版面:Table::row_count".to_string(),
"2 行".to_string(),
format!("{} 行(列数 {})", two_rows.row_count(), two_rows.column_count()),
two_rows.row_count() == 2 && two_rows.column_count() == 1,
));
// 六:接口表两列宽之和 + 开销必须正好填满文档宽度(它是**反算**出来的)。
let interface_width: usize = INTERFACE_NAME_WIDTH + INTERFACE_OUTPUT_WIDTH + 3 * 2 + 1;
lines.push(CheckLine::new(
"版面:接口表两列宽".to_string(),
format!("{} 列", DOCUMENT_WIDTH),
format!("{} 列", interface_width),
interface_width == DOCUMENT_WIDTH,
));
lines
}
/// 幕十一:把所有「应当 == 实测」收成一组体检项。
fn health_check_lines(
probes: &[CarrierProbe],
audit: &BorgAudit,
negative_audit: &BorgAudit,
ledger: &SettlementLedger,
value_report: &BorgValueReport,
fleet: &TerminalFleet,
shared_before_snapshot_change: &str,
shared_after_snapshot_change: &str,
) -> CheckReport {
let mut report: CheckReport = CheckReport::new();
// 宽度模型自检(**工具自己也要被检**)。
let width_probes = self_check_width_model();
let width_failed: usize = width_probes.iter().filter(|(_, ok, _)| !ok).count();
report = report.with_line(CheckLine::new(
"宽度模型自检".to_string(),
format!("{} 项全过", width_probes.len()),
format!("{} 项,未通过 {} 项", width_probes.len(), width_failed),
width_failed == 0,
));
// 契约审计:正常用例必须全过。
report = report.with_line(CheckLine::new(
"契约审计四条(正常用例)".to_string(),
format!("{} 条全过", audit.rules().len()),
format!(
"未通过 {} 条({})",
audit.failed_total(),
audit.failed_codes_text()
),
audit.all_passed(),
));
// ★★ 审计也要被审计:已知应当失败的用例**必须**至少失败一条。
report = report.with_line(CheckLine::new(
"契约审计(已知失败用例)".to_string(),
"至少 1 条不过".to_string(),
format!(
"未通过 {} 条({})",
negative_audit.failed_total(),
negative_audit.failed_codes_text()
),
negative_audit.failed_total() >= 1,
));
// 三档载体对照。
for line in crate::analysis::carrier_check(probes).lines().iter() {
report = report.with_line(line.clone());
}
// 钱箱账本自检。
for line in ledger.self_check().lines().iter() {
report = report.with_line(line.clone());
}
// ★ 已知会失败的输入:收盘汇率表**故意缺澳门元**,路径二必须整店不可用。
let incomplete_table: ExchangeRateTable = ExchangeRateTable::new(vec![
ExchangeRateEntry::new(CNY, 10_000),
ExchangeRateEntry::new(HKD, 9_330),
ExchangeRateEntry::new(USD, 71_250),
// 刻意不配 MOP:这就是「已知会失败的输入」。
]);
let incomplete_ledger: SettlementLedger = SettlementLedger::build(fleet, &incomplete_table);
let missing: Vec<CurrencyCode> = incomplete_ledger.missing_rate_currencies();
let missing_text: String = if missing.is_empty() {
"无".to_string()
} else {
missing.iter().map(CurrencyCode::code).collect::<Vec<&str>>().join("+")
};
report = report.with_line(CheckLine::new(
"缺 MOP 汇率时路径二被拒".to_string(),
"不可用,且缺币种 = MOP".to_string(),
format!(
"{},缺币种 = {}",
match incomplete_ledger.path_two_cny_total() {
Some(_) => "竟然可用".to_string(),
None => "不可用".to_string(),
},
missing_text
),
incomplete_ledger.path_two_cny_total().is_none() && missing.len() == 1,
));
// 切店之后全店必须同时看到新门店(**博格的实证**)。
let mut store_consistent: bool = true;
for member in fleet.members().iter() {
let store: StoreCode = member.context().read(|state| state.store_code());
if store != STORE_MO_01 {
store_consistent = false;
}
}
report = report.with_line(CheckLine::new(
"切店后全店一致".to_string(),
format!("{} 台全部读到 {}", fleet.terminal_total(), STORE_MO_01.code()),
if store_consistent {
format!("{} 台一致", fleet.terminal_total())
} else {
"存在读到旧门店的机器".to_string()
},
store_consistent,
));
// 价值量化自检。
for line in value_report.self_check().lines().iter() {
report = report.with_line(line.clone());
}
// 唯一性:句柄两两不同(**用 ptr::eq,绝不打印地址**)。
let mut all_distinct: bool = true;
let handles: Vec<&BorgHandle<BusinessContextState>> = fleet.handles();
for left in 0..handles.len() {
for right in (left + 1)..handles.len() {
if handles[left].is_same_handle(handles[right]) {
all_distinct = false;
}
}
}
report = report.with_line(CheckLine::new(
"句柄两两不同".to_string(),
"是博格不是单例".to_string(),
if all_distinct {
"两两不同".to_string()
} else {
"存在同一句柄".to_string()
},
all_distinct,
));
// ★★ 改快照档**不影响**共享状态(主动去改,而不是「它没动」)。
//
// 已知会失败的输入:把 `SnapshotContext::apply_shift_handover` 写成
// 「转发给共享状态」------那一瞬间它就变成真博格,这条立刻失败。
report = report.with_line(CheckLine::new(
"改快照档不影响共享状态".to_string(),
"共享指纹不变".to_string(),
if shared_before_snapshot_change == shared_after_snapshot_change {
format!("不变({})", shared_after_snapshot_change)
} else {
format!("{} → {}", shared_before_snapshot_change, shared_after_snapshot_change)
},
shared_before_snapshot_change == shared_after_snapshot_change,
));
// ★ 汇率波动影响额**不得为零**:为零说明两条折算路径重合,双路径实验白跑。
//
// 已知会失败的输入:把盘中刷新汇率那一步删掉(或刷成与原来相同的值)。
let gap_nonzero: bool = match ledger.gap_cny_total() {
Some(gap) => !gap.is_zero(),
None => false,
};
report = report.with_line(CheckLine::new(
"波动影响额不为零".to_string(),
"路径一 ≠ 路径二".to_string(),
match ledger.gap_cny_total() {
Some(gap) => format!("{}", gap.chinese_text()),
None => "不可用".to_string(),
},
gap_nonzero,
));
// 每一台终端都必须能按收盘汇率折算(缺任一台的任一币种 → 整店不可用)。
let all_available: bool = !ledger.entries().is_empty()
&& ledger.entries().iter().all(TillLedgerEntry::path_two_available);
report = report.with_line(CheckLine::new(
"所有终端路径二可用".to_string(),
format!("{} 台全部可用", ledger.till_total()),
format!(
"{} 台可用",
ledger.entries().iter().filter(|entry| entry.path_two_available()).count()
),
all_available,
));
// ★ 营业日**进了内容指纹**:翻牌到次日,指纹必须变。
//
// 已知会失败的输入:给 `BusinessContextState` 加了字段却忘了把它写进
// `content_fingerprint()` 的入参------那时「换了个店/换了个班次但指纹不变」,
// 幕五的 `CONTENT_CONSISTENT` 会从此恒真。
let fingerprint_before: String = shared_context().read(|state| state.content_fingerprint());
let mut rolled_state: BusinessContextState = shared_context().read(|state| state.clone());
let rolled_date: CalendarDate = shared_context().read(|state| state.business_date()).add_days(1);
rolled_state.set_business_date(rolled_date);
let fingerprint_after: String = rolled_state.content_fingerprint();
report = report.with_line(CheckLine::new(
"营业日进内容指纹".to_string(),
"翻牌到次日,指纹改变".to_string(),
format!(
"{} → {}(营业日 {})",
fingerprint_before,
fingerprint_after,
rolled_state.business_date().machine_text()
),
fingerprint_before != fingerprint_after,
));
// 换促销档期:版本必须自增,且**所有句柄**都看到新版本。
let mut promotion_consistent: bool = true;
let mut promotion_version: u32 = 0;
for member in fleet.members().iter() {
let version: u32 = member.context().read(|state| state.promotion_version());
if promotion_version == 0 {
promotion_version = version;
} else if version != promotion_version {
promotion_consistent = false;
}
}
report = report.with_line(CheckLine::new(
"换促销档期全店一致".to_string(),
format!("{} 台读到同一版本", fleet.terminal_total()),
if promotion_consistent {
format!("{} 台一致(v{})", fleet.terminal_total(), promotion_version)
} else {
"存在读到旧版本的机器".to_string()
},
promotion_consistent,
));
// 排版引擎自检(**工具自己也要被检**)。
for line in layout_engine_check_lines().into_iter() {
report = report.with_line(line);
}
// 唯一舍入口径自检:`divide_rounded(-a,b) == -divide_rounded(a,b)`。
let positive: i64 = divide_rounded(2_500_000, 10_000);
let symmetric: bool = divide_rounded(-2_500_000, 10_000) == -positive;
report = report.with_line(CheckLine::new(
"舍入口径对称性".to_string(),
format!("divide_rounded(-2500000,10000) == -{}", positive),
divide_rounded(-2_500_000, 10_000).to_string(),
symmetric,
));
report
}
输出:
