云计算百科
云计算领域专业知识百科平台

rust: Template Method Pattern

项目结构:

//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : overridable_skeleton.rs
//! ★★ 反例一:**把骨架写成 trait 的默认方法,它会被覆盖掉**。
//!
//! 这是本工程要证明的那件事的另一半。
//! 「骨架不可变」若只靠 trait 默认方法表达:
//! – 它编译得过;
//! – 报表上"这个部门跑了七步"这句话**从来没有被检查过**,所以看不出来;
//! – 于是"某个部门悄悄跳过了自检"会一路走到交付。
//!
//! 正确的写法(`services::process_runner`)把骨架放在结构体的 inherent 方法上,
//! inherent 方法**不可能**被任何 trait 实现覆盖——那是语言层面的保证。
//!
//! ⚠️ 本文件刻意保留这个错误写法,并在报表里把它与正确写法**并排对照**:
//! 留着它能让读者一眼看出差别,删掉它就只剩一句"请不要这样做"。

use crate::common::template_method::StepCode;
use crate::common::template_method::StepOutcome;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::SkeletonStepCode;

/// 一步的留痕(反例专用的最简形状:只要"跑了哪一步")。
pub type StepSink = Vec<(StepCode, StepOutcome)>;

/// 反例用的一次骨架执行。
pub trait OverridableProcessTemplate {
/// 部门号。
fn department_code(&self) -> DepartmentCode;

/// 准入。
fn check_entry(&self, context: &DepartmentContext) -> StepOutcome;

/// 作业。
fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome;

/// 自检。
fn inspect(&self, context: &DepartmentContext) -> StepOutcome;

/// 记一步(各实现共用)。
fn record_step(&self, sink: &mut StepSink, code: StepCode, outcome: StepOutcome) {
sink.push((code, outcome));
}

/// ★ 骨架:**默认方法**——这就是问题所在。
///
/// 它看起来与 `DepartmentProcessRunner::execute` 一模一样,
/// 区别只有一个:实现类**可以照抄这个签名把它整个换掉**。
fn run(&self, context: &DepartmentContext, sink: &mut StepSink) -> usize {
self.record_step(
sink,
SkeletonStepCode::LOAD,
StepOutcome::performed(String::from("载入上下文")),
);
let entry: StepOutcome = self.check_entry(context);
let failed: bool = entry.is_failed();
self.record_step(sink, SkeletonStepCode::ENTRY, entry);
if failed {
return sink.len();
}
let performed: DepartmentOutcome = self.perform(context);
self.record_step(
sink,
SkeletonStepCode::PERFORM,
performed.outcome().clone(),
);
self.record_step(
sink,
SkeletonStepCode::EXTERNAL,
StepOutcome::skipped(String::from("本部门不依赖外部系统")),
);
let inspected: StepOutcome = self.inspect(context);
self.record_step(sink, SkeletonStepCode::INSPECT, inspected);
self.record_step(
sink,
SkeletonStepCode::EVIDENCE,
StepOutcome::performed(String::from("留痕")),
);
self.record_step(
sink,
SkeletonStepCode::HANDOFF,
StepOutcome::performed(String::from("移交")),
);
sink.len()
}
}

/// 一个**守规矩**的实现:不覆盖 `run`。
pub struct ObedientDepartment;

impl OverridableProcessTemplate for ObedientDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::QUALITY
}

fn check_entry(&self, _context: &DepartmentContext) -> StepOutcome {
StepOutcome::performed(String::from("准入通过"))
}

fn perform(&self, _context: &DepartmentContext) -> DepartmentOutcome {
DepartmentOutcome::without_mutation(StepOutcome::performed(String::from("作业完成")))
}

fn inspect(&self, _context: &DepartmentContext) -> StepOutcome {
StepOutcome::performed(String::from("自检通过"))
}
}

/// ★ 一个**不守规矩**的实现:覆盖了 `run`,悄悄跳过自检那一步。
///
/// 它编译得过、跑得通、报表上只是"多了一个部门少一步"——
/// 而没有任何地方会说"这个部门的骨架被改过"。
pub struct RogueDepartment;

impl OverridableProcessTemplate for RogueDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::QUALITY
}

fn check_entry(&self, _context: &DepartmentContext) -> StepOutcome {
StepOutcome::performed(String::from("准入通过"))
}

fn perform(&self, _context: &DepartmentContext) -> DepartmentOutcome {
DepartmentOutcome::without_mutation(StepOutcome::performed(String::from("作业完成")))
}

fn inspect(&self, _context: &DepartmentContext) -> StepOutcome {
// 就算这里返回失败,下面那个覆盖版 `run` 也**根本不会调用它**。
StepOutcome::failed(String::from("自检本应失败"))
}

/// ★ 覆盖骨架:少跑 `INSPECT` 与 `EVIDENCE` 两步。
fn run(&self, context: &DepartmentContext, sink: &mut StepSink) -> usize {
self.record_step(
sink,
SkeletonStepCode::LOAD,
StepOutcome::performed(String::from("载入上下文")),
);
self.record_step(sink, SkeletonStepCode::ENTRY, self.check_entry(context));
let performed: DepartmentOutcome = self.perform(context);
self.record_step(
sink,
SkeletonStepCode::PERFORM,
performed.outcome().clone(),
);
self.record_step(
sink,
SkeletonStepCode::HANDOFF,
StepOutcome::performed(String::from("直接移交")),
);
sink.len()
}
}

/// 对照:守规矩的跑了几步 / 覆盖骨架的跑了几步。
///
/// 返回的第二个数**小于**第一个数,就是"骨架被改过"的证据。
pub fn demonstrate_override(context: &DepartmentContext) -> (usize, usize) {
let mut obedient_sink: StepSink = Vec::new();
let obedient_steps: usize = ObedientDepartment.run(context, &mut obedient_sink);
let mut rogue_sink: StepSink = Vec::new();
let rogue_steps: usize = RogueDepartment.run(context, &mut rogue_sink);
(obedient_steps, rogue_steps)
}

/// 覆盖版到底漏了哪几步(**按声明序列比对**,说得出名字才算证据)。
pub fn missing_steps_of_override(context: &DepartmentContext) -> Vec<&'static str> {
let mut rogue_sink: StepSink = Vec::new();
RogueDepartment.run(context, &mut rogue_sink);
let declared: [StepCode; 7] = crate::domain::SKELETON_STEP_SEQUENCE;
let mut missing: Vec<&'static str> = Vec::new();
for code in declared.iter() {
if !rogue_sink.iter().any(|item| item.0 == *code) {
missing.push(code.code());
}
}
missing
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : skeleton_copy.rs
//! ★ 反例二:**骨架复制在每个部门里**(不用模板方法的代价)。
//!
//! 模板方法的价值常被说成"复用代码"——那是最不重要的一半。
//! 真正的一半是:**改骨架要改几处**。
//! 本文件把这个数字算出来,让"我们用了模板方法"变成可反驳的命题。
//!
//! | 形态 | 加一个部门 | 改一步骨架 |
//! |—|—|—|
//! 骨架复制在各部门 | 复制一份七步骨架(7 处) | 改 12 处(每个部门一份) |
//! 模板方法(本工程) | 只实现三个步骤(0 处骨架) | 改 1 处 |
//!
//! ⚠️ 数字随实现形态变化,因此**不可与别处的数字直接比较**;
//! 真正要比较的是"改在哪个文件":
//! 复制版改在**已交付的部门文件**,模板方法版改在**骨架那一个文件**。

/// 复制版骨架的代价测算。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SkeletonCopyCost {
/// 部门数。
department_total: usize,
/// 骨架步数。
step_total: usize,
}

impl SkeletonCopyCost {
/// 测算(部门数、骨架步数)。
pub const fn new(department_total: usize, step_total: usize) -> Self {
SkeletonCopyCost {
department_total,
step_total,
}
}

/// 复制版:骨架代码一共被写了多少处。
pub const fn copied_skeleton_site_total(&self) -> usize {
self.department_total * self.step_total
}

/// 模板方法版:骨架代码一共被写了多少处(**恒为 1**)。
pub const fn shared_skeleton_site_total(&self) -> usize {
1
}

/// ★ 改一步骨架要改几处(复制版)。
pub const fn change_sites_when_copied(&self) -> usize {
self.department_total
}

/// ★ 改一步骨架要改几处(模板方法版)。
pub const fn change_sites_when_shared(&self) -> usize {
1
}

/// 省下的处数。
pub const fn saved_site_total(&self) -> usize {
self.change_sites_when_copied() – self.change_sites_when_shared()
}

/// 一句话结论(报表用,**每个数字都从上面算出来**,不写死)。
pub fn conclusion_text(&self) -> String {
format!(
"{} 个部门 × {} 步:复制版要把骨架写 {} 处、改一步动 {} 处;模板方法版只写 {} 处、改一步动 {} 处(省 {} 处)",
self.department_total,
self.step_total,
self.copied_skeleton_site_total(),
self.change_sites_when_copied(),
self.shared_skeleton_site_total(),
self.change_sites_when_shared(),
self.saved_site_total()
)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : design_drafting_department.rs
//! 设计制图部门:效果图 / 三视图 / 工艺图纸。
//!
//! ★ 准入校验是**本模式最有价值的那一类步骤**:
//! "定金没付够就出图"在类型系统里完全合法(参数是 `i64`,付 10% 也是 `i64`),
//! 只有业务规则拦得住。模板方法把这类规则**固定在一个位置**(骨架第二步),
//! 于是"哪个部门有哪些准入"可以被一张表数出来。

use crate::common::numeric::scale_rounded;
use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::{DESIGN_FEE_BASIS_POINTS, MIN_DEPOSIT_RATIO_BASIS_POINTS};
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 出图工时(分钟)。
const WORK_MINUTES: i64 = 120;

/// 设计制图部门。
pub struct DesignDraftingDepartment;

impl DepartmentSteps for DesignDraftingDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::DESIGN
}

/// 图纸是后面所有工序的依据,定不下来就该停 → `Abort`。
fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Abort
}

fn check_entry(&self, context: &DepartmentContext) -> StepOutcome {
let ratio: i64 = context.deposit_ratio_basis_points();
if ratio < MIN_DEPOSIT_RATIO_BASIS_POINTS {
return StepOutcome::failed(format!(
"定金比率 {} 万分点,低于下限 {} 万分点",
ratio, MIN_DEPOSIT_RATIO_BASIS_POINTS
));
}
StepOutcome::performed(format!(
"定金比率 {} 万分点(下限 {})通过,已收 {} 分",
ratio,
MIN_DEPOSIT_RATIO_BASIS_POINTS,
context.received_minor()
))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
let design_fee: i64 = scale_rounded(
i128::from(context.quoted_minor()),
i128::from(DESIGN_FEE_BASIS_POINTS),
10_000,
);
DepartmentOutcome::new(
StepOutcome::performed(format!(
"出图完成,设计费 {} 分(费率 {} 万分点)",
design_fee, DESIGN_FEE_BASIS_POINTS
)),
OrderMutation::none()
.with_labor_cost(design_fee)
.with_work_minutes(WORK_MINUTES)
.with_note("设计制图:图纸已定稿"),
)
}

fn inspect(&self, context: &DepartmentContext) -> StepOutcome {
// ⚠️ 自检拿到的是**作业前快照**:本次设计费还没记进 `labor_cost_minor`,
// 所以这里核「按报价与费率算出的设计费是否为正」,而不是核累计人工成本。
// 若写成 `context.labor_cost_minor() <= 0`,第一次出图会永远自检失败——
// 那是把「作业后的结果」当成了「作业前的输入」。
let design_fee: i64 = scale_rounded(
i128::from(context.quoted_minor()),
i128::from(DESIGN_FEE_BASIS_POINTS),
10_000,
);
if design_fee <= 0 {
return StepOutcome::failed(String::from("自检失败:按报价算出的设计费为 0"));
}
StepOutcome::performed(format!(
"自检通过:本次设计费 {} 分(费率 {} 万分点),作业前累计人工成本 {} 分",
design_fee,
DESIGN_FEE_BASIS_POINTS,
context.labor_cost_minor()
))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : finance_settlement_department.rs
//! 财务部门:定金 / 尾款 / 成本结转 / 对账。
//!
//! ★ 本部门是**唯一会改"收款"**的部门,也是唯一会把工单标记为已结算的部门。
//! 「收款」这件事在别处(组合根)另有一个独立用例 `receive_payment`,
//! 那是"客户补款",与本部门的"结算收款"是两条不同的路径——
//! 混在一起会让"已结算"与"已收齐"变成同一件事,而它们不是。

use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 结算工时(分钟)。
const WORK_MINUTES: i64 = 60;

/// 财务部门。
pub struct FinanceSettlementDepartment;

impl DepartmentSteps for FinanceSettlementDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::FINANCE
}

fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Abort
}

fn check_entry(&self, context: &DepartmentContext) -> StepOutcome {
if !context.delivered() {
return StepOutcome::failed(String::from("尚未签收,不能结算"));
}
StepOutcome::performed(String::from("已签收,可以结算"))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
let outstanding: i64 = context.outstanding_minor();
if outstanding <= 0 {
return DepartmentOutcome::new(
StepOutcome::performed(String::from("尾款已结清,本次无需收款")),
OrderMutation::none()
.with_work_minutes(WORK_MINUTES)
.with_settled()
.with_note("财务:尾款已结清,标记结算"),
);
}
DepartmentOutcome::new(
StepOutcome::performed(format!("收取尾款 {} 分", outstanding)),
OrderMutation::none()
.with_received(outstanding)
.with_work_minutes(WORK_MINUTES)
.with_settled()
.with_note("财务:已收尾款并结算"),
)
}

fn inspect(&self, context: &DepartmentContext) -> StepOutcome {
// 结算的自检:收完之后"未收"必须为 0。
// ⚠️ 读的是作业前快照,因此核的是"结算前提"(已签收),
// 本次收款的结果由聚合根落库后的口径体现。
if !context.delivered() {
return StepOutcome::failed(String::from("自检失败:未签收却要结算"));
}
StepOutcome::performed(format!(
"自检通过:结算前未收 {} 分,本次收齐",
context.outstanding_minor()
))
}

/// 钩子:依赖外部财务对账系统。
fn requires_external_system(&self) -> bool {
true
}

fn external_operation(&self) -> &'static str {
"RECONCILE"
}

fn extra_evidence_note(&self, _context: &DepartmentContext) -> Option<String> {
Some(String::from("已生成对账单并推送财务系统"))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : human_resources_department.rs
//! 人事行政部门:排班 / 用工 / 行政支持。
//!
//! ★ 本部门是**无准入限制**的那一类(骨架第二步永远通过)。
//! 它的价值在于把"人力成本"按**实际累计工时**折进工单——
//! 于是"工时"这个口径第一次有了对价,报表的毛利率才谈得上完整。

use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::LABOR_COST_MINOR_PER_MINUTE;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 人事行政作业工时(分钟)。
const WORK_MINUTES: i64 = 40;

/// 人事行政部门。
pub struct HumanResourcesDepartment;

impl DepartmentSteps for HumanResourcesDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::HR_ADMIN
}

/// 行政支持不阻断交付 → `Continue`。
fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Continue
}

fn check_entry(&self, _context: &DepartmentContext) -> StepOutcome {
// ★ 准入永远通过,但**必须给出理由**(不能只回一个"通过"):
// 空理由会让"忘了写准入"与"确实没有准入"长得分毫不差。
StepOutcome::performed(String::from("人事行政无准入限制(任何阶段都可排班)"))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
// 人力成本 = 累计工时 × 单价。
// ⚠️ 这里记的是**增量**:本次上岗新增的工时(WORK_MINUTES)对应的那部分。
let labor_cost: i64 = WORK_MINUTES * LABOR_COST_MINOR_PER_MINUTE;
DepartmentOutcome::new(
StepOutcome::performed(format!(
"排班完成,新增人力成本 {} 分({} 分钟 × {} 分/分钟),历史累计工时 {} 分钟",
labor_cost, WORK_MINUTES, LABOR_COST_MINOR_PER_MINUTE, context.work_minutes()
)),
OrderMutation::none()
.with_labor_cost(labor_cost)
.with_work_minutes(WORK_MINUTES)
.with_note("人事行政:排班与用工已登记"),
)
}

fn inspect(&self, context: &DepartmentContext) -> StepOutcome {
if context.work_minutes() < 0 {
return StepOutcome::failed(String::from("自检失败:工时为负"));
}
StepOutcome::performed(String::from("自检通过:排班记录已留存"))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : information_technology_department.rs
//! IT 部门:系统账号 / 工位终端 / 接口运维。
//!
//! ★ 本部门同时演示两件事:
//! ① 覆盖外部系统钩子(账号开通要调 IT 工单系统);
//! ② 无准入限制 + `Continue` 策略——系统没开通也不该挡住发货。

use crate::common::numeric::scale_rounded;
use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::IT_FEE_BASIS_POINTS;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// IT 支持工时(分钟)。
const WORK_MINUTES: i64 = 25;

/// IT 部门。
pub struct InformationTechnologyDepartment;

impl DepartmentSteps for InformationTechnologyDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::IT
}

fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Continue
}

fn check_entry(&self, _context: &DepartmentContext) -> StepOutcome {
StepOutcome::performed(String::from("IT 支持无准入限制(任何阶段都可提单)"))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
let it_fee: i64 = scale_rounded(
i128::from(context.quoted_minor()),
i128::from(IT_FEE_BASIS_POINTS),
10_000,
);
DepartmentOutcome::new(
StepOutcome::performed(format!(
"账号与终端已就绪,IT 服务费 {} 分(费率 {} 万分点)",
it_fee, IT_FEE_BASIS_POINTS
)),
OrderMutation::none()
.with_expense(it_fee)
.with_work_minutes(WORK_MINUTES)
.with_note("IT:账号与终端已开通"),
)
}

fn inspect(&self, _context: &DepartmentContext) -> StepOutcome {
StepOutcome::performed(String::from("自检通过:运维巡检记录已留存"))
}

/// 钩子:依赖外部 IT 工单系统。
fn requires_external_system(&self) -> bool {
true
}

fn external_operation(&self) -> &'static str {
"OPEN_ACCOUNT"
}

fn extra_evidence_note(&self, _context: &DepartmentContext) -> Option<String> {
Some(String::from("账号开通单已同步 IT 工单系统"))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : logistics_delivery_department.rs
//! 物流部门:出库 / 承运 / 签收。
//!
//! ★ 本部门覆盖了**钩子** `requires_external_system` → 骨架的 `EXTERNAL` 那一步
//! 会真的发一次"物流轨迹同步"调用。
//! 不覆盖钩子的部门,那一步的结果是 `Skipped`——**三态里的"跳过"就是这个用途**:
//! 若把 `EXTERNAL` 从序列里裁掉,"没调外部系统"与"调了失败"在报表上都表现为少一步。

use crate::common::numeric::scale_rounded;
use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::FREIGHT_FEE_BASIS_POINTS;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 发运工时(分钟)。
const WORK_MINUTES: i64 = 90;

/// 物流部门。
pub struct LogisticsDeliveryDepartment;

impl DepartmentSteps for LogisticsDeliveryDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::LOGISTICS
}

/// ★ 失败仍继续:运单没打出来也要把作业留痕走完,
/// 否则"货发了但系统里没有"这类问题连痕迹都没有。
fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Continue
}

fn check_entry(&self, context: &DepartmentContext) -> StepOutcome {
if !context.packaged() {
return StepOutcome::failed(String::from("尚未包装,不能出库"));
}
StepOutcome::performed(String::from("已包装,可以出库"))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
let freight: i64 = scale_rounded(
i128::from(context.quoted_minor()),
i128::from(FREIGHT_FEE_BASIS_POINTS),
10_000,
);
DepartmentOutcome::new(
StepOutcome::performed(format!(
"已发运,运费 {} 分(费率 {} 万分点)",
freight, FREIGHT_FEE_BASIS_POINTS
)),
OrderMutation::none()
.with_expense(freight)
.with_work_minutes(WORK_MINUTES)
.with_shipped()
.with_note("物流:已出库发运"),
)
}

fn inspect(&self, context: &DepartmentContext) -> StepOutcome {
if context.shipped() {
return StepOutcome::performed(String::from("自检通过:已发运(重复发运未再计费)"));
}
StepOutcome::performed(String::from("自检通过:本次已标记发运"))
}

/// 钩子一:依赖外部物流轨迹系统。
fn requires_external_system(&self) -> bool {
true
}

/// 钩子二:外部操作名。
fn external_operation(&self) -> &'static str {
"TRACE_SHIPMENT"
}

/// 钩子三:追加一句留痕。
fn extra_evidence_note(&self, _context: &DepartmentContext) -> Option<String> {
Some(String::from("运费与运单已同步至物流轨迹系统"))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : marketing_promotion_department.rs
//! 营销推广部门:素材 / 投放 / 活动结算。
//!
//! ★ 本部门演示的是**"不在主线上"的部门**:
//! 它不产出可交付物,只记一笔期间费用。
//! 模板方法让它与主线部门(采购 / 加工 / 质检…)**共享同一个骨架**——
//! 这正是把十二个部门做成模板方法而不是各写一遍的理由。

use crate::common::numeric::scale_rounded;
use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::PROMOTION_FEE_BASIS_POINTS;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 推广工时(分钟)。
const WORK_MINUTES: i64 = 60;

/// 营销推广部门。
pub struct MarketingPromotionDepartment;

impl DepartmentSteps for MarketingPromotionDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::MARKETING
}

/// 推广做不做成都不该阻断交付 → `Continue`。
fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Continue
}

fn check_entry(&self, context: &DepartmentContext) -> StepOutcome {
if !context.shipped() {
return StepOutcome::failed(String::from("尚未发运,不做推广投放"));
}
StepOutcome::performed(String::from("已发运,可以投放"))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
let promotion: i64 = scale_rounded(
i128::from(context.quoted_minor()),
i128::from(PROMOTION_FEE_BASIS_POINTS),
10_000,
);
DepartmentOutcome::new(
StepOutcome::performed(format!(
"投放完成,推广费 {} 分(费率 {} 万分点)",
promotion, PROMOTION_FEE_BASIS_POINTS
)),
OrderMutation::none()
.with_expense(promotion)
.with_work_minutes(WORK_MINUTES)
.with_note("营销推广:素材已投放"),
)
}

fn inspect(&self, context: &DepartmentContext) -> StepOutcome {
if context.expense_minor() < 0 {
return StepOutcome::failed(String::from("自检失败:费用为负"));
}
StepOutcome::performed(format!(
"自检通过:累计费用 {} 分",
context.expense_minor()
))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : material_procurement_department.rs
//! 原料采购核验部门:**端到端的第一棒**。
//!
//! 骨架七步里,本部门只写三件事(准入 / 作业 / 自检),
//! 其余四步由 `DepartmentProcessRunner` 统一跑——这就是模板方法的分工。

use crate::common::numeric::ratio_basis_points;
use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::MIN_PURITY_BASIS_POINTS;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 采购核验的作业工时(分钟)。
const WORK_MINUTES: i64 = 60;

/// ★ 物料成本占报价的**预警线**(万分点):8000 = 80%。
///
/// 超过这条线通常意味着采购价录入错误——自检把它挡下来,
/// 而不是让"成本吃掉全部毛利"一路走到财务报表才被发现。
const MATERIAL_COST_ALERT_BASIS_POINTS: i64 = 8_000;

/// 原料采购核验部门。
pub struct MaterialProcurementDepartment;

impl DepartmentSteps for MaterialProcurementDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::PROCUREMENT
}

/// 核验不过,后面十一个部门都没有意义 → `Abort`。
fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Abort
}

fn check_entry(&self, context: &DepartmentContext) -> StepOutcome {
if context.material_lot_total() == 0 {
return StepOutcome::failed(String::from("没有任何物料批次,无法核验"));
}
let lowest: i64 = context.lowest_purity_basis_points();
if lowest < MIN_PURITY_BASIS_POINTS {
return StepOutcome::failed(format!(
"最低成色 {} 万分点,低于下限 {} 万分点",
lowest, MIN_PURITY_BASIS_POINTS
));
}
StepOutcome::performed(format!(
"{} 个批次,最低成色 {} 万分点(下限 {})通过",
context.material_lot_total(),
lowest,
MIN_PURITY_BASIS_POINTS
))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
// ★ 目标值来自**批次原始数据**(`lot_derived_material_cost_minor`),
// 这里只补差额:写成"直接赋值"会让重复作业把成本记两遍,
// 而报表上看不出差别。
let delta: i64 = context.lot_derived_material_cost_minor() – context.material_cost_minor();
DepartmentOutcome::new(
StepOutcome::performed(format!(
"核定物料成本 {} 分(本次补记 {} 分),总重量 {} 毫克",
context.lot_derived_material_cost_minor(),
delta,
context.total_weight_milligrams()
)),
OrderMutation::none()
.with_material_cost(delta)
.with_work_minutes(WORK_MINUTES)
.with_note("原料采购核验:物料成本已核定"),
)
}

fn inspect(&self, context: &DepartmentContext) -> StepOutcome {
let derived: i64 = context.lot_derived_material_cost_minor();
if derived <= 0 {
return StepOutcome::failed(String::from("自检失败:由批次重算的物料成本为 0"));
}
let share: i64 = ratio_basis_points(i128::from(derived), i128::from(context.quoted_minor()));
if share > MATERIAL_COST_ALERT_BASIS_POINTS {
return StepOutcome::failed(format!(
"自检失败:物料成本占报价 {} 万分点,超过预警线 {} 万分点",
share, MATERIAL_COST_ALERT_BASIS_POINTS
));
}
StepOutcome::performed(format!(
"自检通过:物料成本 {} 分,占报价 {} 万分点(预警线 {})",
derived, share, MATERIAL_COST_ALERT_BASIS_POINTS
))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : packaging_department.rs
//! 包装部门:成品清洁 / 包装 / 证书配套。
//!
//! ★ 本部门演示的是**准入依赖链**:包装的准入是"质检已通过"。
//! 这条链不是靠调用顺序保证的——调度器完全可能把包装排在质检前面,
//! 是准入校验(骨架第二步)在挡它。这是模板方法相对"手写流程"的价值之一。

use crate::common::numeric::scale_rounded;
use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::PACKAGING_FEE_BASIS_POINTS;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 包装工时(分钟)。
const WORK_MINUTES: i64 = 45;

/// 包装部门。
pub struct PackagingDepartment;

impl DepartmentSteps for PackagingDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::PACKAGING
}

fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Abort
}

fn check_entry(&self, context: &DepartmentContext) -> StepOutcome {
if !context.quality_passed() {
return StepOutcome::failed(String::from("质检未通过,不能包装"));
}
StepOutcome::performed(String::from("质检已通过,可以包装"))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
let packaging_fee: i64 = scale_rounded(
i128::from(context.quoted_minor()),
i128::from(PACKAGING_FEE_BASIS_POINTS),
10_000,
);
DepartmentOutcome::new(
StepOutcome::performed(format!(
"包装完成,包装材料 {} 分(费率 {} 万分点)",
packaging_fee, PACKAGING_FEE_BASIS_POINTS
)),
OrderMutation::none()
.with_material_cost(packaging_fee)
.with_work_minutes(WORK_MINUTES)
.with_packaged()
.with_note("包装:成品已包装并配证书"),
)
}

fn inspect(&self, context: &DepartmentContext) -> StepOutcome {
if context.packaged() {
return StepOutcome::performed(String::from("自检通过:已包装(重复包装未再计费)"));
}
StepOutcome::performed(String::from("自检通过:本次已标记包装完成"))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : production_machining_department.rs
//! 加工生产部门:起版 / 浇铸 / 执模 / 镶嵌 / 抛光。
//!
//! ★ 准入条件是"物料已核定":这条规则**依赖上一个部门的产出**,
//! 而模板方法让"上一步的成果"以只读快照的形式进入本部门的准入校验——
//! 于是"加工跑在核验前面"这种顺序错误有地方被挡住。

use crate::common::numeric::scale_rounded;
use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::MACHINING_FEE_BASIS_POINTS;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 加工工时(分钟)。
const WORK_MINUTES: i64 = 600;

/// 加工生产部门。
pub struct ProductionMachiningDepartment;

impl DepartmentSteps for ProductionMachiningDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::PRODUCTION
}

fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Abort
}

fn check_entry(&self, context: &DepartmentContext) -> StepOutcome {
if context.material_cost_minor() <= 0 {
return StepOutcome::failed(String::from("物料成本尚未核定,不能排产"));
}
if context.total_weight_milligrams() <= 0 {
return StepOutcome::failed(String::from("没有物料重量,无法下料"));
}
StepOutcome::performed(format!(
"物料已核定 {} 分,总重 {} 毫克,可以排产",
context.material_cost_minor(),
context.total_weight_milligrams()
))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
let machining_fee: i64 = scale_rounded(
i128::from(context.quoted_minor()),
i128::from(MACHINING_FEE_BASIS_POINTS),
10_000,
);
DepartmentOutcome::new(
StepOutcome::performed(format!(
"加工完工 {} 分钟,加工费 {} 分(费率 {} 万分点)",
WORK_MINUTES, machining_fee, MACHINING_FEE_BASIS_POINTS
)),
OrderMutation::none()
.with_labor_cost(machining_fee)
.with_work_minutes(WORK_MINUTES)
.with_note("加工生产:工序完工"),
)
}

fn inspect(&self, context: &DepartmentContext) -> StepOutcome {
// 加工完的标志是"人工成本里已经含了加工费"——这里用累计工时作独立证据。
if context.work_minutes() <= 0 {
return StepOutcome::failed(String::from("自检失败:没有工时记录"));
}
StepOutcome::performed(format!(
"自检通过:累计工时 {} 分钟,累计人工成本 {} 分",
context.work_minutes(),
context.labor_cost_minor()
))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : quality_inspection_department.rs
//! 质检部门:成色检测 / 外观检验 / 尺寸核验。
//!
//! ★ 本部门是**全工程唯一会输出"不合格"结论**的部门:
//! 夹具里有一张单的成色低于下限,它的准入会在这里被挡住
//! (原料采购那一步也会挡一次——两条路径给出的**拒绝码不同**,报表上要能分辨)。

use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::MIN_PURITY_BASIS_POINTS;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 检验工时(分钟)。
const WORK_MINUTES: i64 = 90;

/// 质检部门。
pub struct QualityInspectionDepartment;

impl DepartmentSteps for QualityInspectionDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::QUALITY
}

/// 不合格就不能往下走 → `Abort`。
fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Abort
}

fn check_entry(&self, context: &DepartmentContext) -> StepOutcome {
if context.labor_cost_minor() <= 0 {
return StepOutcome::failed(String::from("尚未加工,无可检品"));
}
StepOutcome::performed(format!(
"已加工(累计工时 {} 分钟),可以送检",
context.work_minutes()
))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
let lowest: i64 = context.lowest_purity_basis_points();
let passed: bool = lowest >= MIN_PURITY_BASIS_POINTS;
DepartmentOutcome::new(
StepOutcome::performed(format!(
"出具质检报告:实测最低成色 {} 万分点,结论 {}",
lowest,
if passed { "合格" } else { "不合格" }
)),
OrderMutation::none()
.with_quality_passed(passed)
.with_work_minutes(WORK_MINUTES)
.with_note(if passed {
"质检:合格"
} else {
"质检:不合格,退回加工"
}),
)
}

fn inspect(&self, context: &DepartmentContext) -> StepOutcome {
// ⚠️ 自检读的是**上下文**(作业前的快照),不是本次作业的结果:
// 这是模板方法的固有代价——步骤之间不共享可变状态。
// 因此这里核的是"送检前提"而不是"本次结论",
// 本次结论由聚合根落库后(下一部门的准入)来体现。
if !context.quality_passed() && context.material_lot_total() == 0 {
return StepOutcome::failed(String::from("自检失败:既无历史合格记录也无批次"));
}
StepOutcome::performed(format!(
"自检通过:报告已出具(历史合格={})",
context.quality_passed()
))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : sales_business_department.rs
//! 业务部门:客户需求 / 报价 / 交付回访。
//!
//! ★ 本部门**不改任何金额**:它的产出是"客户关系事实"。
//! 这正好用来验证一条可自检的纪律——
//! 「骨架里只有核心作业会改金额」:本部门的核心作业是跑了的,
//! 但它的金额变更是 0,报表的"金额变更"列会显示 0,而不是"没跑"。

use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 回访工时(分钟)。
const WORK_MINUTES: i64 = 30;

/// 业务部门。
pub struct SalesBusinessDepartment;

impl DepartmentSteps for SalesBusinessDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::SALES
}

fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Continue
}

fn check_entry(&self, context: &DepartmentContext) -> StepOutcome {
if !context.settled() {
return StepOutcome::failed(String::from("尚未结算,不做交付回访"));
}
StepOutcome::performed(String::from("已结算,可以回访"))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
DepartmentOutcome::new(
StepOutcome::performed(format!(
"回访完成:客户已收 {} 分,未收 {} 分",
context.received_minor(),
context.outstanding_minor()
)),
OrderMutation::none()
.with_work_minutes(WORK_MINUTES)
.with_note("业务:交付回访已登记"),
)
}

fn inspect(&self, _context: &DepartmentContext) -> StepOutcome {
StepOutcome::performed(String::from("自检通过:回访记录已留存"))
}

/// 钩子:覆盖移交说明(业务是端到端的最后一棒)。
fn handoff_note(&self) -> &'static str {
"交付回访完成,本单进入归档"
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : training_department.rs
//! 培训部门:工艺标准 / 质检标准 / 系统操作培训。
//!
//! ★ 本部门是**第十三个部门(扩展)的对照组**:
//! 工程外扩展区新增的部门与它写法完全一样,
//! 于是"新增一个部门要改几处"可以被数出来(答案:0 处既有分层文件)。

use crate::common::numeric::scale_rounded;
use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::business_rules::TRAINING_FEE_BASIS_POINTS;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::OrderMutation;

/// 培训工时(分钟)。
const WORK_MINUTES: i64 = 60;

/// 培训部门。
pub struct TrainingDepartment;

impl DepartmentSteps for TrainingDepartment {
fn department_code(&self) -> DepartmentCode {
DepartmentCode::TRAINING
}

fn failure_policy(&self) -> FailurePolicy {
FailurePolicy::Continue
}

fn check_entry(&self, _context: &DepartmentContext) -> StepOutcome {
StepOutcome::performed(String::from("培训无准入限制(任何阶段都可排课)"))
}

fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome {
let training_fee: i64 = scale_rounded(
i128::from(context.quoted_minor()),
i128::from(TRAINING_FEE_BASIS_POINTS),
10_000,
);
DepartmentOutcome::new(
StepOutcome::performed(format!(
"培训 {} 分钟完成,培训费 {} 分(费率 {} 万分点)",
WORK_MINUTES, training_fee, TRAINING_FEE_BASIS_POINTS
)),
OrderMutation::none()
.with_expense(training_fee)
.with_work_minutes(WORK_MINUTES)
.with_note("培训:工艺与系统操作培训已完成"),
)
}

fn inspect(&self, _context: &DepartmentContext) -> StepOutcome {
StepOutcome::performed(String::from("自检通过:培训签到记录已留存"))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : department_task.rs
//! 部门作业任务:**调度器的输入**。
//!
//! ★ 为什么"任务"要单独存在,而不是直接把工单丢给部门:
//! 一张工单要在**十二个部门**各作业一次,每次作业有不同的计划工时与优先级。
//! 「工单 × 部门」这个二元组若不存在,调度器就只能按工单粒度排,
//! 于是"质检排在加工前面"这种错误**没有任何地方能检查**。

use crate::domain::DepartmentCode;
use crate::domain::OrderNumber;

/// 一次部门作业任务。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepartmentTask {
/// 工单号。
order_number: OrderNumber,
/// 部门。
department_code: DepartmentCode,
/// 计划工时(分钟)。
planned_minutes: i64,
/// 优先级(**数字越小越先做**)。
priority: i64,
}

impl DepartmentTask {
/// 造一个任务。
pub fn new(
order_number: OrderNumber,
department_code: DepartmentCode,
planned_minutes: i64,
priority: i64,
) -> Self {
DepartmentTask {
order_number,
department_code,
planned_minutes,
priority,
}
}

/// 工单号。
pub const fn order_number(&self) -> &OrderNumber {
&self.order_number
}

/// 工单号文本。
pub fn order_number_text(&self) -> &str {
self.order_number.raw()
}

/// 部门。
pub const fn department_code(&self) -> DepartmentCode {
self.department_code
}

/// 计划工时(分钟)。
pub const fn planned_minutes(&self) -> i64 {
self.planned_minutes
}

/// 优先级。
pub const fn priority(&self) -> i64 {
self.priority
}
}

impl PartialOrd for DepartmentTask {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

impl Ord for DepartmentTask {
/// ★ 排序键**显式定义**(不派生:派生顺序由字段声明顺序决定,挪一下字段就反了)。
///
/// 口径:`优先级 → 部门 → 工单号`。
/// 第三键必须有:优先级与部门都并列时,若没有唯一定义,
/// 稳定排序会保留入参顺序,而入参顺序可能被调用方改掉 → 两次运行行序不同。
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.priority
.cmp(&other.priority)
.then_with(|| self.department_code.code().cmp(other.department_code.code()))
.then_with(|| self.order_number.raw().cmp(other.order_number.raw()))
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : evidence_record.rs
//! 留痕记录:骨架 `EVIDENCE` 那一步写下来的东西。
//!
//! ★ 为什么留痕是一个**带编码**的实体而不是一句字符串:
//! 「这一步做过了」要能被**查找**(自检问"质检报告留痕在不在"),
//! 字符串只能比对全文,而措辞一改就对不上。

use crate::domain::DepartmentCode;
use crate::domain::EvidenceCode;

/// 一条留痕。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvidenceRecord {
/// 留痕编码。
evidence_code: EvidenceCode,
/// 产生它的部门。
department_code: DepartmentCode,
/// 摘要(**要能复核**:带数值,不写"已完成"这种空话)。
summary: String,
}

impl EvidenceRecord {
/// 造一条留痕。
pub fn new(
evidence_code: EvidenceCode,
department_code: DepartmentCode,
summary: String,
) -> Self {
EvidenceRecord {
evidence_code,
department_code,
summary,
}
}

/// 留痕编码。
pub const fn evidence_code(&self) -> EvidenceCode {
self.evidence_code
}

/// 产生它的部门。
pub const fn department_code(&self) -> DepartmentCode {
self.department_code
}

/// 摘要。
pub fn summary(&self) -> &str {
self.summary.as_str()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : material_lot.rs
//! 物料批次:一张工单用掉的金料 / 宝石。
//!
//! ★ 成本在**批次自己**身上算,不在部门实现里算:
//! 「毫克 × 单价(分/克) ÷ 1000」是**物料的语义**,
//! 若让每个部门各写一遍这个除法,就会出现两种舍入口径,
//! 而它们在报表上长得一模一样。

use crate::common::numeric::divide_rounded;
use crate::domain::CurrencyCode;
use crate::domain::MaterialCode;
use crate::domain::Money;

/// 一个物料批次。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaterialLot {
/// 批次号。
lot_code: &'static str,
/// 物料编码(金 18K / 足金 / 铂金 / 钻石 / 翡翠…)。
material_code: MaterialCode,
/// 重量(**毫克**:整数最小单位,不用克,避免浮点)。
weight_milligrams: i64,
/// 单价(**分/克**)。
unit_price_minor_per_gram: i64,
/// 币种。
currency: CurrencyCode,
/// 成色(**万分点**:9900 = 99.00%)。
purity_basis_points: i64,
}

impl MaterialLot {
/// 造一个批次。
pub fn new(
lot_code: &'static str,
material_code: MaterialCode,
weight_milligrams: i64,
unit_price_minor_per_gram: i64,
currency: CurrencyCode,
purity_basis_points: i64,
) -> Self {
MaterialLot {
lot_code,
material_code,
weight_milligrams,
unit_price_minor_per_gram,
currency,
purity_basis_points,
}
}

/// 批次号。
pub const fn lot_code(&self) -> &'static str {
self.lot_code
}

/// 物料编码。
pub const fn material_code(&self) -> MaterialCode {
self.material_code
}

/// 重量(毫克)。
pub const fn weight_milligrams(&self) -> i64 {
self.weight_milligrams
}

/// 单价(分/克)。
pub const fn unit_price_minor_per_gram(&self) -> i64 {
self.unit_price_minor_per_gram
}

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

/// 成色(万分点)。
pub const fn purity_basis_points(&self) -> i64 {
self.purity_basis_points
}

/// ★ 批次成本:重量(毫克) × 单价(分/克) ÷ 1000,走**唯一舍入口径**。
///
/// ⚠️ 中间量用 `i128`:毫克与单价相乘很容易超过 `i32`,
/// 溢出在 release 下不 panic,只在报表上表现为"成本突然变小"。
pub fn cost(&self) -> Money {
let minor: i64 = divide_rounded(
i128::from(self.weight_milligrams) * i128::from(self.unit_price_minor_per_gram),
1000,
);
Money::new(minor, self.currency)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : order_mutation.rs
//! ★ 一次作业对工单的**变更**:骨架里「只有一步会改工单」。
//!
//! 为什么不做成"每个部门直接 `&mut ProductionOrder`":
//! 那样"哪一步改了工单"就散落在十二个部门实现里,
//! 于是「准入校验那一步不该改工单」这种纪律**没有任何地方能检查**。
//!
//! 做成显式变更后,这条纪律变成可数的:
//! 一次骨架执行里**至多一步**产生非空变更(自检会核对它)。

use crate::domain::EvidenceRecord;

/// 一次作业对工单的变更(**全部字段都是"增量"**)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderMutation {
/// 追加物料成本(分)。
material_cost_minor: i64,
/// 追加人工成本(分)。
labor_cost_minor: i64,
/// 追加费用(分:营销推广 / 行政 / 培训这类期间费用)。
expense_minor: i64,
/// 追加收款(分)。
received_minor: i64,
/// 追加工时(分钟)。
work_minutes: i64,
/// 质检结论(`None` = 不改)。
quality_passed: Option<bool>,
/// 是否已包装。
packaged: bool,
/// 是否已发运。
shipped: bool,
/// 是否已结算。
settled: bool,
/// 要追加的留痕。
evidence: Option<EvidenceRecord>,
/// 变更说明(写进日志与报表)。
note: &'static str,
}

impl OrderMutation {
/// 空变更(**默认就是这个**:骨架里除核心作业外的步骤都用它)。
pub const fn none() -> Self {
OrderMutation {
material_cost_minor: 0,
labor_cost_minor: 0,
expense_minor: 0,
received_minor: 0,
work_minutes: 0,
quality_passed: None,
packaged: false,
shipped: false,
settled: false,
evidence: None,
note: "无变更",
}
}

/// 追加物料成本。
pub const fn with_material_cost(mut self, minor: i64) -> Self {
self.material_cost_minor = minor;
self
}

/// 追加人工成本。
pub const fn with_labor_cost(mut self, minor: i64) -> Self {
self.labor_cost_minor = minor;
self
}

/// 追加费用。
pub const fn with_expense(mut self, minor: i64) -> Self {
self.expense_minor = minor;
self
}

/// 追加收款。
pub const fn with_received(mut self, minor: i64) -> Self {
self.received_minor = minor;
self
}

/// 追加工时。
pub const fn with_work_minutes(mut self, minutes: i64) -> Self {
self.work_minutes = minutes;
self
}

/// 设置质检结论。
pub const fn with_quality_passed(mut self, passed: bool) -> Self {
self.quality_passed = Some(passed);
self
}

/// 标记已包装。
pub const fn with_packaged(mut self) -> Self {
self.packaged = true;
self
}

/// 标记已发运。
pub const fn with_shipped(mut self) -> Self {
self.shipped = true;
self
}

/// 标记已结算。
pub const fn with_settled(mut self) -> Self {
self.settled = true;
self
}

/// 追加一条留痕。
pub fn with_evidence(mut self, evidence: EvidenceRecord) -> Self {
self.evidence = Some(evidence);
self
}

/// 变更说明。
pub const fn with_note(mut self, note: &'static str) -> Self {
self.note = note;
self
}

/// 物料成本增量。
pub const fn material_cost_minor(&self) -> i64 {
self.material_cost_minor
}

/// 人工成本增量。
pub const fn labor_cost_minor(&self) -> i64 {
self.labor_cost_minor
}

/// 费用增量。
pub const fn expense_minor(&self) -> i64 {
self.expense_minor
}

/// 收款增量。
pub const fn received_minor(&self) -> i64 {
self.received_minor
}

/// 工时增量。
pub const fn work_minutes(&self) -> i64 {
self.work_minutes
}

/// 质检结论。
pub const fn quality_passed(&self) -> Option<bool> {
self.quality_passed
}

/// 是否已包装。
pub const fn packaged(&self) -> bool {
self.packaged
}

/// 是否已发运。
pub const fn shipped(&self) -> bool {
self.shipped
}

/// 是否已结算。
pub const fn settled(&self) -> bool {
self.settled
}

/// 要追加的留痕。
pub const fn evidence(&self) -> Option<&EvidenceRecord> {
self.evidence.as_ref()
}

/// 变更说明。
pub const fn note(&self) -> &'static str {
self.note
}

/// ★ 金额变更的**代数和**(物料 + 人工 + 费用 + 收款)。
///
/// 「骨架里只有核心作业会改金额」这条纪律的判定点:
/// 核心作业没跑成功时,这里必须是 0。
pub const fn money_delta_minor(&self) -> i64 {
self.material_cost_minor
+ self.labor_cost_minor
+ self.expense_minor
+ self.received_minor
}

/// ★ 是不是"什么都没改"。
///
/// 除核心作业外的每一步,这个判断都必须是 `true`。
pub fn is_empty(&self) -> bool {
self.material_cost_minor == 0
&& self.labor_cost_minor == 0
&& self.expense_minor == 0
&& self.received_minor == 0
&& self.work_minutes == 0
&& self.quality_passed.is_none()
&& !self.packaged
&& !self.shipped
&& !self.settled
&& self.evidence.is_none()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : production_order.rs
//! 工单聚合根:**唯一有写权限的人**。
//!
//! ★ 与模板方法的关系(本工程的核心形状):
//! 部门实现(`DepartmentSteps`)拿到的是**只读快照** `DepartmentContext`,
//! 它交出的是**变更** `OrderMutation`;真正落笔的是本结构体的 `apply_mutation`。
//! 于是「部门改不了工单,只能申请改」是**类型层**的事,不是注释里的一句约定。
//!
//! ★ 这也正好解释了为什么 Rust 里模板方法必须长这样:
//! 教科书版让"具体子类"持有 `&mut` 上下文,在 Rust 里是借用冲突,写不出来;
//! 而写成「只读快照进、变更出」之后,骨架反而更干净了——
//! **每一步改了什么**是显式数据,可以数、可以自检。

use crate::common::numeric::divide_rounded;
use crate::common::numeric::ratio_basis_points;
use crate::domain::CurrencyCode;
use crate::domain::CustomerCode;
use crate::domain::DepartmentCode;
use crate::domain::EvidenceRecord;
use crate::domain::MaterialLot;
use crate::domain::Money;
use crate::domain::OrderMutation;
use crate::domain::OrderNumber;
use crate::domain::QualityReport;
use crate::domain::SettlementRecord;
use crate::domain::ShipmentRecord;
use crate::domain::WorkMinutes;
use crate::domain::DepartmentContext;

/// 一张生产工单。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProductionOrder {
/// 工单号。
order_number: OrderNumber,
/// 客户。
customer_code: CustomerCode,
/// 作品名。
product_name: String,
/// 币种。
currency: CurrencyCode,
/// 报价。
quoted_minor: i64,
/// 定金比率(万分点)。
deposit_ratio_basis_points: i64,
/// 已收定金(分)。
deposit_minor: i64,
/// 定金之外补收的货款(分)。
balance_minor: i64,
/// 结算时收的款(分)。
settled_minor: i64,
/// 物料成本(分)。
material_cost_minor: i64,
/// 人工成本(分)。
labor_cost_minor: i64,
/// 期间费用(分:营销 / 行政 / 培训)。
expense_minor: i64,
/// 累计工时(分钟)。
work_minutes: i64,
/// 物料批次。
lots: Vec<MaterialLot>,
/// 质检报告。
quality_reports: Vec<QualityReport>,
/// 发运记录。
shipments: Vec<ShipmentRecord>,
/// 收款记录。
settlements: Vec<SettlementRecord>,
/// 留痕。
evidences: Vec<EvidenceRecord>,
/// 已完成的部门(**按完成顺序**)。
completed_departments: Vec<DepartmentCode>,
/// 质检是否通过。
quality_passed: bool,
/// 是否已包装。
packaged: bool,
/// 是否已发运。
shipped: bool,
/// 是否已结算。
settled: bool,
}

impl ProductionOrder {
/// 新建一张工单。
pub fn new(
order_number: OrderNumber,
customer_code: CustomerCode,
product_name: &str,
quoted: Money,
) -> Self {
ProductionOrder {
order_number,
customer_code,
product_name: String::from(product_name),
currency: quoted.currency(),
quoted_minor: quoted.minor(),
deposit_ratio_basis_points: 0,
deposit_minor: 0,
balance_minor: 0,
settled_minor: 0,
material_cost_minor: 0,
labor_cost_minor: 0,
expense_minor: 0,
work_minutes: 0,
lots: Vec::new(),
quality_reports: Vec::new(),
shipments: Vec::new(),
settlements: Vec::new(),
evidences: Vec::new(),
completed_departments: Vec::new(),
quality_passed: false,
packaged: false,
shipped: false,
settled: false,
}
}

// ———————————————————————–
// 只读视图
// ———————————————————————–

/// 工单号。
pub const fn order_number(&self) -> &OrderNumber {
&self.order_number
}

/// 工单号文本。
pub fn order_number_text(&self) -> &str {
self.order_number.raw()
}

/// 客户。
pub const fn customer_code(&self) -> CustomerCode {
self.customer_code
}

/// 作品名。
pub fn product_name(&self) -> &str {
self.product_name.as_str()
}

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

/// 报价。
pub fn quoted_amount(&self) -> Money {
Money::new(self.quoted_minor, self.currency)
}

/// 定金比率(万分点)。
pub const fn deposit_ratio_basis_points(&self) -> i64 {
self.deposit_ratio_basis_points
}

/// 已收定金。
pub fn deposit_amount(&self) -> Money {
Money::new(self.deposit_minor, self.currency)
}

/// 补收货款。
pub fn balance_amount(&self) -> Money {
Money::new(self.balance_minor, self.currency)
}

/// 结算收款。
pub fn settled_amount(&self) -> Money {
Money::new(self.settled_minor, self.currency)
}

/// ★ 已收合计 = 定金 + 补收 + 结算。
///
/// ⚠️ 三项都算:只算定金会得出"尾款永远收不齐",
/// 而报表上那看起来就是"客户没付钱"。
pub fn received_amount(&self) -> Money {
Money::new(
self.deposit_minor + self.balance_minor + self.settled_minor,
self.currency,
)
}

/// 未收(尾款)= 报价 − 已收。
pub fn outstanding_amount(&self) -> Money {
Money::new(
self.quoted_minor – self.deposit_minor – self.balance_minor – self.settled_minor,
self.currency,
)
}

/// 物料成本。
pub fn material_cost(&self) -> Money {
Money::new(self.material_cost_minor, self.currency)
}

/// 人工成本。
pub fn labor_cost(&self) -> Money {
Money::new(self.labor_cost_minor, self.currency)
}

/// 期间费用。
pub fn expense_amount(&self) -> Money {
Money::new(self.expense_minor, self.currency)
}

/// 成本合计 = 物料 + 人工 + 费用。
pub fn cost_total(&self) -> Money {
Money::new(
self.material_cost_minor + self.labor_cost_minor + self.expense_minor,
self.currency,
)
}

/// 毛利 = 已收 − 成本。
pub fn profit_amount(&self) -> Money {
Money::new(
self.received_amount().minor() – self.cost_total().minor(),
self.currency,
)
}

/// 毛利率(万分点)= 毛利 / 报价。
pub fn margin_basis_points(&self) -> i64 {
ratio_basis_points(
i128::from(self.profit_amount().minor()),
i128::from(self.quoted_minor),
)
}

/// 累计工时。
pub fn work_minutes(&self) -> WorkMinutes {
WorkMinutes::new(self.work_minutes)
}

/// 物料批次。
pub fn lots(&self) -> &[MaterialLot] {
self.lots.as_slice()
}

/// 批次条数。
pub fn lot_total(&self) -> usize {
self.lots.len()
}

/// ★ 批次成本之和(**独立于** `material_cost_minor`,两条路径互盯)。
///
/// 为什么要有两条:`material_cost_minor` 是各部门**申请**累加出来的,
/// 由批次重算则是**从原始数据**来的。两者不等说明某部门少记或多记了一笔。
pub fn material_cost_from_lots(&self) -> Money {
let mut total: i64 = 0;
for lot in self.lots.iter() {
total += lot.cost().minor();
}
Money::new(total, self.currency)
}

/// 总重量(毫克)。
pub fn total_weight_milligrams(&self) -> i64 {
let mut total: i64 = 0;
for lot in self.lots.iter() {
total += lot.weight_milligrams();
}
total
}

/// ★ 最低成色(万分点):没有批次时返回 `10000`(= 100%,即"无从挑剔")。
///
/// ⚠️ 返回 `Option` 会让每个调用点都要处理空集,
/// 而"没有批次"在业务上与"成色满分"对准入校验的结论相同——
/// 这里刻意返回一个**中性值**并写清理由,而不是让调用方猜。
pub fn lowest_purity_basis_points(&self) -> i64 {
let mut lowest: i64 = 10_000;
for lot in self.lots.iter() {
if lot.purity_basis_points() < lowest {
lowest = lot.purity_basis_points();
}
}
lowest
}

/// 质检报告。
pub fn quality_reports(&self) -> &[QualityReport] {
self.quality_reports.as_slice()
}

/// 发运记录。
pub fn shipments(&self) -> &[ShipmentRecord] {
self.shipments.as_slice()
}

/// 收款记录。
pub fn settlements(&self) -> &[SettlementRecord] {
self.settlements.as_slice()
}

/// 留痕。
pub fn evidences(&self) -> &[EvidenceRecord] {
self.evidences.as_slice()
}

/// 已完成的部门(按完成顺序)。
pub fn completed_departments(&self) -> &[DepartmentCode] {
self.completed_departments.as_slice()
}

/// 已完成几个部门。
pub fn completed_department_total(&self) -> usize {
self.completed_departments.len()
}

/// 某部门是否已完成。
pub fn is_department_completed(&self, department_code: DepartmentCode) -> bool {
self.completed_departments
.iter()
.any(|code| *code == department_code)
}

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

/// 是否已包装。
pub const fn packaged(&self) -> bool {
self.packaged
}

/// 是否已发运。
pub const fn shipped(&self) -> bool {
self.shipped
}

/// 是否已签收(**全部**运单都签收才算)。
pub fn delivered(&self) -> bool {
!self.shipments.is_empty() && self.shipments.iter().all(|item| item.delivered())
}

/// 是否已结算。
pub const fn settled(&self) -> bool {
self.settled
}

/// ★ 交给部门的**只读快照**。
///
/// 快照是刻意"拍扁"的:部门拿到的是一批数值,不是 `&ProductionOrder`。
/// 于是部门**不可能**绕过骨架直接改工单——这是模板方法在 Rust 里能成立的前提。
pub fn context(&self) -> DepartmentContext {
DepartmentContext::new(
String::from(self.order_number_text()),
String::from(self.product_name()),
self.currency,
self.quoted_minor,
self.received_amount().minor(),
self.outstanding_amount().minor(),
self.material_cost_minor,
self.labor_cost_minor,
self.expense_minor,
self.deposit_ratio_basis_points,
self.lot_total(),
self.lowest_purity_basis_points(),
self.total_weight_milligrams(),
self.quality_passed,
self.packaged,
self.shipped,
self.delivered(),
self.settled,
self.completed_department_total(),
self.work_minutes,
self.material_cost_from_lots().minor(),
)
}

// ———————————————————————–
// 写入口(**全部**都要走这里)
// ———————————————————————–

/// 设置定金比率并收定金。
pub fn receive_deposit(&mut self, deposit: Money) {
debug_assert!(
deposit.currency() == self.currency,
"定金币种与工单不一致"
);
self.deposit_ratio_basis_points = divide_rounded(
i128::from(deposit.minor()) * 10_000,
i128::from(self.quoted_minor),
);
self.deposit_minor = deposit.minor();
}

/// 加一个物料批次。
pub fn add_material_lot(&mut self, lot: MaterialLot) {
self.lots.push(lot);
}

/// ★ 应用一次变更(**唯一的写入口**)。
///
/// 为什么不让各部门直接改字段:那样"哪一步改了工单"就散落十二处,
/// 「准入校验不该改工单」这条纪律将没有任何地方可以检查。
pub fn apply_mutation(&mut self, mutation: &OrderMutation) {
self.material_cost_minor += mutation.material_cost_minor();
self.labor_cost_minor += mutation.labor_cost_minor();
self.expense_minor += mutation.expense_minor();
self.work_minutes += mutation.work_minutes();
if mutation.received_minor() > 0 {
self.settled_minor += mutation.received_minor();
}
if let Some(passed) = mutation.quality_passed() {
self.quality_passed = passed;
}
if mutation.packaged() {
self.packaged = true;
}
if mutation.shipped() {
self.shipped = true;
}
if mutation.settled() {
self.settled = true;
}
if let Some(evidence) = mutation.evidence() {
self.evidences.push(evidence.clone());
}
}

/// 记一次收款(**独立于骨架**的用例:收款不改部门作业状态)。
pub fn receive_payment(&mut self, amount: Money) {
debug_assert!(amount.currency() == self.currency, "收款币种与工单不一致");
debug_assert!(amount.minor() >= 0, "收款金额不能为负");
self.balance_minor += amount.minor();
}

/// 加一份质检报告。
pub fn add_quality_report(&mut self, report: QualityReport) {
self.quality_passed = report.passed();
self.quality_reports.push(report);
}

/// 加一条发运记录。
pub fn add_shipment(&mut self, shipment: ShipmentRecord) {
self.shipments.push(shipment);
}

/// 全部运单标记签收。
pub fn mark_all_delivered(&mut self) {
let mut updated: Vec<ShipmentRecord> = Vec::new();
for shipment in self.shipments.iter() {
updated.push(shipment.marked_delivered());
}
self.shipments = updated;
}

/// 加一笔收款记录。
pub fn add_settlement(&mut self, settlement: SettlementRecord) {
self.settlements.push(settlement);
}

/// 记录某部门已完成。
pub fn record_department_completion(&mut self, department_code: DepartmentCode) {
if !self.is_department_completed(department_code) {
self.completed_departments.push(department_code);
}
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : quality_report.rs
//! 质检报告:质检部门 `PERFORM` 那一步的产出。
//!
//! ★ 报告要带**成色实测值**而不只是"合格/不合格":
//! 「合格」是结论,「为什么合格」是证据。只有结论时,
//! 报表上"这一单为什么过"永远无法复核。

use crate::domain::EmployeeCode;

/// 一份质检报告。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QualityReport {
/// 报告编号。
report_code: String,
/// 是否合格。
passed: bool,
/// 实测成色(万分点)。
measured_purity_basis_points: i64,
/// 检验员。
inspector_employee_code: EmployeeCode,
/// 结论文本。
conclusion: String,
}

impl QualityReport {
/// 造一份报告。
pub fn new(
report_code: String,
passed: bool,
measured_purity_basis_points: i64,
inspector_employee_code: EmployeeCode,
conclusion: String,
) -> Self {
QualityReport {
report_code,
passed,
measured_purity_basis_points,
inspector_employee_code,
conclusion,
}
}

/// 报告编号。
pub fn report_code(&self) -> &str {
self.report_code.as_str()
}

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

/// 实测成色(万分点)。
pub const fn measured_purity_basis_points(&self) -> i64 {
self.measured_purity_basis_points
}

/// 检验员。
pub const fn inspector_employee_code(&self) -> EmployeeCode {
self.inspector_employee_code
}

/// 结论文本。
pub fn conclusion(&self) -> &str {
self.conclusion.as_str()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : settlement_record.rs
//! 收款记录:财务部门 `PERFORM` 那一步的产出。
//!
//! ★ 与 `Money` 分开成一条记录(而不是只给工单加一个数字):
//! 「收了几笔、每笔从哪个渠道来」是**对账**要问的,
//! 只剩一个合计时,"少收了一笔"与"收错了一笔"都无法分辨。

use crate::domain::Money;

/// 一笔收款。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SettlementRecord {
/// 收款流水号。
record_code: String,
/// 金额。
amount: Money,
/// 渠道(`DEPOSIT` 定金 / `BALANCE` 尾款 / `REFUND` 退款)。
channel: &'static str,
}

impl SettlementRecord {
/// 造一笔收款记录。
pub fn new(record_code: String, amount: Money, channel: &'static str) -> Self {
SettlementRecord {
record_code,
amount,
channel,
}
}

/// 收款流水号。
pub fn record_code(&self) -> &str {
self.record_code.as_str()
}

/// 金额。
pub const fn amount(&self) -> Money {
self.amount
}

/// 渠道。
pub const fn channel(&self) -> &'static str {
self.channel
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : shipment_record.rs
//! 发运记录:物流部门 `PERFORM` 那一步的产出。
//!
//! ★ 承运商是**文本**而不是枚举:承运商会不断增加,
//! 用枚举会让"接一家新承运商"变成改领域文件。

/// 一条发运记录。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShipmentRecord {
/// 运单号。
tracking_code: String,
/// 承运商名。
carrier_name: &'static str,
/// 是否已签收。
delivered: bool,
}

impl ShipmentRecord {
/// 造一条发运记录。
pub fn new(tracking_code: String, carrier_name: &'static str) -> Self {
ShipmentRecord {
tracking_code,
carrier_name,
delivered: false,
}
}

/// 运单号。
pub fn tracking_code(&self) -> &str {
self.tracking_code.as_str()
}

/// 承运商名。
pub const fn carrier_name(&self) -> &'static str {
self.carrier_name
}

/// 是否已签收。
pub const fn delivered(&self) -> bool {
self.delivered
}

/// 标记已签收(**返回新的一份**:值对象不可变,"改"就是换一份)。
pub fn marked_delivered(&self) -> Self {
ShipmentRecord {
tracking_code: self.tracking_code.clone(),
carrier_name: self.carrier_name,
delivered: true,
}
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : order_repository.rs
//! 端口:工单仓储(**接口在领域,实现在基础设施**)。
//!
//! ⚠️ 方法全是 `&self`:实现方用内部可变性(`RwLock` / `Mutex`)保证线程安全,
//! 而不是把 `&mut self` 暴露给调用方——`&mut` 一旦出现在共享仓储上,
//! 就没有任何办法让它被多线程同时使用。

use crate::domain::ProductionOrder;

/// 工单仓储。
pub trait OrderRepository: Send + Sync {
/// 保存(新建或覆盖)。
fn save(&self, order: ProductionOrder);

/// ★ 乐观并发写入:**只有"我读到的进度"没被别人改过才写成功**。
///
/// 为什么必须有它:`find` → 计算 → `save` 三步之间,另一个线程可能已经推进了同一张单。
/// 若直接覆盖,后写的会把先写的**静默吞掉**——不报错、不告警,
/// 只在报表上表现为"这张单少走了一个部门"。
///
/// 返回 `false` 表示"有人抢先了",调用方应当重读再算,而不是重试同一个写入。
fn save_expecting_completed_count(&self, expected_count: usize, order: ProductionOrder) -> bool;

/// 按工单号查找。
fn find_by_number(&self, order_number_text: &str) -> Option<ProductionOrder>;

/// 全部工单(**返回前必须排序**,否则两次运行的报表行序会不同)。
fn all(&self) -> Vec<ProductionOrder>;

/// 工单总数。
fn count(&self) -> usize;

/// 仓储实现的名字(报表用,证明"换实现不用改业务代码")。
fn implementation_name(&self) -> &'static str;
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : remote_gateway.rs
//! 端口:外部系统网关(**接口在领域,适配器在基础设施**)。
//!
//! 采购核验系统、物流轨迹、财务对账、IT 工单——
//! 这些外部系统对领域来说形状相同:**发一个请求,等一个结果**。
//! 重试与熔断不该写在每个适配器里,而是包在外面的装饰器上
//! (见 `infrastructure::adapters::RetryingGateway`)。

/// 一次外部调用的请求。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayRequest {
/// 工单号文本。
order_number_text: String,
/// 操作名(形如 `TRACE_SHIPMENT`)。
operation: &'static str,
}

impl GatewayRequest {
/// 构造一个请求。
pub fn new(order_number_text: String, operation: &'static str) -> Self {
GatewayRequest {
order_number_text,
operation,
}
}

/// 工单号文本。
pub fn order_number_text(&self) -> &str {
self.order_number_text.as_str()
}

/// 操作名。
pub const fn operation(&self) -> &'static str {
self.operation
}
}

/// 外部调用的结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayResponse {
/// 是否成功。
succeeded: bool,
/// 结果说明(失败时给原因)。
message: String,
}

impl GatewayResponse {
/// 成功。
pub fn success(message: String) -> Self {
GatewayResponse {
succeeded: true,
message,
}
}

/// 失败。
pub fn failure(message: String) -> Self {
GatewayResponse {
succeeded: false,
message,
}
}

/// 是否成功。
pub const fn succeeded(&self) -> bool {
self.succeeded
}

/// 结果说明。
pub fn message(&self) -> &str {
self.message.as_str()
}
}

/// 外部系统网关。
pub trait RemoteGateway: Send + Sync {
/// 外部系统名(日志与报表用)。
fn system_name(&self) -> &'static str;

/// ★ 原始调用:**不做重试、不做熔断**(那些由装饰器负责)。
///
/// 这一条边界很重要:若每个适配器自己实现重试,
/// "重试了几次"就会散落在五处,报表上永远数不清。
fn invoke(&self, request: &GatewayRequest) -> GatewayResponse;
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : business_rules.rs
//! 业务口径常量:**唯一产地**。
//!
//! ★ 为什么阈值必须集中在一处:
//! 「成色下限 9900」这类数字会出现在准入校验、自检、报表说明三处。
//! 分散写的结果是改一处漏两处,而**漏掉的那处在报表上完全合理**。
//!
//! ⚠️ 常量是 `i64` 的裸值而不是包装类型:它们只参与比较与乘除,
//! 包装成"值对象"只会让每个调用点多写一层 `.value()`,不带来任何新的不变量。

/// 成色下限(万分点):9900 = 99.00%。
pub const MIN_PURITY_BASIS_POINTS: i64 = 9_900;

/// 定金比率下限(万分点):3000 = 30%。
pub const MIN_DEPOSIT_RATIO_BASIS_POINTS: i64 = 3_000;

/// 设计费率(万分点):报价 × 2.00%。
pub const DESIGN_FEE_BASIS_POINTS: i64 = 200;

/// 加工费率(万分点):报价 × 4.50%。
pub const MACHINING_FEE_BASIS_POINTS: i64 = 450;

/// 包装材料费率(万分点):报价 × 0.80%。
pub const PACKAGING_FEE_BASIS_POINTS: i64 = 80;

/// 运资率(万分点):报价 × 1.20%。
pub const FREIGHT_FEE_BASIS_POINTS: i64 = 120;

/// 推广费率(万分点):报价 × 1.50%。
pub const PROMOTION_FEE_BASIS_POINTS: i64 = 150;

/// IT 支持费率(万分点):报价 × 0.20%。
pub const IT_FEE_BASIS_POINTS: i64 = 20;

/// 培训费率(万分点):报价 × 0.30%。
pub const TRAINING_FEE_BASIS_POINTS: i64 = 30;

/// 人力成本单价(**分/分钟**):120 分 = 1.20 元/分钟。
pub const LABOR_COST_MINOR_PER_MINUTE: i64 = 120;

/// 计划工时表:给调度器用(分钟),顺序与 `DepartmentCode::BUILTIN_ALL` 一致。
pub const PLANNED_MINUTES: [i64; 12] = [60, 120, 600, 90, 45, 90, 60, 60, 30, 40, 25, 60];
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : department_context.rs
//! ★ 交给部门的**只读快照**。
//!
//! 为什么不是直接给 `&ProductionOrder`:
//! 「具体步骤只能读、不能改」若靠注释,第一个赶工期的人就会绕过它;
//! 给一个拍扁的快照,部门**在类型上**就没有改工单的能力。
//! 这是模板方法能在 Rust 里成立的前提(教科书版给的是 `&mut Context`,这里写不出来)。
//!
//! ⚠️ 快照字段刻意**全是数值**:
//! 一旦某个字段是引用,部门的"只读"就依赖生命周期,而那是可以被绕过的。

use crate::domain::CurrencyCode;

/// 部门作业上下文(只读)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepartmentContext {
/// 工单号文本。
order_number_text: String,
/// 作品名。
product_name: String,
/// 币种。
currency: CurrencyCode,
/// 报价(分)。
quoted_minor: i64,
/// 已收(分)。
received_minor: i64,
/// 未收(分)。
outstanding_minor: i64,
/// 物料成本(分)。
material_cost_minor: i64,
/// 人工成本(分)。
labor_cost_minor: i64,
/// 期间费用(分)。
expense_minor: i64,
/// 定金比率(万分点)。
deposit_ratio_basis_points: i64,
/// 物料批次数。
material_lot_total: usize,
/// 最低成色(万分点)。
lowest_purity_basis_points: i64,
/// 总重量(毫克)。
total_weight_milligrams: i64,
/// 质检是否通过。
quality_passed: bool,
/// 是否已包装。
packaged: bool,
/// 是否已发运。
shipped: bool,
/// 是否已签收。
delivered: bool,
/// 是否已结算。
settled: bool,
/// 已完成部门数。
completed_department_total: usize,
/// 累计工时(分钟)。
work_minutes: i64,
/// ★ 由**批次原始数据**重算出来的物料成本(分)。
///
/// 为什么上下文里要同时带"已记账的物料成本"与"重算的物料成本":
/// 这两条路径的差异就是"某部门少记或多记了一笔"的证据。
/// 只带一条时,那个差异根本不存在——写错了也没有任何迹象。
lot_derived_material_cost_minor: i64,
}

impl DepartmentContext {
///
/// ⚠️ 参数多是因为"拍扁"是刻意的:部门需要的一切都在这里,
/// 一个引用都不带(带引用就能被绕过)。不要为了参数少而改成传 `&ProductionOrder`。
pub fn new(
order_number_text: String,
product_name: String,
currency: CurrencyCode,
quoted_minor: i64,
received_minor: i64,
outstanding_minor: i64,
material_cost_minor: i64,
labor_cost_minor: i64,
expense_minor: i64,
deposit_ratio_basis_points: i64,
material_lot_total: usize,
lowest_purity_basis_points: i64,
total_weight_milligrams: i64,
quality_passed: bool,
packaged: bool,
shipped: bool,
delivered: bool,
settled: bool,
completed_department_total: usize,
work_minutes: i64,
lot_derived_material_cost_minor: i64,
) -> Self {
DepartmentContext {
order_number_text,
product_name,
currency,
quoted_minor,
received_minor,
outstanding_minor,
material_cost_minor,
labor_cost_minor,
expense_minor,
deposit_ratio_basis_points,
material_lot_total,
lowest_purity_basis_points,
total_weight_milligrams,
quality_passed,
packaged,
shipped,
delivered,
settled,
completed_department_total,
work_minutes,
lot_derived_material_cost_minor,
}
}

/// 工单号文本。
pub fn order_number_text(&self) -> &str {
self.order_number_text.as_str()
}

/// 作品名。
pub fn product_name(&self) -> &str {
self.product_name.as_str()
}

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

/// 报价(分)。
pub const fn quoted_minor(&self) -> i64 {
self.quoted_minor
}

/// 已收(分)。
pub const fn received_minor(&self) -> i64 {
self.received_minor
}

/// 未收(分)。
pub const fn outstanding_minor(&self) -> i64 {
self.outstanding_minor
}

/// 物料成本(分)。
pub const fn material_cost_minor(&self) -> i64 {
self.material_cost_minor
}

/// 人工成本(分)。
pub const fn labor_cost_minor(&self) -> i64 {
self.labor_cost_minor
}

/// 期间费用(分)。
pub const fn expense_minor(&self) -> i64 {
self.expense_minor
}

/// 定金比率(万分点)。
pub const fn deposit_ratio_basis_points(&self) -> i64 {
self.deposit_ratio_basis_points
}

/// 物料批次数。
pub const fn material_lot_total(&self) -> usize {
self.material_lot_total
}

/// 最低成色(万分点)。
pub const fn lowest_purity_basis_points(&self) -> i64 {
self.lowest_purity_basis_points
}

/// 总重量(毫克)。
pub const fn total_weight_milligrams(&self) -> i64 {
self.total_weight_milligrams
}

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

/// 是否已包装。
pub const fn packaged(&self) -> bool {
self.packaged
}

/// 是否已发运。
pub const fn shipped(&self) -> bool {
self.shipped
}

/// 是否已签收。
pub const fn delivered(&self) -> bool {
self.delivered
}

/// 是否已结算。
pub const fn settled(&self) -> bool {
self.settled
}

/// 已完成部门数。
pub const fn completed_department_total(&self) -> usize {
self.completed_department_total
}

/// 累计工时(分钟)。
pub const fn work_minutes(&self) -> i64 {
self.work_minutes
}

/// 由批次原始数据重算的物料成本(分)。
pub const fn lot_derived_material_cost_minor(&self) -> i64 {
self.lot_derived_material_cost_minor
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : department_steps.rs
//! ★ 部门作业步骤:**部门唯一要实现的东西**。
//!
//! 这个 trait 里**没有骨架**——骨架在 `process_runner::DepartmentProcessRunner::execute`
//! (一个 inherent 方法)。这是本工程最重要的设计决定,理由写在 `process_runner.rs` 开头。
//!
//! trait 里只有三类成员:
//! 1. **抽象步骤**(必须实现):`check_entry` / `perform` / `inspect`;
//! 2. **契约**(必须实现):`department_code` / `failure_policy`;
//! 3. **钩子**(可选覆盖,默认关闭):外部系统 / 追加留痕 / 移交说明。
//!
//! ★ 钩子必须有**默认值**,而且默认值必须是"不生效":
//! 钩子是"我可以加严,不可以豁免"的那一类扩展点。
//! 默认值一旦是"生效",大多数实现者就会忘记回答这个问题,
//! 于是"没配"与"配了不用"长得分毫不差。

use crate::common::template_method::FailurePolicy;
use crate::common::template_method::StepOutcome;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::OrderMutation;

/// 一次作业的产出:**结果 + 变更**。
///
/// 为什么结果里要带变更:部门拿不到 `&mut` 工单(那是骨架与聚合根的权力),
/// 它只能**申请**变更。于是"这一步想改什么"是显式数据,可被自检数出来。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepartmentOutcome {
/// 这一步的结果(三态)。
outcome: StepOutcome,
/// 想施加到工单上的变更。
mutation: OrderMutation,
}

impl DepartmentOutcome {
/// 造一个产出。
pub fn new(outcome: StepOutcome, mutation: OrderMutation) -> Self {
DepartmentOutcome { outcome, mutation }
}

/// 只给结果、不改工单(**准入与自检都应该这样**)。
pub fn without_mutation(outcome: StepOutcome) -> Self {
DepartmentOutcome {
outcome,
mutation: OrderMutation::none(),
}
}

/// 结果。
pub const fn outcome(&self) -> &StepOutcome {
&self.outcome
}

/// 变更。
pub const fn mutation(&self) -> &OrderMutation {
&self.mutation
}
}

/// 部门作业步骤。
pub trait DepartmentSteps: Send + Sync {
/// 我是哪个部门。
fn department_code(&self) -> DepartmentCode;

/// ★ 失败策略:这一步不过之后还要不要继续。
///
/// 放在 trait 上而不是由骨架统一决定:
/// 「准入不过 → 后面全没意义」与「留痕没写进去 → 作业本身已完成」
/// 是两种业务判断,只有部门自己说得清。
fn failure_policy(&self) -> FailurePolicy;

/// 抽象步骤一:**准入校验**(不许改工单)。
fn check_entry(&self, context: &DepartmentContext) -> StepOutcome;

/// 抽象步骤二:**核心作业**(唯一可以改金额的一步)。
fn perform(&self, context: &DepartmentContext) -> DepartmentOutcome;

/// 抽象步骤三:**本部门自检**(不许改工单)。
fn inspect(&self, context: &DepartmentContext) -> StepOutcome;

// ———————————————————————–
// 钩子(**默认关闭**:可以加严,不可以豁免)
// ———————————————————————–

/// 钩子一:本部门是否依赖外部系统(默认 `false`)。
///
/// 覆盖为 `true` 时,骨架的 `EXTERNAL` 那一步会真的发一次调用;
/// 不覆盖时那一步的结果是 `Skipped`——**这就是三态里"跳过"的用途**。
fn requires_external_system(&self) -> bool {
false
}

/// 钩子二:要调的那个操作名(**只在 `requires_external_system()` 为真时才有意义**)。
fn external_operation(&self) -> &'static str {
"NO_OPERATION"
}

/// 钩子三:追加一句留痕说明(默认 `None`)。
///
/// ⚠️ 返回 `Option<String>` 而不是 `String`:
/// 「没有补充」与「补充了空串」在报表上是两件事。
fn extra_evidence_note(&self, _context: &DepartmentContext) -> Option<String> {
None
}

/// 钩子四:移交说明(默认一句通用话术)。
fn handoff_note(&self) -> &'static str {
"本部门作业完成,移交下游"
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : process_runner.rs
//! ★★ 骨架本体:模板方法在 Rust 里唯一站得住的形状。
//!
//! ### 为什么骨架是**结构体的 inherent 方法**,而不是 trait 的默认方法
//!
//! 教科书:抽象基类里一个 `final templateMethod()`,调用若干 `abstract` 步骤。
//! Rust 没有继承,也没有 `final`。两种候选:
//!
//! | 写法 | 骨架能不能被改 | 结论 |
//! |—|—|—|
//! | trait 默认方法 `fn run(&self) { … }` | **能**(实现类照抄一遍签名就覆盖了) | ❌ 骨架只是"默认行为" |
//! | 结构体的 inherent 方法(**本文件**) | **不能**(inherent 方法无法被任何 trait 实现覆盖) | ✅ 骨 |
//!
//! 这不是风格偏好,是**能不能把"骨架不可变"这句话写出来**的问题。
//! 反例见 `domain/counterexamples/overridable_skeleton`:同样的七步写成 trait 默认方法,
//! 一个实现类覆盖了它,自检与报表都发现不了——因为"跑了七步"这句话从来没被检查过。
//!
//! ### 骨架里三类步骤的归属
//! – 骨架自己做:`LOAD` / `EVIDENCE` / `HANDOFF`;
//! – 部门实现:`ENTRY` / `PERFORM` / `INSPECT`;
//! – 钩子决定做不做:`EXTERNAL`(不做时记 `Skipped`,**不是**从序列里消失)。

use std::sync::Arc;

use crate::common::template_method::FailurePolicy;
use crate::common::template_method::SkeletonStep;
use crate::common::template_method::SkeletonTrace;
use crate::common::template_method::StepCode;
use crate::common::template_method::StepOutcome;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentContext;
use crate::domain::DepartmentOutcome;
use crate::domain::DepartmentSteps;
use crate::domain::EvidenceRecord;
use crate::domain::GatewayRequest;
use crate::domain::OrderMutation;
use crate::domain::RemoteGateway;
use crate::domain::SkeletonStepCode;
use crate::domain::SKELETON_STEP_SEQUENCE;

/// 一次骨架执行的结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepartmentRunResult {
/// 哪个部门。
department_code: DepartmentCode,
/// 完整轨迹(**唯一物证**:这一次到底跑到哪一步)。
trace: SkeletonTrace,
/// 对工单的变更(**只有核心作业成功时**才有金额变更)。
mutation: OrderMutation,
/// 外部系统调用的详情文本(没调时是空串)。
external_detail: String,
}

impl DepartmentRunResult {
/// 构造(**只在骨架内部调用**)。
fn new(
department_code: DepartmentCode,
trace: SkeletonTrace,
mutation: OrderMutation,
external_detail: String,
) -> Self {
DepartmentRunResult {
department_code,
trace,
mutation,
external_detail,
}
}

/// 哪个部门。
pub const fn department_code(&self) -> DepartmentCode {
self.department_code
}

/// 轨迹。
pub const fn trace(&self) -> &SkeletonTrace {
&self.trace
}

/// 变更。
pub const fn mutation(&self) -> &OrderMutation {
&self.mutation
}

/// 外部调用详情。
pub fn external_detail(&self) -> &str {
self.external_detail.as_str()
}

/// 这次作业是否跑通(没有中止、没有失败)。
pub fn succeeded(&self) -> bool {
self.trace.all_performed()
}

/// ★ 金额变更是否只来自核心作业(**可自检的纪律**)。
///
/// 判据:核心作业那一步不是"已执行"时,金额变更必须是 0。
/// 若某天有人让"准入校验"也改金额,这条会立刻变红。
pub fn money_change_is_legal(&self) -> bool {
let perform_ok: bool = self
.trace
.outcome_of(SkeletonStepCode::PERFORM)
.map(|outcome| outcome.kind().was_executed())
.unwrap_or(false);
if perform_ok {
return true;
}
self.mutation.money_delta_minor() == 0
}
}

/// 骨架执行器:持有一个部门实现,跑那七步。
///
/// ⚠️ `steps` 是 `Arc<dyn DepartmentSteps>`:骨架本身**无状态**,
/// 于是同一个骨架可以被多个 worker 线程同时 `execute`(高并发的前提)。
pub struct DepartmentProcessRunner {
/// 部门实现(只有步骤与钩子)。
steps: Arc<dyn DepartmentSteps>,
/// 声明的步骤序列(**固定七步**)。
declared: Vec<StepCode>,
}

impl DepartmentProcessRunner {
/// 为一个部门实现装上骨架。
pub fn new(steps: Arc<dyn DepartmentSteps>) -> Self {
DepartmentProcessRunner {
steps,
declared: SKELETON_STEP_SEQUENCE.to_vec(),
}
}

/// 部门实现。
pub fn steps(&self) -> &Arc<dyn DepartmentSteps> {
&self.steps
}

/// 声明的步骤序列。
pub fn declared_steps(&self) -> &[StepCode] {
self.declared.as_slice()
}

/// 部门号。
pub fn department_code(&self) -> DepartmentCode {
self.steps.department_code()
}

/// ★★ 跑一遍骨架:**这个方法不可能被覆盖**。
///
/// `gateway` 为 `None` 表示当前装配没有外部系统——此时若部门声明了需要外部系统,
/// 那一步记为 `Skipped` 并写明"未装配",**绝不**记为成功。
pub fn execute(
&self,
context: &DepartmentContext,
gateway: Option<&dyn RemoteGateway>,
) -> DepartmentRunResult {
let policy: FailurePolicy = self.steps.failure_policy();
let mut trace: SkeletonTrace = SkeletonTrace::new(self.declared.clone(), policy);
let mut index: usize = 0;
let mut mutation: OrderMutation = OrderMutation::none();
let mut external_detail: String = String::new();

// —— 第一步 LOAD:骨架自己读上下文 ——
trace.record(SkeletonStep::new(
index,
SkeletonStepCode::LOAD,
StepOutcome::performed(format!(
"载入上下文:工单 {},作品 {},已完成 {} 个部门",
context.order_number_text(),
context.product_name(),
context.completed_department_total()
)),
));
index += 1;

// —— 第二步 ENTRY:部门实现(**不许改工单**) ——
let entry_outcome: StepOutcome = self.steps.check_entry(context);
let entry_failed: bool = entry_outcome.is_failed();
trace.record(SkeletonStep::new(index, SkeletonStepCode::ENTRY, entry_outcome));
index += 1;
if entry_failed && policy.should_stop_on_failure() {
trace.mark_aborted();
return DepartmentRunResult::new(
self.steps.department_code(),
trace,
OrderMutation::none(),
external_detail,
);
}

// —— 第三步 PERFORM:部门实现(**唯一可以改金额的一步**) ——
let performed: DepartmentOutcome = self.steps.perform(context);
let perform_failed: bool = performed.outcome().is_failed();
trace.record(SkeletonStep::new(
index,
SkeletonStepCode::PERFORM,
performed.outcome().clone(),
));
index += 1;
if perform_failed {
// ★ 失败**不**带出变更:作业没做成,却把成本记上了,是最难发现的一类错账。
if policy.should_stop_on_failure() {
trace.mark_aborted();
return DepartmentRunResult::new(
self.steps.department_code(),
trace,
OrderMutation::none(),
external_detail,
);
}
} else {
mutation = performed.mutation().clone();
}

// —— 第四步 EXTERNAL:钩子决定做不做 ——
let external_outcome: StepOutcome = if self.steps.requires_external_system() {
match gateway {
Some(gateway_impl) => {
let request: GatewayRequest = GatewayRequest::new(
String::from(context.order_number_text()),
self.steps.external_operation(),
);
let response = gateway_impl.invoke(&request);
external_detail = format!(
"{} → {}",
gateway_impl.system_name(),
response.message()
);
if response.succeeded() {
StepOutcome::performed(String::from(response.message()))
} else {
StepOutcome::failed(String::from(response.message()))
}
}
// ★ 装配里没有外部系统:记 Skipped 并写明原因,
// 绝不当作成功(那会让"没接系统"变成"系统正常")。
None => StepOutcome::skipped(String::from("未装配外部系统网关,跳过外呼")),
}
} else {
StepOutcome::skipped(String::from("本部门不依赖外部系统"))
};
let external_failed: bool = external_outcome.is_failed();
trace.record(SkeletonStep::new(
index,
SkeletonStepCode::EXTERNAL,
external_outcome,
));
index += 1;
if external_failed && policy.should_stop_on_failure() {
trace.mark_aborted();
return DepartmentRunResult::new(
self.steps.department_code(),
trace,
mutation,
external_detail,
);
}

// —— 第五步 INSPECT:部门实现(不许改工单) ——
let inspect_outcome: StepOutcome = self.steps.inspect(context);
let inspect_failed: bool = inspect_outcome.is_failed();
trace.record(SkeletonStep::new(
index,
SkeletonStepCode::INSPECT,
inspect_outcome,
));
index += 1;
if inspect_failed && policy.should_stop_on_failure() {
trace.mark_aborted();
return DepartmentRunResult::new(
self.steps.department_code(),
trace,
// ★ 自检不过:作业做过了,但这一单**不合格**,
// 因此不把这次的变更带出去(避免"不合格品也计了成本")。
OrderMutation::none(),
external_detail,
);
}

// —— 第六步 EVIDENCE:骨架统一留痕 ——
let mut evidence_text: String = format!(
"{}:{}",
self.steps.department_code().code(),
mutation.note()
);
if let Some(extra) = self.steps.extra_evidence_note(context) {
evidence_text.push_str(";");
evidence_text.push_str(extra.as_str());
}
mutation = mutation.with_evidence(EvidenceRecord::new(
crate::domain::EvidenceCode::new(self.steps.department_code().code()),
self.steps.department_code(),
evidence_text.clone(),
));
trace.record(SkeletonStep::new(
index,
SkeletonStepCode::EVIDENCE,
StepOutcome::performed(evidence_text),
));
index += 1;

// —— 第七步 HANDOFF:骨架统一移交 ——
trace.record(SkeletonStep::new(
index,
SkeletonStepCode::HANDOFF,
StepOutcome::performed(String::from(self.steps.handoff_note())),
));

DepartmentRunResult::new(
self.steps.department_code(),
trace,
mutation,
external_detail,
)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : skeleton_steps.rs
//! ★ 业务骨架的**定义**:七步,顺序固定。
//!
//! 为什么"哪七步、按什么顺序"必须在领域层而不是机制层:
//! 这是**业务知识**(领单 → 准入 → 作业 → 外呼 → 自检 → 留痕 → 移交),
//! 换一个行业它就不是这七步。若把它塞进 `common::template_method`,
//! 换业务就得改机制层——机制从此不再是机制。
//!
//! ★ `EXTERNAL` 为什么**永远在声明序列里**(即使某部门不用外部系统):
//! 不用它时那一步的结果是 `Skipped`,而不是"这一位凭空消失"。
//! 三态里的"跳过"正是为此存在的——
//! 若按需裁剪序列,"这个部门没调外部系统"与"调了但失败"在报表上都表现为"少一步"。

use crate::common::template_method::StepCode;

/// 骨架步骤码(本工程内置的那七步)。
pub struct SkeletonStepCode;

impl SkeletonStepCode {
/// 第一步:领单(骨架自己读上下文)。
pub const LOAD: StepCode = StepCode::new("LOAD");
/// 第二步:准入校验(**部门实现**)。
pub const ENTRY: StepCode = StepCode::new("ENTRY");
/// 第三步:核心作业(**部门实现**,唯一会改金额的一步)。
pub const PERFORM: StepCode = StepCode::new("PERFORM");
/// 第四步:外部系统调用(**钩子**:部门声明要不要它)。
pub const EXTERNAL: StepCode = StepCode::new("EXTERNAL");
/// 第五步:部门自检(**部门实现**)。
pub const INSPECT: StepCode = StepCode::new("INSPECT");
/// 第六步:留痕(骨架统一做)。
pub const EVIDENCE: StepCode = StepCode::new("EVIDENCE");
/// 第七步:移交(骨架统一做)。
pub const HANDOFF: StepCode = StepCode::new("HANDOFF");
}

/// ★ 骨架声明的完整步骤序列(**顺序即骨架**)。
///
/// ⚠️ 这是数组而不是 `HashMap`:顺序必须唯一确定,
/// 否则两次运行的"实跑顺序"自检(A1)会随机失败。
pub const SKELETON_STEP_SEQUENCE: [StepCode; 7] = [
SkeletonStepCode::LOAD,
SkeletonStepCode::ENTRY,
SkeletonStepCode::PERFORM,
SkeletonStepCode::EXTERNAL,
SkeletonStepCode::INSPECT,
SkeletonStepCode::EVIDENCE,
SkeletonStepCode::HANDOFF,
];

/// 这一步是不是由**部门**实现(而不是骨架自己做的)。
///
/// 报表要用它来区分「骨架定的」与「部门填的」——
/// 读者最想知道的就是"我扩展一个新部门要写哪几步"。
/// ⚠️ 这两个判断刻意**不是** `const fn`:`StepCode` 的 `PartialEq` 是派生的,
/// 派生的比较不是 const,放进 `const fn` 会报 `E0015`。
/// 想为它加上 const 就得手写 `impl const PartialEq`,那是为一句判断付出的过度设计。
pub fn is_department_owned(step_code: StepCode) -> bool {
step_code == SkeletonStepCode::ENTRY
|| step_code == SkeletonStepCode::PERFORM
|| step_code == SkeletonStepCode::INSPECT
}

/// 这一步是不是由**钩子**决定要不要真的做。
pub fn is_hook_driven(step_code: StepCode) -> bool {
step_code == SkeletonStepCode::EXTERNAL
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : currency_code.rs
//! 币种:**封闭集合 → `enum`**。
//!
//! 判据:币种会被当作**查找键**(汇率表、报表分组),且真实业务里集合有限,
//! 所以是 enum。若哪天要支持"任意币种",再改成开放型结构体。

/// 币种。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CurrencyCode {
/// 人民币。
CNY,
/// 港币。
HKD,
/// 美元。
USD,
}

impl CurrencyCode {
/// 全部币种(固定顺序)。
pub const ALL: [CurrencyCode; 3] = [CurrencyCode::CNY, CurrencyCode::HKD, CurrencyCode::USD];

/// 代码文本。
pub const fn code(&self) -> &'static str {
match self {
CurrencyCode::CNY => "CNY",
CurrencyCode::HKD => "HKD",
CurrencyCode::USD => "USD",
}
}

/// 中文名。
pub const fn chinese_label(&self) -> &'static str {
match self {
CurrencyCode::CNY => "人民币",
CurrencyCode::HKD => "港币",
CurrencyCode::USD => "美元",
}
}

/// 标准符号。
///
/// ⚠️ 符号是**币种本身的属性**,不是排版细节:
/// 若放到表示层写死,同一个币种在两幕里就可能写成两种不同的符号。
pub const fn symbol(&self) -> &'static str {
match self {
CurrencyCode::CNY => "¥",
CurrencyCode::HKD => "HK$",
CurrencyCode::USD => "US$",
}
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : customer_code.rs
//! 客户编号值对象。

/// 客户编号:形如 `VIP-0001`。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct CustomerCode {
/// 编号文本。
code: &'static str,
}

impl CustomerCode {
/// 声明一个客户编号。
pub const fn new(code: &'static str) -> Self {
CustomerCode { code }
}

/// 编号文本。
pub const fn code(&self) -> &'static str {
self.code
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : department_code.rs
//! 部门标识:**开放型**(`const fn new`,不是 enum)。
//!
//! 判据:部门会被**新增**(本工程扩展区就加了第 13 个部门),
//! 且不会被当作排序键。用 enum 会让"加一个部门"变成改领域文件——
//! 那就不是可扩展。
//!
//! ⚠️ 中文名**不在领域层**:显示名是装配层的字典(`application::catalog`),
//! 领域只认标识。否则换一套叫法就要改核心。

/// 部门标识。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DepartmentCode {
/// 标识文本。
code: &'static str,
}

impl DepartmentCode {
/// 声明一个部门标识。
pub const fn new(code: &'static str) -> Self {
DepartmentCode { code }
}

/// 标识文本。
pub const fn code(&self) -> &'static str {
self.code
}

// ———————————————————————–
// 内置部门:珠宝企业**十二个**端到端环节。
// ———————————————————————–

/// 原料采购核验:核验金料/宝石的成色、重量、来源单证。
pub const PROCUREMENT: DepartmentCode = DepartmentCode::new("PROCUREMENT");
/// 设计制图:效果图、三视图、工艺图纸。
pub const DESIGN: DepartmentCode = DepartmentCode::new("DESIGN");
/// 加工生产:起版、浇铸、执模、镶嵌、抛光。
pub const PRODUCTION: DepartmentCode = DepartmentCode::new("PRODUCTION");
/// 质检:成色检测、外观检验、尺寸核验。
pub const QUALITY: DepartmentCode = DepartmentCode::new("QUALITY");
/// 包装:成品清洁、包装、证书配套。
pub const PACKAGING: DepartmentCode = DepartmentCode::new("PACKAGING");
/// 物流:出库、承运、签收。
pub const LOGISTICS: DepartmentCode = DepartmentCode::new("LOGISTICS");
/// 财务:定金、尾款、成本结转、对账。
pub const FINANCE: DepartmentCode = DepartmentCode::new("FINANCE");
/// 营销推广:素材、投放、活动结算。
pub const MARKETING: DepartmentCode = DepartmentCode::new("MARKETING");
/// 业务:客户需求、报价、交付回访。
pub const SALES: DepartmentCode = DepartmentCode::new("SALES");
/// 人事行政:排班、用工、行政支持。
pub const HR_ADMIN: DepartmentCode = DepartmentCode::new("HR_ADMIN");
/// IT:系统账号、工位终端、接口运维。
pub const IT: DepartmentCode = DepartmentCode::new("IT");
/// 培训:工艺标准、质检标准、系统操作培训。
pub const TRAINING: DepartmentCode = DepartmentCode::new("TRAINING");

/// 内置部门的**完整集合**(顺序即报表顺序)。
///
/// ⚠️ 这是一个数组而不是 `HashMap`:顺序必须是唯一确定的,
/// 否则两次运行的报表行序会不同。
pub const BUILTIN_ALL: [DepartmentCode; 12] = [
DepartmentCode::PROCUREMENT,
DepartmentCode::DESIGN,
DepartmentCode::PRODUCTION,
DepartmentCode::QUALITY,
DepartmentCode::PACKAGING,
DepartmentCode::LOGISTICS,
DepartmentCode::FINANCE,
DepartmentCode::MARKETING,
DepartmentCode::SALES,
DepartmentCode::HR_ADMIN,
DepartmentCode::IT,
DepartmentCode::TRAINING,
];
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : employee_code.rs
//! 员工编号值对象。

/// 员工编号:跨部门统一工号。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct EmployeeCode {
/// 编号文本。
code: &'static str,
}

impl EmployeeCode {
/// 声明一个员工编号。
pub const fn new(code: &'static str) -> Self {
EmployeeCode { code }
}

/// 编号文本。
pub const fn code(&self) -> &'static str {
self.code
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : evidence_code.rs
//! 留痕编码:**开放型**(`const fn new`)。
//!
//! 判据:留痕种类会随部门增加而增加(工程外新增的第十三个部门自带它的留痕种类),
//! 而且它**会被当作查找键**(按编码找这一条留痕),所以实现 `PartialEq`;
//! 但不需要排序,因此**不**实现 `Ord` / `Hash`。
//!
//! ★ 为什么留痕必须要有编码:
//! 「这一步做过了」若只留一句文本,报表上就只能按文本去重,
//! 而文本会随措辞变化——编码才是可核对的那个东西。

/// 留痕编码。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EvidenceCode {
/// 编码文本(形如 `EVID-QUALITY-REPORT`)。
code: &'static str,
}

impl EvidenceCode {
/// 声明一个留痕编码。
pub const fn new(code: &'static str) -> Self {
EvidenceCode { code }
}

/// 编码文本。
pub const fn code(&self) -> &'static str {
self.code
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : material_code.rs
//! 物料编号值对象。

/// 物料编号:金料 / 宝石 / 辅料的唯一编号。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct MaterialCode {
/// 编号文本。
code: &'static str,
}

impl MaterialCode {
/// 声明一个物料编号。
pub const fn new(code: &'static str) -> Self {
MaterialCode { code }
}

/// 编号文本。
pub const fn code(&self) -> &'static str {
self.code
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : money.rs
//! 金额值对象:**整数分**存储。
//!
//! 为什么不用浮点数:金额在报表上要可加、可比、可对账;
//! 浮点会让「合计 = 各行之和」变成一条会随机失败的断言。

use crate::common::numeric::scale_rounded;
use crate::domain::CurrencyCode;

/// 金额:最小单位(分)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Money {
/// 金额(分)。负数表示冲销/退款。
minor: i64,
/// 币种。
currency: CurrencyCode,
}

impl Money {
/// 构造一个金额。
pub const fn new(minor: i64, currency: CurrencyCode) -> Self {
Money { minor, currency }
}

/// 零值。
pub const fn zero(currency: CurrencyCode) -> Self {
Money { minor: 0, currency }
}

/// 由「元」构造(**只在夹具里用**:元 → 分是 ×100 的精确换算)。
pub const fn from_yuan(yuan: i64, currency: CurrencyCode) -> Self {
Money {
minor: yuan * 100,
currency,
}
}

/// 金额(分)。
pub const fn minor(&self) -> i64 {
self.minor
}

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

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

/// 是否为正。
pub const fn is_positive(&self) -> bool {
self.minor > 0
}

/// 加法。
///
/// ⚠️ 跨币种相加是**逻辑错误**:港币单和人民币单混在一起求和,
/// 报表上会得到一个谁都对不上的数。这里直接断言而不是静默返回零。
pub fn add(&self, other: Money) -> Money {
debug_assert!(
self.currency == other.currency,
"跨币种相加:金额不可跨币种求和"
);
Money::new(self.minor + other.minor, self.currency)
}

/// 减法。
pub fn subtract(&self, other: Money) -> Money {
debug_assert!(
self.currency == other.currency,
"跨币种相减:金额不可跨币种求和"
);
Money::new(self.minor – other.minor, self.currency)
}

/// 取反(冲销)。
pub const fn negate(&self) -> Money {
Money::new(-self.minor, self.currency)
}

/// ★ 按比率缩放(**唯一舍入口径**:`common::numeric::scale_rounded`)。
pub fn scale_by_basis_points(&self, basis_points: i64) -> Money {
let scaled: i64 = scale_rounded(
i128::from(self.minor),
i128::from(basis_points),
i128::from(10_000_i64),
);
Money::new(scaled, self.currency)
}

/// 取大。
pub fn max(&self, other: Money) -> Money {
if self.minor >= other.minor {
*self
} else {
other
}
}

/// 取小。
pub fn min(&self, other: Money) -> Money {
if self.minor <= other.minor {
*self
} else {
other
}
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : order_number.rs
//! 工单号值对象。

/// 工单号:形如 `PO-2026-0001`。
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct OrderNumber {
/// 原始文本。
raw: String,
}

impl OrderNumber {
/// 由文本构造。
pub fn new(raw: String) -> Self {
debug_assert!(!raw.is_empty(), "工单号不得为空");
OrderNumber { raw }
}

/// 由「年份 + 序号」确定性构造(**夹具与运行期都走这里**,保证可复现)。
pub fn from_parts(prefix: &str, year: i64, sequence: i64) -> Self {
OrderNumber {
raw: format!("{}-{}-{:04}", prefix, year, sequence),
}
}

/// 原始文本。
pub fn raw(&self) -> &str {
self.raw.as_str()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : work_minutes.rs
//! 工时值对象:**整数分钟**。
//!
//! 与金额同理:工时在报表上要跨部门求和,用浮点会让"合计"变成近似值。

/// 工时(分钟)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct WorkMinutes {
/// 分钟数。
minutes: i64,
}

impl WorkMinutes {
/// 构造一个工时。
pub const fn new(minutes: i64) -> Self {
WorkMinutes { minutes }
}

/// 零。
pub const fn zero() -> Self {
WorkMinutes { minutes: 0 }
}

/// 分钟数。
pub const fn minutes(&self) -> i64 {
self.minutes
}

/// 累加。
pub const fn plus(&self, other: WorkMinutes) -> WorkMinutes {
WorkMinutes::new(self.minutes + other.minutes)
}

/// 小时数(**整数小时**,按唯一舍入口径)。
pub fn whole_hours(&self) -> i64 {
crate::common::numeric::divide_rounded(i128::from(self.minutes), 60)
}
}

//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : fault_plan.rs
//! 故障注入:**确定性**的外部系统故障剧本。
//!
//! ★ 为什么不用随机失败:
//! 随机会让"重试 3 次后成功"这件事在两次运行里给出不同的日志,
//! 于是没人能验证熔断是真的按阈值打开的。
//! 用"前 N 次失败"这种剧本,每一次调用的结果是**可预测**的,
//! 熔断的每一次状态迁移都能对上账。
//!
//! ⚠️ 计数是全局共享的(`ConcurrentCounter`),所以剧本在并发下也成立;
//! 但本工程的适配器只在单线程演示阶段使用,避免"第几次调用"本身不确定。

use crate::common::concurrency::ConcurrentCounter;

/// 故障模式。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaultMode {
/// 从不失败。
Never,
/// 前 `n` 次调用失败,之后一直成功。
FailFirst(usize),
/// 总是失败。
Always,
}

impl FaultMode {
/// 模式说明(报表用)。
pub fn describe(&self) -> String {
match self {
FaultMode::Never => String::from("从不失败"),
FaultMode::FailFirst(count) => format!("前 {} 次失败,之后成功", count),
FaultMode::Always => String::from("总是失败"),
}
}
}

/// 故障剧本。
#[derive(Debug)]
pub struct FaultPlan {
/// 模式。
mode: FaultMode,
/// 调用计数。
counter: ConcurrentCounter,
}

impl FaultPlan {
/// 声明一个剧本。
pub fn new(mode: FaultMode) -> Self {
FaultPlan {
mode,
counter: ConcurrentCounter::new(),
}
}

/// ★ 这一次调用是否应该失败。
pub fn should_fail(&self) -> bool {
match self.mode {
FaultMode::Never => false,
FaultMode::Always => true,
FaultMode::FailFirst(count) => self.counter.fetch_increment() < count,
}
}

/// 已被问过多少次。
pub fn invocation_total(&self) -> usize {
self.counter.value()
}

/// 模式。
pub const fn mode(&self) -> FaultMode {
self.mode
}

/// 模式说明。
pub fn describe(&self) -> String {
self.mode.describe()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : retrying_gateway.rs
//! ★ 带**重试 + 熔断**的网关装饰器。
//!
//! 形状:`RetryingGateway<G: RemoteGateway>` 包住任意网关,
//! 自己不认识任何业务——于是五个外部系统共用同一套重试与熔断口径。
//!
//! 分工:
//! – 重试:处理"这一次运气不好";
//! – 熔断:处理"下游已经不行了,别再打"。
//!
//! ⚠️ 被熔断挡下来的请求**不记为下游失败**:
//! 它根本没有发出去,把它算进失败会误判下游更糟(雪球越滚越大)。

use std::sync::Arc;

use crate::application::GatewayCallDto;
use crate::common::logging::AppLogger;
use crate::common::resilience::CircuitBreaker;
use crate::common::resilience::AttemptRecord;
use crate::common::resilience::RetryPolicy;
use crate::domain::{GatewayRequest, GatewayResponse, RemoteGateway};

/// 带重试与熔断的网关。
pub struct RetryingGateway {
/// 被包住的网关。
inner: Arc<dyn RemoteGateway>,
/// 重试策略。
retry_policy: RetryPolicy,
/// 熔断器。
breaker: Arc<CircuitBreaker>,
/// 日志对象。
logger: Arc<dyn AppLogger>,
}

impl RetryingGateway {
/// 包一个网关。
pub fn new(
inner: Arc<dyn RemoteGateway>,
retry_policy: RetryPolicy,
breaker: Arc<CircuitBreaker>,
logger: Arc<dyn AppLogger>,
) -> Self {
RetryingGateway {
inner,
retry_policy,
breaker,
logger,
}
}

/// 被包住的网关。
pub fn inner(&self) -> &Arc<dyn RemoteGateway> {
&self.inner
}

/// 熔断器(报表要展示它的状态迁移)。
pub fn breaker(&self) -> &Arc<CircuitBreaker> {
&self.breaker
}

/// 重试策略。
pub const fn retry_policy(&self) -> RetryPolicy {
self.retry_policy
}

/// ★ 带韧性的一次调用。
///
/// 返回 DTO:调用方拿到的是"这次调用最终怎么样",而不是某一次尝试的结果。
pub fn invoke_with_resilience(&self, request: &GatewayRequest) -> GatewayCallDto {
let system_name: &'static str = self.inner.system_name();

// —— 先看熔断 ——
if !self.breaker.allow_request() {
self.logger.warn(
format!(
"{} 已断开,请求 {} 被熔断挡下(未发出)",
system_name,
request.operation()
)
.as_str(),
);
return GatewayCallDto::new(
system_name,
request.operation(),
0,
false,
true,
0,
String::from("被熔断器挡下,未发出请求"),
);
}

let mut attempts: Vec<AttemptRecord> = Vec::new();
let mut backoff_total: u64 = 0;
let mut succeeded: bool = false;
let mut last_message: String = String::from("(未调用)");
let mut attempt: u32 = 0;

while attempt < self.retry_policy.max_attempts() {
attempt += 1;
let wait: u64 = self.retry_policy.backoff_millis(attempt);
backoff_total += wait;

let response: GatewayResponse = self.inner.invoke(request);
last_message = String::from(response.message());
if response.succeeded() {
succeeded = true;
attempts.push(AttemptRecord::new(
attempt,
true,
wait,
String::from("成功"),
));
self.breaker.record_success();
self.logger.info(
format!(
"{} 第 {} 次调用成功:{}",
system_name, attempt, last_message
)
.as_str(),
);
break;
}

attempts.push(AttemptRecord::new(
attempt,
false,
wait,
last_message.clone(),
));
self.breaker.record_failure();
self.logger.warn(
format!(
"{} 第 {} 次调用失败:{}",
system_name, attempt, last_message
)
.as_str(),
);

if !self.retry_policy.should_retry(attempt) {
break;
}
self.logger.debug(
format!("{} 退避 {} ms 后重试", system_name, wait).as_str(),
);
}

GatewayCallDto::new(
system_name,
request.operation(),
attempts.len(),
succeeded,
false,
backoff_total,
if succeeded {
String::from("最终成功")
} else {
format!("最终失败:{}", last_message)
},
)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : simulated_gateway.rs
//! 模拟外部系统网关:**五个真实存在的外挂系统**。
//!
//! 1. 采购核验系统(供应商成色证书、金料来源单证);
//! 2. 物流轨迹系统(运单推送、签收回执);
//! 3. 财务对账系统(凭证过账、往来对账);
//! 4. 营销投放平台(素材发布、活动返点结算);
//! 5. IT 账号系统(账号开通、工位终端部署)。
//!
//! ★ 适配器**只做协议翻译**,不做重试、不做熔断:
//! 那两件事由 `RetryingGateway` 装饰器统一负责,
//! 于是"重试了几次""熔断有没有开"只有一处口径。

use crate::domain::{GatewayRequest, GatewayResponse, RemoteGateway};
// ⚠️ `pub use` 而不是 `use`:本模块的调用方需要拿到 `FaultPlan` 来配置剧本,
// 私有 `use` 会让上层拿不到类型(编译期报 E0603,而读代码看不出来)。
pub use crate::infrastructure::adapters::fault_plan::{FaultMode, FaultPlan};

/// 一个模拟网关。
pub struct SimulatedGateway {
/// 系统名。
system_name: &'static str,
/// 支持的操作。
operations: Vec<&'static str>,
/// 成功时的消息模板前缀。
success_prefix: &'static str,
/// 失败原因。
failure_reason: &'static str,
/// 故障剧本。
plan: FaultPlan,
}

impl SimulatedGateway {
/// 声明一个模拟网关。
pub fn new(
system_name: &'static str,
operations: Vec<&'static str>,
success_prefix: &'static str,
failure_reason: &'static str,
plan: FaultPlan,
) -> Self {
SimulatedGateway {
system_name,
operations,
success_prefix,
failure_reason,
plan,
}
}

/// 支持的操作。
pub fn operations(&self) -> &[&'static str] {
self.operations.as_slice()
}

/// 故障剧本。
pub fn fault_plan(&self) -> &FaultPlan {
&self.plan
}

/// 是否支持某个操作。
pub fn supports(&self, operation: &str) -> bool {
self.operations.contains(&operation)
}
}

impl RemoteGateway for SimulatedGateway {
fn system_name(&self) -> &'static str {
self.system_name
}

fn invoke(&self, request: &GatewayRequest) -> GatewayResponse {
if !self.supports(request.operation()) {
return GatewayResponse::failure(format!(
"{} 不支持操作 {}",
self.system_name,
request.operation()
));
}
if self.plan.should_fail() {
return GatewayResponse::failure(format!(
"{} 调用失败:{}(工单 {})",
self.system_name,
self.failure_reason,
request.order_number_text()
));
}
GatewayResponse::success(format!(
"{}:{} 工单 {} 的 {} 已完成",
self.system_name,
self.success_prefix,
request.order_number_text(),
request.operation()
))
}
}

/// 采购核验系统:**前 2 次失败**(足以触发"第 3 次重试成功")。
pub fn procurement_gateway() -> SimulatedGateway {
SimulatedGateway::new(
"采购核验系统",
vec!["VERIFY_MATERIAL", "QUERY_CERTIFICATE"],
"已回执",
"检测机构接口超时",
FaultPlan::new(FaultMode::FailFirst(2)),
)
}

/// 物流轨迹系统:从不失败(对照:稳定的下游不该被熔断)。
pub fn logistics_gateway() -> SimulatedGateway {
SimulatedGateway::new(
"物流轨迹系统",
vec!["PUSH_TRACKING", "QUERY_SIGNATURE"],
"已同步",
"运单号不存在",
FaultPlan::new(FaultMode::Never),
)
}

/// 财务对账系统:**前 1 次失败**(一次重试即恢复)。
pub fn finance_gateway() -> SimulatedGateway {
SimulatedGateway::new(
"财务对账系统",
vec!["POST_VOUCHER", "QUERY_RECONCILE"],
"已过账",
"凭证期间已关闭",
FaultPlan::new(FaultMode::FailFirst(1)),
)
}

/// 营销投放平台:**总是失败**(用来把熔断器打到断开)。
pub fn marketing_gateway() -> SimulatedGateway {
SimulatedGateway::new(
"营销投放平台",
vec!["PUBLISH_MATERIAL", "SETTLE_REBATE"],
"已发布",
"投放平台鉴权失效",
FaultPlan::new(FaultMode::Always),
)
}

/// IT 账号系统:从不失败。
pub fn it_gateway() -> SimulatedGateway {
SimulatedGateway::new(
"IT 账号系统",
vec!["CREATE_ACCOUNT", "DEPLOY_TERMINAL"],
"已开通",
"账号目录不可达",
FaultPlan::new(FaultMode::Never),
)
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : container.rs
//! DI 容器:**组合根造一次,其它地方只解析**。
//!
//! Rust 里的 DI 没有反射,形状就是「构造器注入 + `Arc<dyn Trait>`」。
//! 容器的价值不是"自动装配",而是把"谁依赖谁"集中在一处——
//! 于是加一个新实现时,改动只发生在这个文件里。
//!
//! ⚠️ 容器本身在 `infrastructure`,但它**不被业务层依赖**:
//! 业务层只认 `Arc<dyn OrderRepository>` 这类端口,
//! 容器是谁不重要——这正是"兼容 DI"而不是"依赖某个 DI 框架"。

use std::sync::Arc;

use crate::application::DepartmentDispatchService;
use crate::application::ProcessApplicationService;
use crate::application::ProcessOrchestrator;
use crate::common::logging::AppLogger;
use crate::common::logging::LogCategory;
use crate::common::logging::LoggingDispatcher;
use crate::common::logging::Logger;
use crate::common::resilience::CircuitPolicy;
use crate::common::resilience::RetryPolicy;
use crate::domain::OrderRepository;
use crate::domain::RemoteGateway;
use crate::infrastructure::LoggerFactory;
use crate::infrastructure::RetryingGateway;

/// 服务容器。
pub struct ServiceContainer {
/// 工单仓储。
repository: Arc<dyn OrderRepository>,
/// 流程编排器。
orchestrator: Arc<ProcessOrchestrator>,
/// 部门完成后的支撑任务派发服务。
dispatch: Arc<DepartmentDispatchService>,
/// 日志分发器(全工程唯一)。
dispatcher: Arc<LoggingDispatcher>,
/// ★ `ILogger<T>` 的**唯一产地**。
///
/// 容器自己不 `Logger::new(…)`,而是转交工厂:
/// "日志对象从哪来"只有一处答案,换落地方式只改工厂。
logger_factory: LoggerFactory,
/// 重试策略。
retry_policy: RetryPolicy,
/// 熔断策略。
circuit_policy: CircuitPolicy,
}

impl ServiceContainer {
/// 装配一个容器。
pub fn new(
repository: Arc<dyn OrderRepository>,
orchestrator: Arc<ProcessOrchestrator>,
dispatch: Arc<DepartmentDispatchService>,
dispatcher: Arc<LoggingDispatcher>,
retry_policy: RetryPolicy,
circuit_policy: CircuitPolicy,
) -> Self {
ServiceContainer {
repository,
orchestrator,
dispatch,
logger_factory: LoggerFactory::new(Arc::clone(&dispatcher)),
dispatcher,
retry_policy,
circuit_policy,
}
}

/// 仓储。
pub fn repository(&self) -> &Arc<dyn OrderRepository> {
&self.repository
}

/// 编排器。
pub fn orchestrator(&self) -> &Arc<ProcessOrchestrator> {
&self.orchestrator
}

/// 派发服务。
pub fn dispatch(&self) -> &Arc<DepartmentDispatchService> {
&self.dispatch
}

/// 日志分发器。
pub fn dispatcher(&self) -> &Arc<LoggingDispatcher> {
&self.dispatcher
}

/// 重试策略。
pub const fn retry_policy(&self) -> RetryPolicy {
self.retry_policy
}

/// 熔断策略。
pub const fn circuit_policy(&self) -> CircuitPolicy {
self.circuit_policy
}

/// ★ 解析一个 `ILogger<T>`(转交工厂,容器自己不 new)。
pub fn logger<T: LogCategory>(&self) -> Logger<T> {
self.logger_factory.create::<T>()
}

/// 解析一个日志对象(**向上转型为门面**,业务层只依赖 `dyn AppLogger`)。
pub fn logger_as<T: LogCategory>(&self) -> Arc<dyn AppLogger> {
Arc::new(self.logger_factory.create::<T>())
}

/// ★ 解析应用服务(**每次调用造一个新实例**:服务本身无状态,
/// 造多个实例不会分裂状态,状态都在共享的仓储与分发器上)。
pub fn order_service<T: LogCategory>(&self) -> Arc<ProcessApplicationService> {
Arc::new(ProcessApplicationService::new(
Arc::clone(&self.repository),
Arc::clone(&self.orchestrator),
self.logger_as::<T>(),
))
}

/// ★ 包一个带重试与熔断的网关。
pub fn retrying_gateway<T: LogCategory>(
&self,
inner: Arc<dyn RemoteGateway>,
breaker: Arc<crate::common::resilience::CircuitBreaker>,
) -> RetryingGateway {
RetryingGateway::new(inner, self.retry_policy, breaker, self.logger_as::<T>())
}

/// 注册清单(**报表用**:让人看到"到底注入了哪些东西",而不是靠读代码猜)。
pub fn registration_list(&self) -> Vec<String> {
let ledger = self.orchestrator.ledger().snapshot();
vec![
format!("工单仓储 = {}", self.repository.implementation_name()),
format!(
"骨架台账 = 已跑 {} 次 / 中止 {} 次",
ledger.run_total(),
ledger.aborted_total()
),
format!("重试策略 = {}", self.retry_policy.describe()),
format!("熔断策略 = {}", self.circuit_policy.describe()),
format!("时钟 = {}", self.dispatcher.clock_name()),
format!(
"日志工厂绑定的分发器 = {}",
self.logger_factory.dispatcher().clock_name()
),
format!("派发服务累计条数 = {}", self.dispatch.dispatch_total()),
]
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : categories.rs
//! 日志类别标记:为每一种使用方写一个单位结构体。
//!
//! ★ 这就是 `ILogger<T>` 里的 `T`:它只决定日志行里的 `category` 字段
//! (形如 `order-service`),让"这条日志是谁写的"不用靠猜消息内容。
//!
//! ⚠️ 为什么这些类型放在 `infrastructure` 而不是 `common`:
//! `order`、`gateway` 是业务词,而 `common` 严禁出现业务词。
//! 类别名属于"这套系统怎么给自己的组件命名",是部署层的知识。

use crate::common::logging::LogCategory;

/// 工单应用服务。
pub struct OrderServiceCategory;

impl LogCategory for OrderServiceCategory {
fn category_name() -> &'static str {
"order-service"
}
}

/// 调度器。
pub struct SchedulerCategory;

impl LogCategory for SchedulerCategory {
fn category_name() -> &'static str {
"scheduler"
}
}

/// 外部系统适配器。
pub struct GatewayCategory;

impl LogCategory for GatewayCategory {
fn category_name() -> &'static str {
"gateway"
}
}

/// 报表输出。
pub struct ReportCategory;

impl LogCategory for ReportCategory {
fn category_name() -> &'static str {
"report"
}
}

/// 系统(装配、体检、自检)。
pub struct SystemCategory;

impl LogCategory for SystemCategory {
fn category_name() -> &'static str {
"system"
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : console_sink.rs
//! 控制台落地:与文件落地**共用同一个格式化器**。
//!
//! ★ 这就是「日志和控制台输出一样」的实现方式:
//! 不是两边各写一遍格式(那一定会走样),而是调用同一个 `format_line`。
//! 差别只在"写到哪"——控制台收全部等级,文件按等级分流。
//!
//! 于是「`app-info.log` 的每一行都恰好出现在控制台的 INFO 行里」是可以断言的。

use std::sync::{Arc, Mutex};

use crate::common::logging::format_line;
use crate::common::logging::LogLevel;
use crate::common::logging::LogRecord;
use crate::common::logging::LogSink;

/// 控制台落地。
///
/// ⚠️ 行内容放在 `Arc<Mutex<…>>` 里而不是字段里:
/// sink 一旦注册进分发器就被"移走"了,调用方再也拿不到它;
/// 而"控制台到底打印了什么"正是本工程要拿去和文件逐行比对的东西。
/// 共享句柄让"读"不需要持有 sink 本身。
pub struct ConsoleLogSink {
/// 已打印的行:`(等级, 行)`,按序号顺序。
lines: Arc<Mutex<Vec<(LogLevel, String)>>>,
}

impl ConsoleLogSink {
/// 造一个控制台落地。
pub fn new() -> Self {
ConsoleLogSink {
lines: Arc::new(Mutex::new(Vec::new())),
}
}

/// ★ 取共享句柄(注册进分发器**之前**调用)。
pub fn shared_handle(&self) -> Arc<Mutex<Vec<(LogLevel, String)>>> {
Arc::clone(&self.lines)
}

/// 累计行数。
pub fn line_total(&self) -> usize {
self.lock().len()
}

/// 某一等级的全部行(**按序号顺序**)。
pub fn lines_of(&self, level: LogLevel) -> Vec<String> {
self.lock()
.iter()
.filter(|(record_level, _)| *record_level == level)
.map(|(_, line)| line.clone())
.collect()
}

/// 全部行(按写入顺序,即按序号顺序)。
pub fn all_lines(&self) -> Vec<String> {
self.lock().iter().map(|(_, line)| line.clone()).collect()
}

/// 取锁(锁被污染时照常返回内容:日志不该因为一次 panic 就消失)。
fn lock(&self) -> std::sync::MutexGuard<'_, Vec<(LogLevel, String)>> {
self.lines.lock().unwrap_or_else(|e| e.into_inner())
}
}

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

impl LogSink for ConsoleLogSink {
fn sink_name(&self) -> &'static str {
"控制台落地(与文件共用格式化器)"
}

fn write_batch(&mut self, records: &[LogRecord]) -> usize {
let mut written: usize = 0;
// 先在临界区内把行拼好、打印出来,避免持锁做 IO。
for record in records.iter() {
let line: String = format_line(record);
println!("{}", line);
self.lock().push((record.level(), line));
written += 1;
}
written
}

fn close(&mut self) -> usize {
self.line_total()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : file_sink.rs
//! 文件落地:按 `(日期, 等级)` 分流写文件。
//!
//! ★ 高并发写入的正确姿势:**写者只管把记录交给分发器,落盘是批量的**。
//! 每条日志都 `write` 一次,在高并发下会把锁的粒度变成 IO 的粒度;
//! 攒批后一次 `BufWriter::write` 只锁一次。
//!
//! 而且批量写带来一个额外好处:分发器在 `flush` 前会按序号排序,
//! 于是**文件里的行序与线程调度无关**,两次运行逐字节一致。

use std::collections::BTreeMap;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use crate::common::logging::format_line;
use crate::common::logging::LogLevel;
use crate::common::logging::LogRecord;
use crate::common::logging::LogSink;
use crate::infrastructure::LogDirectoryLayout;

/// 文件落地。
pub struct FileLogSink {
/// 目录布局。
layout: LogDirectoryLayout,
/// 已打开的写者:`(日期, 等级) -> 写者`。
///
/// ⚠️ 用 `BTreeMap` 而不是 `HashMap`:遍历顺序必须是唯一确定的。
writers: BTreeMap<(String, LogLevel), BufWriter<File>>,
/// 每个文件的统计:`(日期, 等级) -> (行数, 字节数)`。
///
/// ⚠️ 同样放在 `Arc<Mutex<…>>` 里:sink 注册进分发器后就被移走了,
/// 而报表要读的正是这些统计。共享句柄让"读统计"不需要持有 sink。
statistics: Arc<Mutex<BTreeMap<(String, LogLevel), (usize, usize)>>>,
/// 累计行数。
line_total: usize,
}

impl FileLogSink {
/// 造一个文件落地。
pub fn new(layout: LogDirectoryLayout) -> Self {
FileLogSink {
layout,
writers: BTreeMap::new(),
statistics: Arc::new(Mutex::new(BTreeMap::new())),
line_total: 0,
}
}

/// 布局。
pub fn layout(&self) -> &LogDirectoryLayout {
&self.layout
}

/// ★ 取统计句柄(注册进分发器**之前**调用)。
pub fn statistics_handle(&self) -> Arc<Mutex<BTreeMap<(String, LogLevel), (usize, usize)>>> {
Arc::clone(&self.statistics)
}

/// 统计:`(日期, 等级, 行数, 字节数)`,按 `(日期, 等级)` 排序。
pub fn statistics(&self) -> Vec<(String, LogLevel, usize, usize)> {
self.lock()
.iter()
.map(|((date_text, level), (lines, bytes))| {
(date_text.clone(), *level, *lines, *bytes)
})
.collect()
}

/// 累计行数。
pub const fn line_total(&self) -> usize {
self.line_total
}

/// 取锁。
fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<(String, LogLevel), (usize, usize)>> {
self.statistics.lock().unwrap_or_else(|e| e.into_inner())
}

/// 取(必要时新建)某个 `(日期, 等级)` 的写者。
fn writer_for(&mut self, date_text: &str, level: LogLevel) -> std::io::Result<()> {
let key: (String, LogLevel) = (String::from(date_text), level);
if self.writers.contains_key(&key) {
return Ok(());
}
self.layout.ensure_directory(date_text)?;
let path: PathBuf = self.layout.file_path_for(date_text, level);
// ⚠️ `File::create` 会截断:每次运行都是干净的一份,
// 否则第二次运行的文件里混着第一次的内容,"两份一致"就无法验证。
let file: File = File::create(path)?;
self.writers.insert(key, BufWriter::new(file));
Ok(())
}

/// 刷盘并把统计留在内存里(统计不随文件关闭而消失)。
fn flush_all(&mut self) -> std::io::Result<()> {
for writer in self.writers.values_mut() {
writer.flush()?;
}
Ok(())
}
}

impl LogSink for FileLogSink {
fn sink_name(&self) -> &'static str {
"文件落地(按日期目录 / 按等级分文件)"
}

fn write_batch(&mut self, records: &[LogRecord]) -> usize {
let mut written: usize = 0;
for record in records.iter() {
if self.writer_for(record.date_text(), record.level()).is_err() {
// 落盘失败不能让程序崩(日志不该成为单点故障),但必须留下痕迹。
eprintln!("[日志] 无法打开文件:{}", record.message());
continue;
}
let key: (String, LogLevel) = (String::from(record.date_text()), record.level());
let mut line: String = format_line(record);
line.push('\\n');

if let Some(writer) = self.writers.get_mut(&key) {
let bytes: usize = line.as_bytes().len();
if writer.write_all(line.as_bytes()).is_ok() {
written += 1;
self.line_total += 1;
let mut guard = self.lock();
let entry = guard.entry(key).or_insert((0, 0));
entry.0 += 1;
entry.1 += bytes;
}
}
}
written
}

/// ★ 真正落到磁盘(`BufWriter` → OS)。
///
/// 这一行是"日志与控制台一致"这条体检能成立的前提:
/// 没有它,运行期读自己的 app-info.log 会少读到缓冲区里那几行。
fn flush(&mut self) {
if self.flush_all().is_err() {
eprintln!("[日志] 刷盘失败");
}
}

fn close(&mut self) -> usize {
if self.flush_all().is_err() {
eprintln!("[日志] 刷盘失败");
}
self.line_total
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : log_directory.rs
//! ★ 日志目录布局:`Logs/<日期>/<等级文件>`。
//!
//! 例:`Logs/2026-07-06/app-info.log`。
//!
//! 为什么按日期分目录、按等级分文件:
//! – 按日期分目录 → 保留与清理都以"天"为单位,删一天就是 `rm -r` 一个目录;
//! – 按等级分文件 → 排查错误时只看 `app-error.log`,不用在海量 INFO 里 grep。
//!
//! ⚠️ 目录名来自**日志里的日期**,不是"当前系统日期"——
//! 后者会让同一批日志在跨零点运行时被切成两个目录,而那时人正在排查。

use std::path::PathBuf;

use crate::common::logging::LogLevel;

/// 日志目录布局。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogDirectoryLayout {
/// 根目录名。
base_dir: String,
}

impl LogDirectoryLayout {
/// 声明一个布局。
pub fn new(base_dir: &str) -> Self {
LogDirectoryLayout {
base_dir: String::from(base_dir),
}
}

/// 本工程默认布局:`Logs/`。
pub fn default_layout() -> Self {
LogDirectoryLayout::new("Logs")
}

/// 根目录名。
pub fn base_dir(&self) -> &str {
self.base_dir.as_str()
}

/// 某一天的目录路径。
pub fn directory_for(&self, date_text: &str) -> PathBuf {
let mut path: PathBuf = PathBuf::from(self.base_dir.as_str());
path.push(date_text);
path
}

/// 某天某等级的文件路径。
pub fn file_path_for(&self, date_text: &str, level: LogLevel) -> PathBuf {
let mut path: PathBuf = self.directory_for(date_text);
path.push(level.file_name());
path
}

/// 相对路径文本(报表里展示,不暴露绝对路径——绝对路径在不同机器上不同)。
pub fn relative_path_for(&self, date_text: &str, level: LogLevel) -> String {
format!("{}/{}/{}", self.base_dir, date_text, level.file_name())
}

/// 建目录(已存在则无操作)。
pub fn ensure_directory(&self, date_text: &str) -> std::io::Result<PathBuf> {
let path: PathBuf = self.directory_for(date_text);
std::fs::create_dir_all(&path)?;
Ok(path)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : logger_factory.rs
//! 日志对象工厂:**`Logger<T>` 的唯一来源**。
//!
//! 业务代码不该自己去 `Arc::new(LoggingDispatcher)`——
//! 那样"分发器有几个实例"就没人管得住了,而多一个实例就多一套序号,
//! 时间戳会立刻对不上。

use std::sync::Arc;

use crate::common::logging::LoggingDispatcher;
use crate::common::logging::LogCategory;
use crate::common::logging::Logger;

/// 日志对象工厂。
pub struct LoggerFactory {
/// 分发器(**全工程唯一**)。
dispatcher: Arc<LoggingDispatcher>,
}

impl LoggerFactory {
/// 造一个工厂。
pub fn new(dispatcher: Arc<LoggingDispatcher>) -> Self {
LoggerFactory { dispatcher }
}

/// ★ 造一个 `ILogger<T>`。
pub fn create<T: LogCategory>(&self) -> Logger<T> {
Logger::new(Arc::clone(&self.dispatcher))
}

/// 分发器。
pub fn dispatcher(&self) -> &Arc<LoggingDispatcher> {
&self.dispatcher
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : in_memory_order_repository.rs
//! 内存工单仓储(**线程安全**)。
//!
//! ★ 为什么用 `RwLock<BTreeMap<…>>`:
//! – `BTreeMap` 而不是 `HashMap`:遍历顺序必须是唯一确定的,
//! 否则两次运行的报表行序会不同(这是本工程"逐字节一致"的硬要求);
//! – `RwLock` 而不是 `Mutex`:读多写少,且读之间不该互相阻塞;
//! – **不用 `unsafe`**:`static` 要求 `Sync`,`Rc`/`RefCell` 根本进不来——
//! "在别的语言里靠纪律保证的事,在这里由类型系统保证"。

use std::collections::BTreeMap;
use std::sync::{Arc, RwLock};

use crate::domain::OrderRepository;
use crate::domain::ProductionOrder;

/// 内存仓储。
pub struct InMemoryOrderRepository {
/// 工单表(**按工单号有序**)。
orders: RwLock<BTreeMap<String, ProductionOrder>>,
}

impl InMemoryOrderRepository {
/// 空仓储。
pub fn new() -> Self {
InMemoryOrderRepository {
orders: RwLock::new(BTreeMap::new()),
}
}

/// ★ 乐观并发写入:**判断与写入在同一把写锁内**。
///
/// 若写成"先读一次比较、再单独写一次",两步之间另一个线程可能已经改了——
/// 那正是乐观并发要防的事情,分开写等于没防。
fn lock(&self) -> std::sync::RwLockWriteGuard<'_, BTreeMap<String, ProductionOrder>> {
match self.orders.write() {
Ok(guard) => guard,
// 锁被污染(某线程持锁时 panic):不要让程序静默变成"没有仓储"。
Err(poisoned) => poisoned.into_inner(),
}
}
}

impl OrderRepository for InMemoryOrderRepository {
fn save(&self, order: ProductionOrder) {
let mut guard = self.lock();
guard.insert(String::from(order.order_number_text()), order);
}

fn save_expecting_completed_count(&self, expected_count: usize, order: ProductionOrder) -> bool {
let mut guard = self.lock();
let key: String = String::from(order.order_number_text());
match guard.get(&key) {
Some(current) => {
if current.completed_department_total() != expected_count {
return false;
}
}
None => {
// 新单:只有"期望计数为 0"才算没被抢先。
if expected_count != 0 {
return false;
}
}
}
guard.insert(key, order);
true
}

fn find_by_number(&self, order_number_text: &str) -> Option<ProductionOrder> {
match self.orders.read() {
Ok(guard) => guard.get(order_number_text).cloned(),
Err(poisoned) => poisoned.into_inner().get(order_number_text).cloned(),
}
}

fn all(&self) -> Vec<ProductionOrder> {
let mut orders: Vec<ProductionOrder> = match self.orders.read() {
Ok(guard) => guard.values().cloned().collect(),
Err(poisoned) => poisoned.into_inner().values().cloned().collect(),
};
// ★ 排序键显式定义:`BTreeMap` 已按 key 有序,这里再排一次是**双保险**——
// 若哪天换成一个无序容器,这一行会让报表仍然稳定。
orders.sort_by(|left, right| left.order_number_text().cmp(right.order_number_text()));
orders
}

fn count(&self) -> usize {
match self.orders.read() {
Ok(guard) => guard.len(),
Err(poisoned) => poisoned.into_inner().len(),
}
}

fn implementation_name(&self) -> &'static str {
"内存仓储(RwLock + BTreeMap)"
}
}

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

/// 供组合根使用:把仓储包成 `Arc<dyn OrderRepository>`。
pub fn shared_repository() -> Arc<dyn OrderRepository> {
Arc::new(InMemoryOrderRepository::new())
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : order_seed.rs
//! 工单夹具:**演示数据**。
//!
//! ⚠️ 数据刻意包含几组"会分叉"的输入,因为恒真的检查没有价值:
//! – `PO-2026-0004` 是**港币**单 → 证明"金额不可跨币种求和";
//! – `PO-2026-0005` 报价 77777.75 元 → 定金 30% 除不尽,
//! 于是"远离零四舍五入"与"向零截断"真的会给出**不同**的结果;
//! – `PO-2026-0006` 定金只有 10% → 设计制图会被拒绝(准入拦下);
//! – `PO-2026-0009` 有一批成色 9850 bps(低于下限 9900)→ 核验与质检都会被挡;
//! – `PO-2026-0003` 物料成本占报价过高 → 采购自检会把作业判失败。

use crate::domain::CurrencyCode;
use crate::domain::CustomerCode;
use crate::domain::DepartmentCode;
use crate::domain::MaterialCode;
use crate::domain::MaterialLot;
use crate::domain::Money;
use crate::domain::OrderNumber;
use crate::domain::ProductionOrder;
use crate::domain::ShipmentRecord;

/// 金料(18K)。
const GOLD_18K: MaterialCode = MaterialCode::new("GOLD-18K");
/// 足金。
const GOLD_999: MaterialCode = MaterialCode::new("GOLD-999");
/// 铂金。
const PLATINUM_950: MaterialCode = MaterialCode::new("PT-950");
/// 钻石。
const DIAMOND: MaterialCode = MaterialCode::new("DIAMOND");
/// 翡翠。
const JADEITE: MaterialCode = MaterialCode::new("JADEITE");

/// 全部演示工单(**顺序固定**,工单号即顺序)。
pub fn seed_orders() -> Vec<ProductionOrder> {
vec![
build_order(
1,
CustomerCode::new("VIP-0001"),
"18K金镶钻项链",
12_800_000,
CurrencyCode::CNY,
4_000,
vec![
lot("LOT-0001", GOLD_18K, 12_500, 48_000, CurrencyCode::CNY, 9_950),
lot("LOT-0002", DIAMOND, 1_800, 1_650_000, CurrencyCode::CNY, 10_000),
],
),
build_order(
2,
CustomerCode::new("VIP-0002"),
"足金龙凤手镯",
8_860_000,
CurrencyCode::CNY,
3_000,
vec![lot("LOT-0003", GOLD_999, 38_600, 52_000, CurrencyCode::CNY, 9_990)],
),
// ★ 物料成本占报价过高:采购自检会判失败。
build_order(
3,
CustomerCode::new("VIP-0003"),
"翡翠蛋面戒指",
6_400_000,
CurrencyCode::CNY,
3_500,
vec![
lot("LOT-0004", JADEITE, 9_400, 1_180_000, CurrencyCode::CNY, 10_000),
lot("LOT-0005", GOLD_18K, 8_200, 48_000, CurrencyCode::CNY, 9_950),
],
),
// ★ 港币单:金额**不可**并入人民币合计。
build_order(
4,
CustomerCode::new("VIP-0004"),
"铂金对戒",
3_980_000,
CurrencyCode::HKD,
3_000,
vec![lot("LOT-0006", PLATINUM_950, 16_800, 96_000, CurrencyCode::HKD, 9_950)],
),
// ★ 除不尽的报价:77777.75 元 × 30% = 2333332.5 分。
build_order(
5,
CustomerCode::new("VIP-0005"),
"定制刻字吊坠",
7_777_775,
CurrencyCode::CNY,
3_000,
vec![lot("LOT-0007", GOLD_999, 21_400, 52_000, CurrencyCode::CNY, 9_990)],
),
// ★ 定金只有 10%:设计制图会被拒绝。
build_order(
6,
CustomerCode::new("VIP-0006"),
"珍珠耳饰套装",
4_560_000,
CurrencyCode::CNY,
1_000,
vec![lot("LOT-0008", GOLD_18K, 6_300, 48_000, CurrencyCode::CNY, 9_950)],
),
build_order(
7,
CustomerCode::new("VIP-0007"),
"祖母绿胸针",
16_800_000,
CurrencyCode::CNY,
3_000,
vec![
lot("LOT-0009", GOLD_18K, 15_600, 48_000, CurrencyCode::CNY, 9_960),
lot("LOT-0010", JADEITE, 4_200, 1_180_000, CurrencyCode::CNY, 10_000),
],
),
build_order(
8,
CustomerCode::new("VIP-0008"),
"婚庆套链",
9_999_900,
CurrencyCode::CNY,
3_200,
vec![lot("LOT-0011", GOLD_999, 42_800, 52_000, CurrencyCode::CNY, 9_990)],
),
// ★ 有一批成色 9850 bps(低于下限 9900)→ 核验与质检被挡。
build_order(
9,
CustomerCode::new("VIP-0009"),
"回购旧金重铸",
6_400_000,
CurrencyCode::CNY,
3_000,
vec![lot("LOT-0012", GOLD_999, 30_200, 52_000, CurrencyCode::CNY, 9_850)],
),
build_order(
10,
CustomerCode::new("VIP-0010"),
"高定翡翠项链",
13_200_000,
CurrencyCode::CNY,
3_000,
vec![
lot("LOT-0013", JADEITE, 12_600, 1_180_000, CurrencyCode::CNY, 10_000),
lot("LOT-0014", DIAMOND, 2_400, 1_650_000, CurrencyCode::CNY, 10_000),
lot("LOT-0015", GOLD_18K, 9_800, 48_000, CurrencyCode::CNY, 9_950),
],
),
]
}

/// 造一批物料。
fn lot(
lot_code: &'static str,
material_code: MaterialCode,
weight_milligrams: i64,
unit_price_minor_per_gram: i64,
currency: CurrencyCode,
purity_basis_points: i64,
) -> MaterialLot {
MaterialLot::new(
lot_code,
material_code,
weight_milligrams,
unit_price_minor_per_gram,
currency,
purity_basis_points,
)
}

/// 造一张工单:报价、定金比率、物料批次。
///
/// ⚠️ 定金额由 `scale_by_basis_points` 算出(**唯一舍入口径**),
/// 夹具里不手写"折扣后的数"——手写的数会与领域算出的差 1 分,
/// 而那 1 分在报表上完全看不出来。
fn build_order(
sequence: i64,
customer_code: CustomerCode,
product_name: &'static str,
quoted_minor: i64,
currency: CurrencyCode,
deposit_ratio_basis_points: i64,
lots: Vec<MaterialLot>,
) -> ProductionOrder {
let quoted: Money = Money::new(quoted_minor, currency);
let mut order: ProductionOrder = ProductionOrder::new(
OrderNumber::from_parts("PO", 2026, sequence),
customer_code,
product_name,
quoted,
);
order.receive_deposit(quoted.scale_by_basis_points(deposit_ratio_basis_points));
for item in lots {
order.add_material_lot(item);
}
order.add_shipment(ShipmentRecord::new(
format!("SF{}{:03}", 2026, sequence),
"顺丰珠宝专送",
));
order
}

/// ★ 计划工时表:给调度器用。
///
/// 顺序与 `DepartmentCode::BUILTIN_ALL` 一致,
/// 于是"第 i 个部门的计划工时"是一个纯函数,不会与部门清单悄悄错位。
const PLANNED_MINUTES: [i64; 12] = [60, 120, 600, 90, 45, 90, 60, 60, 30, 40, 25, 60];

/// 某个部门的计划工时(**没登记就给一个显式默认值并写清理由**)。
pub fn planned_minutes_of(department_code: DepartmentCode) -> i64 {
let codes: [DepartmentCode; 12] = DepartmentCode::BUILTIN_ALL;
for (index, code) in codes.iter().enumerate() {
if *code == department_code {
return PLANNED_MINUTES[index];
}
}
// 工程外新增的部门没有登记计划工时:给 30 分钟并让它在报表上显示为"默认"。
30
}

//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : deferred.rs
//! 延迟日志:**并发阶段保持可复现的关键**。
//!
//! 问题:多个线程同时 `logger.info(…)`,谁先抢到分发器的锁,
//! 就先拿到较小的序号——于是"第 N 条是哪个线程写的"在不同运行里不同。
//! 排序后字节流仍然不同,两次运行无法逐字节一致。
//!
//! 解法:并发阶段各线程**先攒在自己这里**,阶段结束后由主线程按
//! `(worker 编号, 该 worker 内序号)` 的顺序统一提交。
//! 于是"哪条是哪个线程产生的"这个信息**完整保留**(行里仍然有线程名),
//! 而最终字节流唯一确定。

use std::sync::Mutex;

use crate::common::logging::LogLevel;
use crate::common::logging::AppLogger;

/// 一条尚未提交到分发器的日志。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeferredLogLine {
/// 等级。
level: LogLevel,
/// 类别。
category: &'static str,
/// 线程名(**显式记录**,不依赖提交时的线程)。
thread_name: String,
/// 消息。
message: String,
}

impl DeferredLogLine {
/// 构造一条延迟日志。
pub fn new(level: LogLevel, category: &'static str, thread_name: String, message: String) -> Self {
DeferredLogLine {
level,
category,
thread_name,
message,
}
}

/// 等级。
pub const fn level(&self) -> LogLevel {
self.level
}

/// 类别。
pub const fn category(&self) -> &'static str {
self.category
}

/// 线程名。
pub fn thread_name(&self) -> &str {
self.thread_name.as_str()
}

/// 消息。
pub fn message(&self) -> &str {
self.message.as_str()
}

/// 提交到分发器(**由主线程调用**,于是序号唯一确定)。
pub fn emit_into(&self, dispatcher: &super::dispatcher::LoggingDispatcher) {
dispatcher.emit(
self.level,
self.category,
self.thread_name.as_str(),
self.message.as_str(),
);
}
}

/// 延迟日志器:实现 `AppLogger`,但只把日志攒在内存里。
///
/// ⚠️ 每个 worker 线程**独占一个实例**(不跨线程共享),
/// 所以这里的 `Mutex` 只是为了满足 `AppLogger: Send + Sync` 的约束,
/// 运行期不会真的发生争用。
pub struct DeferredLogger {
/// 缓冲区。
buffer: Mutex<Vec<DeferredLogLine>>,
/// 类别。
category: &'static str,
}

impl DeferredLogger {
/// 造一个延迟日志器。
pub fn new(category: &'static str) -> Self {
DeferredLogger {
buffer: Mutex::new(Vec::new()),
category,
}
}

/// 取走全部已攒下的日志(**取走后缓冲区清空**)。
pub fn drain(&self) -> Vec<DeferredLogLine> {
match self.buffer.lock() {
Ok(mut guard) => std::mem::take(&mut *guard),
Err(poisoned) => std::mem::take(&mut *poisoned.into_inner()),
}
}

/// 已攒下的条数。
pub fn pending_total(&self) -> usize {
match self.buffer.lock() {
Ok(guard) => guard.len(),
Err(poisoned) => poisoned.into_inner().len(),
}
}
}

impl AppLogger for DeferredLogger {
fn debug(&self, message: &str) {
self.push(LogLevel::Debug, message);
}

fn info(&self, message: &str) {
self.push(LogLevel::Info, message);
}

fn warn(&self, message: &str) {
self.push(LogLevel::Warn, message);
}

fn error(&self, message: &str) {
self.push(LogLevel::Error, message);
}

fn category_name(&self) -> &'static str {
self.category
}
}

impl DeferredLogger {
/// 攒一条。
fn push(&self, level: LogLevel, message: &str) {
let line: DeferredLogLine = DeferredLogLine::new(
level,
self.category,
super::dispatcher::LoggingDispatcher::current_thread_name(),
String::from(message),
);
match self.buffer.lock() {
Ok(mut guard) => guard.push(line),
Err(poisoned) => poisoned.into_inner().push(line),
}
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : dispatcher.rs
//! 日志分发器:**并发写入的唯一入口**。
//!
//! ★ 为什么"高并发写入"与"两次运行逐字节一致"可以同时成立:
//!
//! 1. 各线程**只做一件事**:`push` 一条 `LogRecord` 进缓冲区(临界区极短);
//! 2. 序号在**临界区内**分配,所以序号唯一且连续;
//! 3. 真正落盘发生在 `flush()`:先按序号排序,再交给 sink。
//!
//! 于是线程调度只影响"谁先抢到锁",**不影响最终字节流**。
//! 反过来的写法(每条日志即时 `println!` / 即时 `write`)必然顺序不定。

use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};

use crate::common::logging::LogLevel;
use crate::common::logging::LogRecord;
use crate::common::logging::LogSink;
use crate::common::clock::Clock;

/// 日志分发器:`Send + Sync`,可跨线程共享。
pub struct LoggingDispatcher {
/// 序号分配器(在临界区内自增)。
next_sequence: AtomicU64,
/// 待落盘的记录缓冲区。
buffer: Mutex<Vec<LogRecord>>,
/// 所有落地(控制台 / 文件…)。
sinks: Mutex<Vec<Box<dyn LogSink + Send>>>,
/// 时钟(由外部注入,便于固定时间)。
clock: Arc<dyn Clock>,
/// 各等级的累计条数。
level_counters: [AtomicUsize; 4],
}

impl LoggingDispatcher {
/// 新建一个分发器。
pub fn new(clock: Arc<dyn Clock>) -> Self {
LoggingDispatcher {
next_sequence: AtomicU64::new(0),
buffer: Mutex::new(Vec::new()),
sinks: Mutex::new(Vec::new()),
clock,
level_counters: [
AtomicUsize::new(0),
AtomicUsize::new(0),
AtomicUsize::new(0),
AtomicUsize::new(0),
],
}
}

/// 注册一个落地。
pub fn register_sink(&self, sink: Box<dyn LogSink + Send>) {
match self.sinks.lock() {
Ok(mut guard) => guard.push(sink),
// 锁被污染(某线程在持锁时 panic):不要让程序静默变成"没有日志"。
Err(poisoned) => poisoned.into_inner().push(sink),
}
}

/// 写一条日志:`Logger<T>` 的唯一出口。
///
/// 参数刻意是「等级 + 类别 + 线程名 + 消息」,
/// 序号与时间戳不在这里生成,避免调用方影响顺序。
pub fn emit(
&self,
level: LogLevel,
category: &'static str,
thread_name: &str,
message: &str,
) {
// 序号分配的原子性由 `fetch_add` 保证。
let sequence: u64 = self.next_sequence.fetch_add(1, Ordering::Relaxed);
self.level_counters[level.index()].fetch_add(1, Ordering::Relaxed);

let stamp = self.clock.stamp_for(sequence);
let record: LogRecord = LogRecord::new(
sequence,
level,
category,
String::from(thread_name),
String::from(stamp.date_text()),
stamp.time_text().to_string(),
String::from(message),
);

match self.buffer.lock() {
Ok(mut guard) => guard.push(record),
Err(poisoned) => poisoned.into_inner().push(record),
}
}

/// 当前线程名:主线程叫 `main`,worker 线程由调度器显式命名。
pub fn current_thread_name() -> String {
match std::thread::current().name() {
Some(name) => String::from(name),
None => String::from("unnamed"),
}
}

/// 把缓冲区里**已排好序**的记录交给所有落地。
///
/// 可以多次调用:每次都只排空当前缓冲,而序号全局单调,
/// 所以多次 flush 拼接起来仍然是有序的。
pub fn flush(&self) -> usize {
let mut records: Vec<LogRecord> = match self.buffer.lock() {
Ok(mut guard) => std::mem::take(&mut *guard),
Err(poisoned) => std::mem::take(&mut *poisoned.into_inner()),
};
if records.is_empty() {
return 0;
}
// ★ 确定性就在这一行:不管哪个线程先到,最终顺序只由序号决定。
records.sort();

let mut written: usize = 0;
match self.sinks.lock() {
Ok(mut guard) => {
for sink in guard.iter_mut() {
written += sink.write_batch(records.as_slice());
}
// ★ 写完必须再 `flush` 一次:批量写是带缓冲的,
// 不 flush 则"这次 flush 之后文件里有什么"是**没有保证**的。
for sink in guard.iter_mut() {
sink.flush();
}
}
Err(poisoned) => {
let mut guard = poisoned.into_inner();
for sink in guard.iter_mut() {
written += sink.write_batch(records.as_slice());
}
for sink in guard.iter_mut() {
sink.flush();
}
}
}
written
}

/// 收尾:先 flush 再关闭所有落地。
pub fn shutdown(&self) -> usize {
let written: usize = self.flush();
match self.sinks.lock() {
Ok(mut guard) => {
for sink in guard.iter_mut() {
sink.close();
}
}
Err(poisoned) => {
for sink in poisoned.into_inner().iter_mut() {
sink.close();
}
}
}
written
}

/// 已产生的日志总条数(含未落盘)。
pub fn emitted_total(&self) -> u64 {
self.next_sequence.load(Ordering::Relaxed)
}

/// 某一等级的累计条数。
pub fn level_total(&self, level: LogLevel) -> usize {
self.level_counters[level.index()].load(Ordering::Relaxed)
}

/// 时钟名字(报表展示用)。
pub fn clock_name(&self) -> &'static str {
self.clock.clock_name()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : formatter.rs
//! ★ 全工程**唯一**的日志行格式化器。
//!
//! 控制台 sink 与文件 sink 都调用本文件的 `format_line`,
//! 所以「控制台里的一行」与「文件里的一行」在字节上不可能不同——
//! 这不是靠约定,是靠只有一条代码路径。

use crate::common::logging::LogLevel;
use crate::common::logging::LogRecord;

/// 格式化一行日志。
///
/// 形如:
/// `2026-07-06 09:00:00.000 │ INFO │ #000137 │ order-service │ worker-0 │ 工单 XXX 已进入质检`
///
/// 字段顺序刻意固定:`时间 → 等级 → 序号 → 类别 → 线程 → 消息`,
/// 便于用固定列宽比对(也便于人眼扫)。
pub fn format_line(record: &LogRecord) -> String {
let mut line: String = String::new();
line.push_str(record.date_text());
line.push(' ');
line.push_str(record.time_text());
line.push_str(" │ ");
line.push_str(LogLevel::tag_of(record.level()));
line.push_str(" │ #");
line.push_str(format!("{:06}", record.sequence()).as_str());
line.push_str(" │ ");
line.push_str(record.category());
line.push_str(" │ ");
line.push_str(record.thread_name());
line.push_str(" │ ");
line.push_str(record.message());
line
}

/// 只取「消息正文」的镜像形式:控制台与文件共用的就是 `format_line`,
/// 这里额外提供裸消息,供「一致性断言」比对(证明两边连正文都没改写)。
pub fn plain_message(record: &LogRecord) -> &str {
record.message()
}

impl LogLevel {
/// 等级标签(`format_line` 内部走这里,保证与 `tag()` 同源)。
pub const fn tag_of(level: LogLevel) -> &'static str {
level.tag()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : level.rs
//! 日志等级:**封闭集合 → `enum`**。
//!
//! 判据:等级会被当作**查找键**(文件名映射),且集合确定不变,
//! 所以是 enum 而不是 `const fn new` 的开放结构体。

/// 日志等级。声明顺序即严重度升序(`Ord` 派生依赖它)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LogLevel {
/// 调试。
Debug,
/// 信息。
Info,
/// 警告。
Warn,
/// 错误。
Error,
}

impl LogLevel {
/// 全部等级(**固定顺序**,遍历它不会产生顺序歧义)。
pub const ALL: [LogLevel; 4] = [LogLevel::Debug, LogLevel::Info, LogLevel::Warn, LogLevel::Error];

/// 日志文件名:按等级分文件。
pub const fn file_name(&self) -> &'static str {
match self {
LogLevel::Debug => "app-debug.log",
LogLevel::Info => "app-info.log",
LogLevel::Warn => "app-warn.log",
LogLevel::Error => "app-error.log",
}
}

/// 行内等级标签(**固定 5 列**,保证各等级行对齐)。
pub const fn tag(&self) -> &'static str {
match self {
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO ",
LogLevel::Warn => "WARN ",
LogLevel::Error => "ERROR",
}
}

/// 中文名(报表用)。
pub const fn chinese_label(&self) -> &'static str {
match self {
LogLevel::Debug => "调试",
LogLevel::Info => "信息",
LogLevel::Warn => "警告",
LogLevel::Error => "错误",
}
}

/// 在数组里的下标(计数器索引)。
pub const fn index(&self) -> usize {
match self {
LogLevel::Debug => 0,
LogLevel::Info => 1,
LogLevel::Warn => 2,
LogLevel::Error => 3,
}
}

/// 是否上控制台。
///
/// 四个等级**全部**上控制台:控制台是「全量镜像」,
/// 文件是「按等级分流」。两者的 INFO 行必须逐字节相同(可断言)。
pub const fn goes_to_console(&self) -> bool {
true
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : logger.rs
//! ★ `ILogger<T>`:本工程统一的日志输出对象。
//!
//! 形状对齐 .NET 的 `ILogger<T>`:`T` 只是**类别标记**,
//! 由它决定日志行里的 `category` 字段(形如 `order-service`)。
//!
//! 为什么 `T` 不直接用 `std::any::type_name::<T>()`:
//! `type_name` 的输出格式不保证稳定,还可能带模块路径;
//! 本工程要的是**逐字节一致**,所以类别名必须由实现 `LogCategory` 的人写死。

use std::marker::PhantomData;
use std::sync::Arc;

use crate::common::logging::LoggingDispatcher;
use crate::common::logging::LogLevel;

/// 日志类别标记:为每一种使用方实现一次,写死一个短名。
///
/// ⚠️ 为什么要求 `Send + Sync`:`Logger<T>` 会被多个 worker 线程共享,
/// 而 `PhantomData<T>` 会把 `T` 的 auto trait 传导过来。
/// 类别标记都是零大小的单位结构体,天然满足;加在 trait 上是为了
/// 「以后有人往标记里塞字段」时立刻报错,而不是到运行时才发现。
pub trait LogCategory: Send + Sync + 'static {
/// 类别名(将出现在每一行日志里)。
fn category_name() -> &'static str;
}

/// 统一日志输出对象:**可克隆、可跨线程、零成本转发**。
///
/// `Logger<T>` 本身不含可变状态,状态全在共享的 `Arc<LoggingDispatcher>` 上,
/// 所以 `Clone` 一个 `Logger<T>` 只是复制一个指针——
/// 每个 worker 线程拿自己的副本,写的是同一个缓冲区。
#[derive(Clone)]
pub struct Logger<T: LogCategory> {
/// 分发器(共享)。
dispatcher: Arc<LoggingDispatcher>,
/// 类别标记(零大小)。
marker: PhantomData<T>,
}

impl<T: LogCategory> Logger<T> {
/// 由分发器造一个 `Logger<T>`(通常由 DI 容器调用)。
pub fn new(dispatcher: Arc<LoggingDispatcher>) -> Self {
Logger {
dispatcher,
marker: PhantomData,
}
}

/// 类别名。
pub fn category_name(&self) -> &'static str {
T::category_name()
}

/// 底层分发器(需要在组合根 flush 时用到)。
pub fn dispatcher(&self) -> &Arc<LoggingDispatcher> {
&self.dispatcher
}

/// 调试级。
pub fn debug(&self, message: &str) {
self.write(LogLevel::Debug, message);
}

/// 信息级。
pub fn info(&self, message: &str) {
self.write(LogLevel::Info, message);
}

/// 警告级。
pub fn warn(&self, message: &str) {
self.write(LogLevel::Warn, message);
}

/// 错误级。
pub fn error(&self, message: &str) {
self.write(LogLevel::Error, message);
}

/// 统一出口。
///
/// ⚠️ 刻意**不**提供 `&mut self` 的方法:日志记录器必须是只读的,
/// 否则「谁都能改配置」就会变成运行期的不确定性来源。
fn write(&self, level: LogLevel, message: &str) {
self.dispatcher.emit(
level,
T::category_name(),
LoggingDispatcher::current_thread_name().as_str(),
message,
);
}
}

/// 对象安全的日志门面:**业务层只依赖这个 trait**,不依赖具体 `Logger<T>`。
///
/// 这样仓储/服务里写 `logger: Arc<dyn AppLogger>` 就能被任意类别的 `Logger<T>` 注入,
/// 也便于在测试里替换成空实现。
pub trait AppLogger: Send + Sync {
/// 调试级。
fn debug(&self, message: &str);

/// 信息级。
fn info(&self, message: &str);

/// 警告级。
fn warn(&self, message: &str);

/// 错误级。
fn error(&self, message: &str);

/// 类别名(便于在报表里列出"注入了哪些日志对象")。
fn category_name(&self) -> &'static str;
}

impl<T: LogCategory> AppLogger for Logger<T> {
fn debug(&self, message: &str) {
Logger::debug(self, message);
}

fn info(&self, message: &str) {
Logger::info(self, message);
}

fn warn(&self, message: &str) {
Logger::warn(self, message);
}

fn error(&self, message: &str) {
Logger::error(self, message);
}

fn category_name(&self) -> &'static str {
Logger::category_name(self)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : record.rs
//! 一条日志记录(值对象:构造后只读)。
//!
//! 为什么要做成值对象而不是直接拼字符串:
//! 文件 sink 需要 `date_text` 决定目录、需要 `level` 决定文件名;
//! 若只传字符串,落地层就要反解文本——那是最容易写错的耦合。

use crate::common::logging::LogLevel;

/// 一条日志记录。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogRecord {
/// 全局序号:**由分发器统一分配**,是时间戳与顺序的唯一依据。
sequence: u64,
/// 等级。
level: LogLevel,
/// 类别(来自 `Logger<T>` 的 `T`,形如 `order-service`)。
category: &'static str,
/// 线程名(**不打印线程 ID**:ID 在不同运行里可能不同)。
thread_name: String,
/// 日期文本(决定日志目录)。
date_text: String,
/// 时间文本。
time_text: String,
/// 消息正文。
message: String,
}

impl LogRecord {
/// 构造一条记录。
pub fn new(
sequence: u64,
level: LogLevel,
category: &'static str,
thread_name: String,
date_text: String,
time_text: String,
message: String,
) -> Self {
LogRecord {
sequence,
level,
category,
thread_name,
date_text,
time_text,
message,
}
}

/// 全局序号。
pub const fn sequence(&self) -> u64 {
self.sequence
}

/// 等级。
pub const fn level(&self) -> LogLevel {
self.level
}

/// 类别。
pub const fn category(&self) -> &'static str {
self.category
}

/// 线程名。
pub fn thread_name(&self) -> &str {
self.thread_name.as_str()
}

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

/// 时间文本。
pub fn time_text(&self) -> &str {
self.time_text.as_str()
}

/// 消息正文。
pub fn message(&self) -> &str {
self.message.as_str()
}
}

impl PartialOrd for LogRecord {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

impl Ord for LogRecord {
/// ★ 唯一排序键就是序号:序号决定先后,先后决定时间戳。
///
/// 排序键**显式定义**而不是派生(派生顺序由字段声明顺序决定,挪字段就反了)。
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.sequence.cmp(&other.sequence)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : sink.rs
//! 日志落地(`Sink`)的契约。
//!
//! `&mut self` 而不是 `&self`:落地内部要持有 `BufWriter`,
//! 由分发器的 `Mutex` 提供互斥(于是 `Box<dyn LogSink + Send>` 整体是 `Sync`)。

use crate::common::logging::LogRecord;

/// 日志落地。
pub trait LogSink: Send {
/// 落地的名字(报表里展示)。
fn sink_name(&self) -> &'static str;

/// 批量写入(**已按序号排好序**),返回本次写入的**行数**。
fn write_batch(&mut self, records: &[LogRecord]) -> usize;

/// ★ 把已经写进来的内容**真正落到目的地**。
///
/// 与 `write_batch` 分开,是因为批量写必然要缓冲(`BufWriter`)——
/// 而"缓冲"意味着 `flush()` 之后文件里还不一定有内容。
/// 少了这一步,"日志与控制台一致"这条体检就只能等到进程退出才成立,
/// 运行期读自己的日志文件会读到**比实际少几行**的内容——
/// 不崩、不报错,只是那几行"神秘消失"。
fn flush(&mut self) {
// 默认什么都不做:内存型落地没有"缓冲"这回事。
}

/// 收尾(刷盘、关闭文件)。返回已写入的总行数。
fn close(&mut self) -> usize;
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : closed_state.rs
//! 闭合态:正常放行,累计连续失败;达到阈值就断开。

use super::{CircuitBreakerState, CircuitStateKind};
use crate::common::resilience::CircuitPolicy;
use crate::common::resilience::CircuitSnapshot;

/// 闭合状态对象(无字段 → 天然 `Send + Sync`)。
pub struct ClosedState;

impl CircuitBreakerState for ClosedState {
fn state_name(&self) -> &'static str {
"CLOSED"
}

fn allows_request(&self, _snapshot: &CircuitSnapshot, _policy: &CircuitPolicy) -> bool {
true
}

fn next_after_success(
&self,
_snapshot: &CircuitSnapshot,
_policy: &CircuitPolicy,
) -> Option<CircuitStateKind> {
// 本来就是闭合,成功不需要迁移。
None
}

fn next_after_failure(
&self,
snapshot: &CircuitSnapshot,
policy: &CircuitPolicy,
) -> Option<CircuitStateKind> {
if snapshot.consecutive_failures() >= policy.failure_threshold() {
return Some(CircuitStateKind::Open);
}
None
}

fn description(&self) -> &'static str {
"正常放行;连续失败达到阈值即断开"
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : half_open_state.rs
//! 半开态:只放有限个探测请求,用真实结果决定回闭合还是再断开。

use super::{CircuitBreakerState, CircuitStateKind};
use crate::common::resilience::CircuitPolicy;
use crate::common::resilience::CircuitSnapshot;

/// 半开状态对象。
pub struct HalfOpenState;

impl CircuitBreakerState for HalfOpenState {
fn state_name(&self) -> &'static str {
"HALF_OPEN"
}

fn allows_request(&self, snapshot: &CircuitSnapshot, policy: &CircuitPolicy) -> bool {
// 探测预算用完就不再放行(这一轮探测结束,等下一次进半开)。
snapshot.probes_used() < policy.probe_budget()
}

fn next_after_success(
&self,
snapshot: &CircuitSnapshot,
policy: &CircuitPolicy,
) -> Option<CircuitStateKind> {
if snapshot.consecutive_successes() >= policy.success_threshold() {
return Some(CircuitStateKind::Closed);
}
None
}

fn next_after_failure(
&self,
_snapshot: &CircuitSnapshot,
_policy: &CircuitPolicy,
) -> Option<CircuitStateKind> {
// 半开期间只要有一次失败,立刻回到断开:下游还没好。
Some(CircuitStateKind::Open)
}

fn description(&self) -> &'static str {
"放行有限个探测请求;连续成功达标回闭合,任一失败回断开"
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : open_state.rs
//! 断开态:直接拒绝,不打扰已经出问题的下游。
//!
//! ★ 为什么"断开"之后要有一个半开态而不是直接回闭合:
//! 下游刚挂的时候,通常还没恢复;若断开若干次调用后直接回闭合,
//! 会把**全量流量**一下子压回一个半死的下游(雪崩的经典成因)。
//! 半开只放有限个探测请求,用真实结果决定去留。

use super::{CircuitBreakerState, CircuitStateKind};
use crate::common::resilience::CircuitPolicy;
use crate::common::resilience::CircuitSnapshot;

/// 断开状态对象。
pub struct OpenState;

impl CircuitBreakerState for OpenState {
fn state_name(&self) -> &'static str {
"OPEN"
}

fn allows_request(&self, _snapshot: &CircuitSnapshot, _policy: &CircuitPolicy) -> bool {
false
}

fn next_after_success(
&self,
_snapshot: &CircuitSnapshot,
_policy: &CircuitPolicy,
) -> Option<CircuitStateKind> {
// 断开态不应该收到成功(没放行就不会有调用结果);收到说明装配错了。
None
}

fn next_after_failure(
&self,
_snapshot: &CircuitSnapshot,
_policy: &CircuitPolicy,
) -> Option<CircuitStateKind> {
// 断开态下的"失败"是被熔断挡下来的,不算下游的失败证据;
// 是否转半开由 `CircuitBreaker::allow_request` 依据探测预算决定,不在这里。
None
}

fn description(&self) -> &'static str {
"直接拒绝,不打扰下游;探测预算到来时转半开"
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : circuit_breaker.rs
//! 熔断器:**持有唯一写权限的人**。
//!
//! 与工序聚合根同构:状态对象只读、只给"建议",
//! 真正改"当前是哪个状态"的只有本类型——于是并发下只有一个写入点。

use crate::common::resilience::{CircuitBreakerState, CircuitStateKind};
use std::sync::Mutex;

/// 熔断策略(构造期确定,运行期只读)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CircuitPolicy {
/// 连续失败多少次就断开。
failure_threshold: u64,
/// 半开期放行几个探测请求。
probe_budget: u64,
/// 半开期连续成功多少次就回闭合。
success_threshold: u64,
}

impl CircuitPolicy {
/// 声明一个熔断策略。
pub const fn new(failure_threshold: u64, probe_budget: u64, success_threshold: u64) -> Self {
CircuitPolicy {
failure_threshold,
probe_budget,
success_threshold,
}
}

/// 本工程默认策略:连续 3 次失败断开,半开放 2 个探测,连续 2 次成功回闭合。
pub const fn default_policy() -> Self {
CircuitPolicy::new(3, 2, 2)
}

/// 连续失败阈值。
pub const fn failure_threshold(&self) -> u64 {
self.failure_threshold
}

/// 半开探测预算。
pub const fn probe_budget(&self) -> u64 {
self.probe_budget
}

/// 半开回闭合所需连续成功数。
pub const fn success_threshold(&self) -> u64 {
self.success_threshold
}

/// 策略文本。
pub fn describe(&self) -> String {
format!(
"失败 {} 次断开 / 探测 {} / 成功 {} 次闭合",
self.failure_threshold, self.probe_budget, self.success_threshold
)
}
}

/// 计数快照:传给状态对象的**只读**输入。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CircuitSnapshot {
/// 连续失败次数。
consecutive_failures: u64,
/// 连续成功次数。
consecutive_successes: u64,
/// 本轮半开已用掉的探测数。
probes_used: u64,
}

impl CircuitSnapshot {
/// 构造一份快照。
pub const fn new(consecutive_failures: u64, consecutive_successes: u64, probes_used: u64) -> Self {
CircuitSnapshot {
consecutive_failures,
consecutive_successes,
probes_used,
}
}

/// 连续失败次数。
pub const fn consecutive_failures(&self) -> u64 {
self.consecutive_failures
}

/// 连续成功次数。
pub const fn consecutive_successes(&self) -> u64 {
self.consecutive_successes
}

/// 已用探测数。
pub const fn probes_used(&self) -> u64 {
self.probes_used
}
}

/// 一次状态迁移记录(只增不减,报表直接读)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CircuitTransition {
/// 序号。
index: usize,
/// 从哪个状态。
from_name: &'static str,
/// 到哪个状态。
to_name: &'static str,
/// 触发原因。
reason: String,
}

impl CircuitTransition {
/// 构造一条迁移记录。
pub fn new(index: usize, from_name: &'static str, to_name: &'static str, reason: String) -> Self {
CircuitTransition {
index,
from_name,
to_name,
reason,
}
}

/// 序号。
pub const fn index(&self) -> usize {
self.index
}

/// 起始状态名。
pub const fn from_name(&self) -> &'static str {
self.from_name
}

/// 目标状态名。
pub const fn to_name(&self) -> &'static str {
self.to_name
}

/// 原因。
pub fn reason(&self) -> &str {
self.reason.as_str()
}
}

/// 熔断器:线程安全(`Mutex` 保护写入点)。
#[derive(Debug)]
pub struct CircuitBreaker {
/// 策略(只读)。
policy: CircuitPolicy,
/// 当前状态(唯一写入点)。
kind: Mutex<CircuitStateKind>,
/// 计数(唯一写入点)。
snapshot: Mutex<CircuitSnapshot>,
/// 迁移台账(只增不减)。
transitions: Mutex<Vec<CircuitTransition>>,
/// 被熔断挡下来的请求数。
rejected_total: Mutex<u64>,
}

impl CircuitBreaker {
/// 新建一个闭合态熔断器。
pub fn new(policy: CircuitPolicy) -> Self {
CircuitBreaker {
policy,
kind: Mutex::new(CircuitStateKind::Closed),
snapshot: Mutex::new(CircuitSnapshot::default()),
transitions: Mutex::new(Vec::new()),
rejected_total: Mutex::new(0_u64),
}
}

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

/// 当前状态。
pub fn current_kind(&self) -> CircuitStateKind {
*self.lock_kind()
}

/// 当前计数快照。
pub fn snapshot(&self) -> CircuitSnapshot {
*self.lock_snapshot()
}

/// 迁移台账(只读快照:复制出来,调用方拿不到内部可变引用)。
pub fn transitions(&self) -> Vec<CircuitTransition> {
self.lock_transitions().clone()
}

/// 被熔断挡下来的请求数。
pub fn rejected_total(&self) -> u64 {
*self.lock_rejected()
}

/// ★ 是否放行(半开态会用掉一个探测名额,所以这不是纯查询)。
pub fn allow_request(&self) -> bool {
let mut kind_guard = self.lock_kind();
let mut snapshot_guard = self.lock_snapshot();
let state: &dyn CircuitBreakerState = (*kind_guard).state();

if state.allows_request(&snapshot_guard, &self.policy) {
if *kind_guard == CircuitStateKind::HalfOpen {
// 半开:放行即消耗一个探测名额。
*snapshot_guard = CircuitSnapshot::new(
snapshot_guard.consecutive_failures(),
snapshot_guard.consecutive_successes(),
snapshot_guard.probes_used() + 1,
);
}
return true;
}

// 断开态:不放行,并记一次"被挡下来"。
if *kind_guard == CircuitStateKind::Open {
*self.lock_rejected() += 1;
// ★ 断开到半开的迁移放在这里:断开态自己不知道"已经挡了几次",
// 计数在熔断器上。这与工序状态机同构——状态对象只读、只给建议。
let rejected: u64 = *self.lock_rejected();
if rejected % self.policy.failure_threshold() == 0 {
let from: &'static str = (*kind_guard).name();
*kind_guard = CircuitStateKind::HalfOpen;
*snapshot_guard = CircuitSnapshot::new(
snapshot_guard.consecutive_failures(),
0,
0,
);
self.push_transition(from, CircuitStateKind::HalfOpen.name(), "断开后到达探测窗口");
}
}
false
}

/// 记录一次成功(**只在真的发出请求并成功之后调用**)。
pub fn record_success(&self) {
let mut kind_guard = self.lock_kind();
let mut snapshot_guard = self.lock_snapshot();
let next_snapshot: CircuitSnapshot = CircuitSnapshot::new(
0,
snapshot_guard.consecutive_successes() + 1,
snapshot_guard.probes_used(),
);
*snapshot_guard = next_snapshot;

let state: &dyn CircuitBreakerState = (*kind_guard).state();
if let Some(next) = state.next_after_success(&next_snapshot, &self.policy) {
let from: &'static str = (*kind_guard).name();
*kind_guard = next;
self.push_transition(from, next.name(), "连续成功达到阈值");
}
}

/// 记录一次失败。
pub fn record_failure(&self) {
let mut kind_guard = self.lock_kind();
let mut snapshot_guard = self.lock_snapshot();
let next_snapshot: CircuitSnapshot = CircuitSnapshot::new(
snapshot_guard.consecutive_failures() + 1,
0,
snapshot_guard.probes_used(),
);
*snapshot_guard = next_snapshot;

let state: &dyn CircuitBreakerState = (*kind_guard).state();
if let Some(next) = state.next_after_failure(&next_snapshot, &self.policy) {
let from: &'static str = (*kind_guard).name();
*kind_guard = next;
let reason: String = if next == CircuitStateKind::Open {
String::from("连续失败达到阈值")
} else {
String::from("半开期出现失败")
};
self.push_transition(from, next.name(), reason.as_str());
}
}

// —————- 私有:锁与台账 —————-

/// 取状态锁(锁被污染时照常工作:熔断器不该因为一次 panic 就消失)。
fn lock_kind(&self) -> std::sync::MutexGuard<'_, CircuitStateKind> {
self.kind.lock().unwrap_or_else(|e| e.into_inner())
}

/// 取计数锁。
fn lock_snapshot(&self) -> std::sync::MutexGuard<'_, CircuitSnapshot> {
self.snapshot.lock().unwrap_or_else(|e| e.into_inner())
}

/// 取台账锁。
fn lock_transitions(&self) -> std::sync::MutexGuard<'_, Vec<CircuitTransition>> {
self.transitions.lock().unwrap_or_else(|e| e.into_inner())
}

/// 取拒绝计数锁。
fn lock_rejected(&self) -> std::sync::MutexGuard<'_, u64> {
self.rejected_total.lock().unwrap_or_else(|e| e.into_inner())
}

/// 追加一条迁移记录。
fn push_transition(&self, from_name: &'static str, to_name: &'static str, reason: &str) {
let mut guard = self.lock_transitions();
let index: usize = guard.len() + 1;
guard.push(CircuitTransition::new(
index,
from_name,
to_name,
String::from(reason),
));
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : retry_policy.rs
//! 重试策略:**刻意不带抖动(jitter)**。
//!
//! ⚠️ 教科书里的指数退避都带随机抖动,但本工程的验收标准之一是
//! 「两次运行逐字节一致」——随机数会直接破坏它。
//! 所以这里用**确定性退避**:`base * multiplier^(attempt-1)`,
//! 并在注释里写明:真实生产环境应加抖动,代价是不可复现。

/// 一次尝试的记录(用于报表与断言)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttemptRecord {
/// 第几次尝试(从 1 开始)。
attempt: u32,
/// 是否成功。
succeeded: bool,
/// 本次尝试**之前**等待的毫秒数(第一次为 0)。
backoff_millis: u64,
/// 结果说明。
note: String,
}

impl AttemptRecord {
/// 构造一条尝试记录。
pub fn new(attempt: u32, succeeded: bool, backoff_millis: u64, note: String) -> Self {
AttemptRecord {
attempt,
succeeded,
backoff_millis,
note,
}
}

/// 第几次尝试。
pub const fn attempt(&self) -> u32 {
self.attempt
}

/// 是否成功。
pub const fn succeeded(&self) -> bool {
self.succeeded
}

/// 退避毫秒数。
pub const fn backoff_millis(&self) -> u64 {
self.backoff_millis
}

/// 结果说明。
pub fn note(&self) -> &str {
self.note.as_str()
}
}

/// 重试策略。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPolicy {
/// 最多尝试次数(含第一次)。
max_attempts: u32,
/// 基础退避毫秒。
base_backoff_millis: u64,
/// 退避倍数。
backoff_multiplier: u64,
}

impl RetryPolicy {
/// 声明一个重试策略。
pub const fn new(max_attempts: u32, base_backoff_millis: u64, backoff_multiplier: u64) -> Self {
RetryPolicy {
max_attempts,
base_backoff_millis,
backoff_multiplier,
}
}

/// 本工程默认策略:最多 3 次,基础 50ms,倍数 2(50 / 100)。
pub const fn default_policy() -> Self {
RetryPolicy::new(3, 50, 2)
}

/// 最多尝试次数。
pub const fn max_attempts(&self) -> u32 {
self.max_attempts
}

/// 基础退避毫秒。
pub const fn base_backoff_millis(&self) -> u64 {
self.base_backoff_millis
}

/// 是否还要再试一次(`attempt` 为**已经做过的**次数)。
pub const fn should_retry(&self, attempt: u32) -> bool {
attempt < self.max_attempts
}

/// 第 `attempt` 次尝试**之前**等待的毫秒数(第一次恒为 0)。
///
/// 公式:`base * multiplier^(attempt-2)`(attempt=1 → 0,2 → base,3 → base*mult)。
pub fn backoff_millis(&self, attempt: u32) -> u64 {
if attempt <= 1 {
return 0;
}
let mut value: u64 = self.base_backoff_millis;
for _ in 2..attempt {
value = value.saturating_mul(self.backoff_multiplier);
}
value
}

/// 策略文本(报表用)。
pub fn describe(&self) -> String {
let mut third: u64 = self.base_backoff_millis;
third = third.saturating_mul(self.backoff_multiplier);
format!(
"最多 {} 次,退避 {} / {} ms(确定性,无抖动)",
self.max_attempts, self.base_backoff_millis, third
)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : failure_policy.rs
//! 失败策略:某一步失败之后,骨架还要不要继续往下跑。
//!
//! ★ 为什么它必须在机制层:**"中止"是一个可自检的性质**。
//! 若把「失败后继续跑」的逻辑散落在各部门实现里,
//! 「说中止却多跑了两步」就没有任何地方能发现——
//! 每个部门各自 `return`,读者只能逐个文件读完才能确信。
//!
//! 判据:**这一步不过,后面还有没有意义**。
//! – 准入校验不过 → 后面全没意义 → `Abort`;
//! – 留痕没写进去 → 作业本身已完成,不该回滚 → `Continue`。

/// 失败策略:**封闭集合** → `enum`(换编译期穷尽性检查)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FailurePolicy {
/// 中止:失败之后**后续步骤一步都不跑**。
Abort,
/// 继续:失败之后仍跑完后续步骤(失败只留痕,不阻断)。
Continue,
}

impl FailurePolicy {
/// 编码。
pub const fn code(&self) -> &'static str {
match self {
FailurePolicy::Abort => "ABORT",
FailurePolicy::Continue => "CONTINUE",
}
}

/// 中文标签。
pub const fn chinese_label(&self) -> &'static str {
match self {
FailurePolicy::Abort => "失败即中止",
FailurePolicy::Continue => "失败仍继续",
}
}

/// 遇到失败时是否应当停止。
pub const fn should_stop_on_failure(&self) -> bool {
matches!(self, FailurePolicy::Abort)
}

/// 说明(报表用,写清"为什么这一档是这个策略")。
pub const fn describe(&self) -> &'static str {
match self {
FailurePolicy::Abort => "这一步不过,后面都没有意义",
FailurePolicy::Continue => "这一步不过也要留痕,作业本身已完成",
}
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : skeleton_audit.rs
//! 骨架自检:**"这次跑法有没有违反骨架自己的纪律"**。
//!
//! ★ 为什么需要它:骨架不可覆盖是**类型层**的事(骨架是 inherent 方法),
//! 但"这一次跑到了哪一步、失败有没有真的中止"是**运行期**的事。
//! 类型挡不住「策略写 Abort 却在失败后继续跑了三步」——那种写法编译得过。
//!
//! ★ 每条规则都必须有"已知会失败的输入":`skeleton_probe` 会喂四个坏样本,
//! 四条规则必须**全部变红**。一条不会红的规则不是规则,它只是没被跑过的注释。

use crate::common::template_method::FailurePolicy;
use crate::common::template_method::SkeletonTrace;
use crate::common::template_method::StepOutcomeKind;

/// 一条自检结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkeletonAuditLine {
/// 规则编码。
rule_code: &'static str,
/// 规则标题。
title: &'static str,
/// 是否通过。
passed: bool,
/// 实测说明(**要能让人复核**:带数字)。
detail: String,
}

impl SkeletonAuditLine {
/// 构造一条自检结果。
fn new(rule_code: &'static str, title: &'static str, passed: bool, detail: String) -> Self {
SkeletonAuditLine {
rule_code,
title,
passed,
detail,
}
}

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

/// 规则标题。
pub const fn title(&self) -> &'static str {
self.title
}

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

/// 实测说明。
pub fn detail(&self) -> &str {
self.detail.as_str()
}
}

/// 自检一条轨迹(四条规则)。
pub fn audit_skeleton_trace(trace: &SkeletonTrace) -> Vec<SkeletonAuditLine> {
let mut lines: Vec<SkeletonAuditLine> = Vec::new();

// —— A1 顺序一致:实际跑过的步骤必须是声明序列的**前缀** ——
let declared: &[crate::common::template_method::StepCode] = trace.declared_steps();
let mut order_ok: bool = true;
let mut first_bad: usize = usize::MAX;
for (position, step) in trace.steps().iter().enumerate() {
if position >= declared.len() || declared[position] != step.step_code() {
order_ok = false;
first_bad = position;
break;
}
}
lines.push(SkeletonAuditLine::new(
"A1_ORDER",
"实际步骤顺序 == 声明序列的前缀",
order_ok,
if order_ok {
format!("实跑 {} 步,与声明的前 {} 步逐条相同", trace.recorded_total(), trace.recorded_total())
} else {
format!("第 {} 位与声明不符(声明 {} 步)", first_bad, declared.len())
},
));

// —— A2 中止有据:标了"中止"就必须真的停在一次失败上 ——
let last_is_failed: bool = trace
.steps()
.last()
.map(|step| step.outcome().kind() == StepOutcomeKind::Failed)
.unwrap_or(false);
let abort_ok: bool = !trace.aborted() || last_is_failed;
lines.push(SkeletonAuditLine::new(
"A2_ABORT",
"标了中止 → 最后一步必须是失败",
abort_ok,
format!(
"中止={},最后一步={}",
trace.aborted(),
trace
.steps()
.last()
.map(|step| step.outcome().kind().chinese_label())
.unwrap_or("(无步骤)")
),
));

// —— A3 序号连续:不允许凭空跳过中间步骤 ——
let mut index_ok: bool = true;
for (position, step) in trace.steps().iter().enumerate() {
if step.order_index() != position {
index_ok = false;
break;
}
}
lines.push(SkeletonAuditLine::new(
"A3_INDEX",
"步骤序号从 0 起且严格连续",
index_ok,
format!("实跑 {} 步,序号是否连续={}", trace.recorded_total(), index_ok),
));

// —— A4 策略兑现:策略说中止,失败就必须真的中止 ——
let has_failed: bool = trace.count_of(StepOutcomeKind::Failed) > 0;
let policy_ok: bool = match trace.failure_policy() {
// 说中止:有失败就必须标中止,且失败是最后一步。
FailurePolicy::Abort => !has_failed || (trace.aborted() && last_is_failed),
// 说继续:有失败也不许标中止。
FailurePolicy::Continue => !trace.aborted(),
};
lines.push(SkeletonAuditLine::new(
"A4_POLICY",
"失败策略与实测一致",
policy_ok,
format!(
"策略={},有失败={},中止={}",
trace.failure_policy().chinese_label(),
has_failed,
trace.aborted()
),
));

lines
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : skeleton_ledger.rs
//! 骨架台账:**只增不减**,跨多次执行累计。
//!
//! ★ 只增不减的量必须用**快照**读(本工程处处在用的纪律):
//! 台账是全局共享的,边跑边读会得到"读到哪算到哪"的数,两次运行对不上。
//! 因此对外只读入口只有 `snapshot()`。

use std::sync::atomic::{AtomicUsize, Ordering};

use crate::common::template_method::SkeletonTrace;
use crate::common::template_method::StepOutcomeKind;

/// 台账快照(**一次取走,之后不再变**)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SkeletonLedgerSnapshot {
/// 跑过几次骨架。
run_total: usize,
/// 被中止几次。
aborted_total: usize,
/// 已执行步数。
performed_total: usize,
/// 已跳过度数。
skipped_total: usize,
/// 已失败步数。
failed_total: usize,
}

impl SkeletonLedgerSnapshot {
/// 构造(**只在 `snapshot()` 里调用**,避免外部手搓一个"看起来合理"的快照)。
fn new(
run_total: usize,
aborted_total: usize,
performed_total: usize,
skipped_total: usize,
failed_total: usize,
) -> Self {
SkeletonLedgerSnapshot {
run_total,
aborted_total,
performed_total,
skipped_total,
failed_total,
}
}

/// 跑过几次。
pub const fn run_total(&self) -> usize {
self.run_total
}

/// 被中止几次。
pub const fn aborted_total(&self) -> usize {
self.aborted_total
}

/// 已执行步数。
pub const fn performed_total(&self) -> usize {
self.performed_total
}

/// 已跳过度数。
pub const fn skipped_total(&self) -> usize {
self.skipped_total
}

/// 已失败步数。
pub const fn failed_total(&self) -> usize {
self.failed_total
}

/// 累计步数。
pub const fn step_total(&self) -> usize {
self.performed_total + self.skipped_total + self.failed_total
}
}

/// 骨架台账(线程安全:全原子计数器,无锁)。
pub struct SkeletonLedger {
/// 跑过几次。
run_total: AtomicUsize,
/// 被中止几次。
aborted_total: AtomicUsize,
/// 已执行步数。
performed_total: AtomicUsize,
/// 已跳过度数。
skipped_total: AtomicUsize,
/// 已失败步数。
failed_total: AtomicUsize,
}

impl SkeletonLedger {
/// 空台账。
pub const fn new() -> Self {
SkeletonLedger {
run_total: AtomicUsize::new(0),
aborted_total: AtomicUsize::new(0),
performed_total: AtomicUsize::new(0),
skipped_total: AtomicUsize::new(0),
failed_total: AtomicUsize::new(0),
}
}

/// 记一次骨架执行。
///
/// ⚠️ 计数一律**各自累加**,绝不用减法推另一个:
/// 一旦出现"跳过"这条路径,`步数 − 失败` 立刻不等于"已执行"。
pub fn record_trace(&self, trace: &SkeletonTrace) {
self.run_total.fetch_add(1, Ordering::Relaxed);
if trace.aborted() {
self.aborted_total.fetch_add(1, Ordering::Relaxed);
}
self.performed_total
.fetch_add(trace.count_of(StepOutcomeKind::Performed), Ordering::Relaxed);
self.skipped_total
.fetch_add(trace.count_of(StepOutcomeKind::Skipped), Ordering::Relaxed);
self.failed_total
.fetch_add(trace.count_of(StepOutcomeKind::Failed), Ordering::Relaxed);
}

/// 快照(**只增不减的量唯一的对外读法**)。
pub fn snapshot(&self) -> SkeletonLedgerSnapshot {
SkeletonLedgerSnapshot::new(
self.run_total.load(Ordering::Relaxed),
self.aborted_total.load(Ordering::Relaxed),
self.performed_total.load(Ordering::Relaxed),
self.skipped_total.load(Ordering::Relaxed),
self.failed_total.load(Ordering::Relaxed),
)
}
}

impl Default for SkeletonLedger {
fn default() -> Self {
SkeletonLedger::new()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : skeleton_probe.rs
//! ★ 机制层自检的**负向探针**:喂四个坏样本,四条规则必须全部变红。
//!
//! ★ 恒真的检查没有价值。四条规则若只在"正常轨迹"上跑,
//! 它们永远全绿——那不是规则在守着,那是规则在自我确认。
//! 因此机制层**自带**坏样本:轨迹必须能被**手工组装**
//! (这正是 `SkeletonTrace` 把 `record` / `mark_aborted` 做成公开的动机)。
//!
//! ⚠️ 探针**自己造轨迹**,绝不用生产对象做实验:
//! 台账只增不减,拿真实轨迹做实验会把全局计数污染掉。
//!
//! 四个坏样本各自对应一条规则:
//! – 乱序 → A1 红
//! – 标了中止却没有失败 → A2 红
//! – 序号从 1 起(缺第 0 步) → A3 红
//! – 策略写"失败即中止"却没中止 → A4 红

use crate::common::template_method::audit_skeleton_trace;
use crate::common::template_method::FailurePolicy;
use crate::common::template_method::SkeletonAuditLine;
use crate::common::template_method::SkeletonStep;
use crate::common::template_method::SkeletonTrace;
use crate::common::template_method::StepCode;
use crate::common::template_method::StepOutcome;

/// 一个坏样本的核对结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProbeLine {
/// 样本名。
sample_name: &'static str,
/// 预期变红的规则编码。
expected_rule: &'static str,
/// 是否真的变红。
turned_red: bool,
}

impl ProbeLine {
/// 样本名。
pub const fn sample_name(&self) -> &'static str {
self.sample_name
}

/// 预期变红的规则编码。
pub const fn expected_rule(&self) -> &'static str {
self.expected_rule
}

/// 是否真的变红。
pub const fn turned_red(&self) -> bool {
self.turned_red
}
}

/// 第一/二步(样本共用的两个步骤码)。
const STEP_LOAD: StepCode = StepCode::new("LOAD");
/// 第二步。
const STEP_CHECK: StepCode = StepCode::new("CHECK");
/// 第三步。
const STEP_PERFORM: StepCode = StepCode::new("PERFORM");

/// 声明序列(三条样本都用它)。
fn declared() -> Vec<StepCode> {
vec![STEP_LOAD, STEP_CHECK, STEP_PERFORM]
}

/// 样本一:**乱序**(先记第二步再记第一步)。
fn sample_out_of_order() -> SkeletonTrace {
let mut trace: SkeletonTrace = SkeletonTrace::new(declared(), FailurePolicy::Abort);
trace.record(SkeletonStep::new(
0,
STEP_CHECK,
StepOutcome::performed(String::from("第二步")),
));
trace.record(SkeletonStep::new(
1,
STEP_LOAD,
StepOutcome::performed(String::from("第一步")),
));
trace
}

/// 样本二:**标了中止却没有失败**(最后一步是成功)。
fn sample_abort_without_failure() -> SkeletonTrace {
let mut trace: SkeletonTrace = SkeletonTrace::new(declared(), FailurePolicy::Abort);
trace.record(SkeletonStep::new(
0,
STEP_LOAD,
StepOutcome::performed(String::from("载入完成")),
));
trace.mark_aborted();
trace
}

/// 样本三:**序号不连续**(从 1 起,缺第 0 步)。
fn sample_index_gap() -> SkeletonTrace {
let mut trace: SkeletonTrace = SkeletonTrace::new(declared(), FailurePolicy::Abort);
trace.record(SkeletonStep::new(
1,
STEP_CHECK,
StepOutcome::performed(String::from("第二步")),
));
trace
}

/// 样本四:**策略写中止却没中止**(有失败但继续跑完了)。
fn sample_policy_not_honored() -> SkeletonTrace {
let mut trace: SkeletonTrace = SkeletonTrace::new(declared(), FailurePolicy::Abort);
trace.record(SkeletonStep::new(
0,
STEP_LOAD,
StepOutcome::failed(String::from("载入失败")),
));
trace.record(SkeletonStep::new(
1,
STEP_CHECK,
StepOutcome::performed(String::from("仍在继续")),
));
trace
}

/// 机制层自检探针:跑四个坏样本,逐条核对"预期变红的规则真的红了"。
pub struct SkeletonProbe {
/// 逐条结果。
lines: Vec<ProbeLine>,
}

impl SkeletonProbe {
/// 跑一遍四个坏样本。
pub fn run() -> Self {
let samples: [(&'static str, &'static str, SkeletonTrace); 4] = [
("乱序(先记第二步)", "A1_ORDER", sample_out_of_order()),
(
"标了中止却没有失败",
"A2_ABORT",
sample_abort_without_failure(),
),
("序号从 1 起(缺第 0 步)", "A3_INDEX", sample_index_gap()),
(
"策略写中止却没中止",
"A4_POLICY",
sample_policy_not_honored(),
),
];

let mut lines: Vec<ProbeLine> = Vec::new();
for (name, expected_rule, trace) in samples.iter() {
let audit: Vec<SkeletonAuditLine> = audit_skeleton_trace(trace);
let turned_red: bool = audit
.iter()
.any(|line| line.rule_code() == *expected_rule && !line.passed());
lines.push(ProbeLine {
sample_name: name,
expected_rule,
turned_red,
});
}
// 四个样本都跑完了,此时才成探针(不是每跑一个就变一次状态)。
SkeletonProbe { lines }
}

/// 逐条结果。
pub fn lines(&self) -> &[ProbeLine] {
self.lines.as_slice()
}

/// 变红的条数。
pub fn red_total(&self) -> usize {
self.lines.iter().filter(|line| line.turned_red()).count()
}

/// 全部变红(**这就是"机制层自检"这一项体检的依据**)。
pub fn all_turned_red(&self) -> bool {
self.red_total() == self.lines.len()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : skeleton_step.rs
//! 骨架里的一步留痕(值对象:构造后只读)。
//!
//! ★ 为什么要单独一个"步骤留痕"类型,而不是直接把结果塞进 `Vec<StepOutcome>`:
//! 「结果的种类」回答不了「这是第几步、是哪一步」。
//! 骨架自检要核对**顺序**(实际跑过的步骤是不是预期序列的前缀),
//! 因此每一步必须自带序号与步骤码。

use crate::common::template_method::StepCode;
use crate::common::template_method::StepOutcome;

/// 骨架里一步的留痕。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkeletonStep {
/// 第几步(从 0 起,骨架内的位置)。
order_index: usize,
/// 步骤码。
step_code: StepCode,
/// 这一步的结果(三态)。
outcome: StepOutcome,
}

impl SkeletonStep {
/// 造一条留痕。
pub fn new(order_index: usize, step_code: StepCode, outcome: StepOutcome) -> Self {
SkeletonStep {
order_index,
step_code,
outcome,
}
}

/// 第几步。
pub const fn order_index(&self) -> usize {
self.order_index
}

/// 步骤码。
pub const fn step_code(&self) -> StepCode {
self.step_code
}

/// 结果。
pub const fn outcome(&self) -> &StepOutcome {
&self.outcome
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : skeleton_trace.rs
//! 一次骨架执行的**完整轨迹**。
//!
//! ★ 它是"模板方法跑了一遍"这件事的**唯一物证**:
//! 骨架不可覆盖(那是类型层保证的事),而"这一次到底跑到了哪一步"
//! 只有轨迹说得清。报表上的一切结论都从这里读,不重新计算。

use crate::common::template_method::FailurePolicy;
use crate::common::template_method::SkeletonStep;
use crate::common::template_method::StepCode;
use crate::common::template_method::StepOutcome;
use crate::common::template_method::StepOutcomeKind;

/// 一次骨架执行的轨迹。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkeletonTrace {
/// 骨架声明的**完整**步骤序列(无论有没有跑到)。
declared_steps: Vec<StepCode>,
/// 实际产生的留痕(**按发生顺序**)。
steps: Vec<SkeletonStep>,
/// 是否被中止(有一步失败且策略要求中止)。
aborted: bool,
/// 本次采用的失败策略。
failure_policy: FailurePolicy,
}

impl SkeletonTrace {
/// 开一条新轨迹。
pub fn new(declared_steps: Vec<StepCode>, failure_policy: FailurePolicy) -> Self {
SkeletonTrace {
declared_steps,
steps: Vec::new(),
aborted: false,
failure_policy,
}
}

/// 记一步。
///
/// ⚠️ 这里的 `order_index` 由**调用方**(骨架)给出,不由本层自增:
/// 骨架是唯一知道"第几步"的人,机制层不该替它数数。
pub fn record(&mut self, step: SkeletonStep) {
self.steps.push(step);
}

/// 标记被中止。
pub fn mark_aborted(&mut self) {
self.aborted = true;
}

/// 是否被中止。
pub const fn aborted(&self) -> bool {
self.aborted
}

/// 失败策略。
pub const fn failure_policy(&self) -> FailurePolicy {
self.failure_policy
}

/// 声明的步骤序列。
pub fn declared_steps(&self) -> &[StepCode] {
self.declared_steps.as_slice()
}

/// 实际留痕(按发生顺序)。
pub fn steps(&self) -> &[SkeletonStep] {
self.steps.as_slice()
}

/// 实际记录了几步。
pub fn recorded_total(&self) -> usize {
self.steps.len()
}

/// 某一步有没有被记录过。
pub fn was_recorded(&self, step_code: StepCode) -> bool {
self.steps.iter().any(|step| step.step_code() == step_code)
}

/// 某一步的结果(没记录则返回 `None`)。
///
/// ★ 返回 `Option` 而不是给一个默认结果:
/// 「没跑到这一步」与「跑到了并且跳过」是两件不同的事,不能塌成一个值。
pub fn outcome_of(&self, step_code: StepCode) -> Option<&StepOutcome> {
self.steps
.iter()
.rev()
.find(|step| step.step_code() == step_code)
.map(|step| step.outcome())
}

/// 某一态出现了几次。
pub fn count_of(&self, kind: StepOutcomeKind) -> usize {
self.steps
.iter()
.filter(|step| step.outcome().kind() == kind)
.count()
}

/// 是否全部通过(每一步都是"已执行",且没有中止)。
pub fn all_performed(&self) -> bool {
!self.aborted && self.steps.iter().all(|step| !step.outcome().is_failed())
}

/// 结论文本(报表用:一句话说清这次跑得怎么样)。
pub fn conclusion_text(&self) -> String {
if self.aborted {
return format!(
"中止于第 {} 步(共声明 {} 步,实跑 {} 步)",
self.recorded_total(),
self.declared_steps.len(),
self.recorded_total()
);
}
format!(
"跑完 {} 步:已执行 {} / 已跳过 {} / 已失败 {}",
self.recorded_total(),
self.count_of(StepOutcomeKind::Performed),
self.count_of(StepOutcomeKind::Skipped),
self.count_of(StepOutcomeKind::Failed)
)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : step_code.rs
//! 步骤码:**开放型标签** → `const fn new` 的结构体,不是 `enum`。
//!
//! ★ 判据(本工程处处在用):**会不会被工程外不断新增**?
//! 骨架里"有哪些步骤"由领域层定死(那是业务骨架,不可扩展才对),
//! 但"一步作业叫什么名字"会随部门增加而增加——
//! 工程外新增第十三个部门时,它自带的步骤名不该逼我们回来改这个文件。
//!
//! ⚠️ 刻意**不**实现 `PartialEq` 以外的比较 trait:
//! 步骤码会被当作查找键(按名字找这一步的结果),所以实现 `PartialEq`;
//! 但它**不需要排序**,不实现 `Ord` / `Hash`——不实现用不上的 trait 本身就是防错。

/// 一步作业的名字。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StepCode {
/// 步骤码(形如 `LOAD`)。
code: &'static str,
}

impl StepCode {
/// 声明一个步骤码。
pub const fn new(code: &'static str) -> Self {
StepCode { code }
}

/// 步骤码文本。
pub const fn code(&self) -> &'static str {
self.code
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : step_outcome.rs
//! ★ 一步作业的结果:**三态**,不是两态。
//!
//! 两态(成功 / 失败)会把两件完全不同的事塌成一件:
//! – `Skipped`:这一步**没有发生**(钩子没启用,或前一步已中止);
//! – `Failed`:这一步**发生了,但没过**。
//!
//! 塌成"没成功"之后,「这个部门没调外部系统」会被读成「外部系统调失败了」——
//! 于是「某家下游有问题」这种结论永远得不出来。这正是三态必须存在的理由。
//!
//! ⚠️ 三态是哪三种,由 `StepOutcomeKind` 这个**封闭集合**给出(它需要被当查找键);
//! 具体说明文字是动态的(要带数值),所以放在 `StepOutcome` 里。

/// 一步结果的三态:**封闭集合** → `enum`。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StepOutcomeKind {
/// 已执行(且通过)。
Performed,
/// 已跳过(这一步没发生)。
Skipped,
/// 已执行但失败。
Failed,
}

impl StepOutcomeKind {
/// 三态编码(报表与自检里当键用)。
pub const fn code(&self) -> &'static str {
match self {
StepOutcomeKind::Performed => "PERFORMED",
StepOutcomeKind::Skipped => "SKIPPED",
StepOutcomeKind::Failed => "FAILED",
}
}

/// 中文标签。
pub const fn chinese_label(&self) -> &'static str {
match self {
StepOutcomeKind::Performed => "已执行",
StepOutcomeKind::Skipped => "已跳过",
StepOutcomeKind::Failed => "已失败",
}
}

/// 是否算"失败"。
pub const fn is_failed(&self) -> bool {
matches!(self, StepOutcomeKind::Failed)
}

/// 是否算"真的跑了这一步"。
///
/// ★ 与 `is_failed` 是两件事:跳过的那一步**没有跑**,
/// 失败的那一步**跑了**。报表上"这一步跑没跑"要用这个,不能用 `!is_failed`。
pub const fn was_executed(&self) -> bool {
matches!(self, StepOutcomeKind::Performed | StepOutcomeKind::Failed)
}
}

/// 一步作业的结果(三态 + 说明)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StepOutcome {
/// 已执行。
Performed {
/// 做了什么(要能让人复核:带上数值)。
detail: String,
},
/// 已跳过。
Skipped {
/// 为什么跳过(**必须给理由**,否则"没做"与"忘了做"不可分辨)。
reason: String,
},
/// 已执行但失败。
Failed {
/// 失败原因。
reason: String,
},
}

impl StepOutcome {
/// 已执行。
pub fn performed(detail: String) -> Self {
StepOutcome::Performed { detail }
}

/// 已跳过。
pub fn skipped(reason: String) -> Self {
StepOutcome::Skipped { reason }
}

/// 失败。
pub fn failed(reason: String) -> Self {
StepOutcome::Failed { reason }
}

/// 三态种类。
pub const fn kind(&self) -> StepOutcomeKind {
match self {
StepOutcome::Performed { .. } => StepOutcomeKind::Performed,
StepOutcome::Skipped { .. } => StepOutcomeKind::Skipped,
StepOutcome::Failed { .. } => StepOutcomeKind::Failed,
}
}

/// 说明文字(三种形态共用一个出口,报表里不必分叉)。
pub fn text(&self) -> &str {
match self {
StepOutcome::Performed { detail } => detail.as_str(),
StepOutcome::Skipped { reason } => reason.as_str(),
StepOutcome::Failed { reason } => reason.as_str(),
}
}

/// 是否失败。
pub const fn is_failed(&self) -> bool {
self.kind().is_failed()
}

/// 是否真的跑了这一步。
pub const fn was_executed(&self) -> bool {
self.kind().was_executed()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : clock.rs
//! 时钟抽象。
//!
//! ★ 关键设计:**时间戳由日志序号推导,不由"当前时刻"推导**。
//!
//! 若每次打日志都去读墙上时钟,两次运行的毫秒数必然不同 → 逐字节一致不可能;
//! 若用 `AtomicU64` 自增序号,多线程下序号分配顺序不定 → 同样不可能。
//!
//! 解法:序号由分发器(唯一入口)分配,时间戳 = 基准 + 序号 × 步长。
//! 于是「第 N 条日志的时间戳」是一个**纯函数**,与线程调度完全无关。

/// 一次取到的时间文本(日期 + 时间,都已格式化好)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimestampText {
/// 日期文本,形如 `2026-07-06`。
date_text: String,
/// 时间文本,形如 `09:00:00.000`。
time_text: String,
}

impl TimestampText {
/// 构造一个时间文本。
pub fn new(date_text: String, time_text: String) -> Self {
TimestampText {
date_text,
time_text,
}
}

/// 日期文本(用于日志目录名)。
pub fn date_text(&self) -> &str {
self.date_text.as_str()
}

/// 时间文本。
pub fn time_text(&self) -> &str {
self.time_text.as_str()
}

/// 完整文本(`日期 时间`)。
pub fn full_text(&self) -> String {
let mut out: String = self.date_text.clone();
out.push(' ');
out.push_str(self.time_text.as_str());
out
}
}

/// 时钟:**必须 `Send + Sync`**(多线程 worker 会共享同一个时钟)。
pub trait Clock: Send + Sync {
/// 第 `sequence` 条日志对应的时间文本。
fn stamp_for(&self, sequence: u64) -> TimestampText;

/// 第 `sequence` 条日志对应的日期文本(日志目录按它分文件夹)。
fn date_for(&self, sequence: u64) -> String;

/// 时钟的名字(报表里说明用的是哪一种)。
fn clock_name(&self) -> &'static str;
}

/// 固定时钟:基准时刻 + 每条步长。
///
/// 步长取 **137 毫秒**(质数):既不会让时间戳整齐得可疑,
/// 又保证 1000 条日志跨约 137 秒——不会在演示里跨天,
/// 从而「日志只落在一个日期目录」这件事是可断言的。
pub struct FixedClock {
/// 基准日期文本。
base_date: &'static str,
/// 基准时刻的毫秒数(自当日 00:00:00 起)。
base_millis: u64,
/// 每条日志前进的毫秒数。
step_millis: u64,
}

impl FixedClock {
/// 一天的毫秒数。
const MILLIS_PER_DAY: u64 = 24 * 60 * 60 * 1000;

/// 声明一个固定时钟。
pub const fn new(base_date: &'static str, base_millis: u64, step_millis: u64) -> Self {
FixedClock {
base_date,
base_millis,
step_millis,
}
}

/// 本工程默认时钟:2026-07-06 09:00:00.000 起,每条 +137ms。
pub const fn default_clock() -> Self {
FixedClock::new("2026-07-06", 9 * 60 * 60 * 1000, 137)
}

/// 把「自当日 0 点起的毫秒」格式化成 `HH:MM:SS.mmm`。
fn format_time(millis_of_day: u64) -> String {
let total_seconds: u64 = millis_of_day / 1000;
let millis: u64 = millis_of_day % 1000;
let hours: u64 = total_seconds / 3600;
let minutes: u64 = (total_seconds % 3600) / 60;
let seconds: u64 = total_seconds % 60;
format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
}
}

impl Clock for FixedClock {
fn stamp_for(&self, sequence: u64) -> TimestampText {
// ⚠️ 这里没有原子操作:给定 sequence 输出恒定,这是确定性的根源。
let offset: u64 = sequence * self.step_millis;
let millis_of_day: u64 = (self.base_millis + offset) % Self::MILLIS_PER_DAY;
TimestampText::new(
String::from(self.base_date),
Self::format_time(millis_of_day),
)
}

fn date_for(&self, _sequence: u64) -> String {
// 固定时钟刻意不跨天:日志只落在一个日期目录里,便于断言目录结构。
String::from(self.base_date)
}

fn clock_name(&self) -> &'static str {
"固定时钟(基准2026-07-06,+137ms/条)"
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : concurrency.rs
//! 并发原语的薄封装。
//!
//! 为什么还要包一层:本工程对并发的要求不是"跑得快",而是
//! **"并发下结论仍然可复现"**。封装把「计数」与「取值」收口到一处,
//! 报表上就不会出现两个口径不同的同名数字。

use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};

/// 线程安全计数器:只增不减。
///
/// 用 `Relaxed` 即可:本工程不靠它做跨线程同步(同步靠 `Mutex` / 通道),
/// 只用它做「发生了几次」的计数。
#[derive(Debug, Default)]
pub struct ConcurrentCounter {
/// 计数值。
value: AtomicUsize,
}

impl ConcurrentCounter {
/// 新建一个从 0 开始的计数器。
pub const fn new() -> Self {
ConcurrentCounter {
value: AtomicUsize::new(0),
}
}

/// 自增并返回**自增前**的值。
pub fn fetch_increment(&self) -> usize {
self.value.fetch_add(1, Ordering::Relaxed)
}

/// 自增(丢弃返回值)。
pub fn increment(&self) {
self.value.fetch_add(1, Ordering::Relaxed);
}

/// 当前值(快照)。
///
/// ⚠️ 只在**实验跑完之后**取:边跑边取会让「快照 vs 实时计数」差一个不可解释的量。
pub fn value(&self) -> usize {
self.value.load(Ordering::Relaxed)
}
}

/// 单调递增序号分配器(返回从 `start` 开始的序号)。
#[derive(Debug, Default)]
pub struct SequenceAllocator {
/// 下一个序号。
next: AtomicU64,
}

impl SequenceAllocator {
/// 从 0 开始。
pub const fn new() -> Self {
SequenceAllocator {
next: AtomicU64::new(0),
}
}

/// 分配下一个序号。
pub fn next(&self) -> u64 {
self.next.fetch_add(1, Ordering::Relaxed)
}

/// 已分配的总数。
pub fn issued(&self) -> u64 {
self.next.load(Ordering::Relaxed)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : deterministic.rs
//! 确定性工具:**两次运行必须逐字节一致**,所以任何「靠遍历顺序」的东西都不许出现。
//!
//! 铁律:
//! 1. 需要键值查找时用 `BTreeMap`(有序 = 遍历确定),**不用 `HashMap`**;
//! 2. 需要「谁先谁后」时显式排序并给**第二键**(并列时也要有唯一定义);
//! 3. 降序用「取负」,不用 `reverse()`——所有排序共用一个比较方向。

/// FNV-1a 64 位哈希:给字符串一个稳定的指纹。
///
/// 用途是把长文本压成可比较的短指纹(例如「两行内容是否一致」),
/// **绝不**用它做遍历顺序(那等于又引入了顺序来源)。
pub fn stable_hash(text: &str) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in text.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}

/// 由前缀与种子生成确定性代码(避免运行期随机数)。
pub fn deterministic_code(prefix: &str, seed: &str) -> String {
let mut out: String = String::from(prefix);
out.push('-');
out.push_str(format!("{:016x}", stable_hash(seed)).as_str());
out
}

/// 把 `u64` 压成固定宽度的十六进制指纹(报表里替代内存地址)。
///
/// ⚠️ 绝不打印指针地址:ASLR 会让两次运行的输出不同。
pub fn fingerprint_text(value: u64) -> String {
format!("{:016x}", value)
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : numeric.rs
//! 确定性数值运算:**全工程唯一的舍入口径**。
//!
//! 为什么必须收口到这一个文件:
//! 报表上「先乘后四舍五入」与「向零截断」的差通常只有 1 分钱,
//! 不崩、不告警、看起来完全合理——只有把两种口径都算一遍并数出分叉行数才抓得到。

/// 一个「万」:比率统一用**整数万分点**(basis points)表示,1 倍 = 10000。
pub const FULL_BASIS_POINTS: i64 = 10_000;

/// 绝对值(i128 版,避免 `i64::MIN.abs()` 溢出)。
pub const fn absolute_i128(value: i128) -> i128 {
if value < 0 {
-value
} else {
value
}
}

/// 两个 i64 取大。
pub const fn max_i64(left: i64, right: i64) -> i64 {
if left > right {
left
} else {
right
}
}

/// 两个 i64 取小。
pub const fn min_i64(left: i64, right: i64) -> i64 {
if left < right {
left
} else {
right
}
}

/// ★ 唯一除法口径:**远离零方向四舍五入**(half away from zero)。
///
/// 做法是先把符号摘出去、中间量全程无符号,最后补 `-`。
///
/// ⚠️ 不要写成 `(numerator / denominator) + (余数处理)` 的 `div_euclid` 拆分:
/// 下取整会把 `-104 / 1000` 拆成 `-1 / 896`,得出 `-1.896` 而正确答案是 `-0.104`。
/// 这种错误不崩、不告警,在报表上长得完全合理。
pub fn divide_rounded(numerator: i128, denominator: i128) -> i64 {
// 分母为 0 是调用方的逻辑错误,直接让它在调试期炸出来,而不是静默返回 0。
debug_assert!(denominator != 0, "除法的分母不得为 0");

let magnitude: i128 = absolute_i128(numerator);
let divisor: i128 = absolute_i128(denominator);
let quotient: i128 = magnitude / divisor;
let remainder: i128 = magnitude % divisor;
// 余数达到除数的一半即进位:`2 * remainder >= divisor`。
let carry: i128 = if 2_i128 * remainder >= divisor {
1
} else {
0
};
let unsigned_result: i128 = quotient + carry;
// 符号:分子分母异号为负。
let negative: bool = (numerator < 0) != (denominator < 0);
let signed: i128 = if negative {
-unsigned_result
} else {
unsigned_result
};
signed as i64
}

/// 按比率缩放:`value * numerator / denominator`,只在这里舍入一次。
pub fn scale_rounded(value: i128, numerator: i128, denominator: i128) -> i64 {
divide_rounded(value * numerator, denominator)
}

/// 比率(万分点):`part / whole`,返回 0..=10000 之外的整数也能被表达。
pub fn ratio_basis_points(part: i128, whole: i128) -> i64 {
if whole == 0 {
return 0;
}
divide_rounded(part * FULL_BASIS_POINTS as i128, whole)
}

/// 向零截断(**只在负向对照实验里用**,正式口径永远是 `divide_rounded`)。
///
/// ★ 保留它是为了「证明两种口径真的会分叉」:
/// 若演示数据全部整除,那条对照恒真,等于没有对照。
pub fn divide_toward_zero(numerator: i128, denominator: i128) -> i64 {
debug_assert!(denominator != 0, "除法的分母不得为 0");
(numerator / denominator) as i64
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:模板方法模式Template Method Pattern 行为模式 Behavioral Patterns
//!# 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/19 21:26
//!# User : geovindu
//!# Product : RustRover
//!# Project : templatemethodpattern
//!# File : text_layout.rs
//! 等宽终端的显示宽度模型与表格渲染原语。
//!
//! CJK 字符占 **2** 列,而 Rust 的 `{:<N}` 按 char 数补齐——
//! 直接 `{:<N}` 会让所有含中文的列错位。宽度必须自己算。

/// 一个字符的显示列数。
///
/// ⚠️ 刻意**不是** `const fn`:`RangeInclusive::contains` 目前还不是 const fn,
/// 强行写 const 只会换来一条 `E0658`。「能在编译期算」在这里没有任何收益——
/// 它被 `display_width` 在每个字符上调用,真正的成本在那儿。
fn char_display_width(character: char) -> usize {
let code: u32 = character as u32;
// 以下每一段都是闭区间判断,写成分开的比较以便一眼看出边界。
let wide: bool = (code >= 0x1100 && code <= 0x115F)
|| (code >= 0x2E80 && code <= 0xA4CF)
|| (code >= 0xAC00 && code <= 0xD7A3)
|| (code >= 0xF900 && code <= 0xFAFF)
|| (code >= 0xFE30 && code <= 0xFE6F)
|| (code >= 0xFF00 && code <= 0xFF60)
|| (code >= 0xFFE0 && code <= 0xFFE6)
// ★ 以下两段是**东亚歧义宽度**字符(终端可以画 1 列也可以画 2 列):
// 几何图形(■ □ ▲ ◆ ○ ●)与杂项符号(★ ☆)。
// 本工程统一按 **2 列**处理——报表是等宽表格,
// 宽度必须自己说了算,不能交给终端决定。
// ⚠️ 注意边界:框线 `─` U+2500 / `│` U+2502 / `━` U+2501 都在 0x25A0 **之前**,
// 它们仍是 1 列——把它们算成 2 列,框线会比内容行短一格。
|| (code >= 0x25A0 && code <= 0x25FF)
|| (code >= 0x2605 && code <= 0x2606)
|| (code >= 0x20000 && code <= 0x3FFFD);
if wide {
2
} else {
1
}
}

/// 文本的显示列数。
pub fn display_width(text: &str) -> usize {
text.chars().map(char_display_width).sum()
}

/// 左侧对齐并补空格到指定显示宽度。
pub fn pad_right(text: &str, width: usize) -> String {
let used: usize = display_width(text);
if used >= width {
return String::from(text);
}
let mut out: String = String::from(text);
for _ in 0..(width – used) {
out.push(' ');
}
out
}

/// 右侧对齐并补空格到指定显示宽度(数字列用)。
pub fn pad_left(text: &str, width: usize) -> String {
let used: usize = display_width(text);
if used >= width {
return String::from(text);
}
let mut out: String = String::new();
for _ in 0..(width – used) {
out.push(' ');
}
out.push_str(text);
out
}

/// 居中(左右差额先给右边)。
pub fn pad_center(text: &str, width: usize) -> String {
let used: usize = display_width(text);
if used >= width {
return String::from(text);
}
let total: usize = width – used;
let left: usize = total / 2;
let right: usize = total – left;
let mut out: String = String::new();
for _ in 0..left {
out.push(' ');
}
out.push_str(text);
for _ in 0..right {
out.push(' ');
}
out
}

/// 截断到指定显示宽度(超宽时砍到 `width`,绝不静默超宽)。
pub fn truncate_to_width(text: &str, width: usize) -> String {
let mut used: usize = 0;
let mut out: String = String::new();
for character in text.chars() {
let cell: usize = char_display_width(character);
if used + cell > width {
break;
}
used += cell;
out.push(character);
}
out
}

/// 宽度模型自检:几条已知答案的断言。
///
/// ★ 宽度常量本身也要被审计:`U+2500`(─)不在 `0x2E80..` 里,它占 1 列,
/// 若把它当 2 列,框线就会比内容行短一格。
pub fn self_check_width_model() -> bool {
let mut ok: bool = true;
ok &= display_width("珠宝") == 4;
ok &= display_width("abc") == 3;
ok &= display_width("─") == 1;
ok &= display_width("│") == 1;
ok &= display_width("★") == 2;
ok &= display_width("珠宝abc") == 7;
ok &= display_width(pad_center("中", 6).as_str()) == 6;
ok &= display_width(pad_left("12", 8).as_str()) == 8;
ok &= display_width(truncate_to_width("珠宝企业", 5).as_str()) == 4;
ok
}

代码未完后续篇

赞(0)
未经允许不得转载:网硕互联帮助中心 » rust: Template Method Pattern
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!