rust: Template Method Pattern

项目结构:

rust 复制代码
//!# 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)
    }
}
rust 复制代码
//!# 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
}
rust 复制代码
//!# 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
}

代码未完后续篇

相关推荐
BingoGo1 小时前
PHP 8.7 协程新提案 让跨协程代码共享请求身份
后端·php
星轨zb1 小时前
[专题5]支付发放物品的一致性保障:事务、幂等与状态流转
数据库·后端·mongodb·支付架构学习
JaguarJack1 小时前
PHP 8.7 协程新提案 让跨协程代码共享请求身份
后端·php·服务端
vipxieliang1 小时前
Go 泛型结构体与约束进阶:从自定义约束到标准库泛型 API 实战
后端·go
isfox1 小时前
Python 文件操作:读写文件这件事,记住 with open 就够了
后端
武子康2 小时前
虚拟线程不是无限并发:Java Agent 后端真正该限制的是这些资源
人工智能·后端·agent
未秃头的程序猿2 小时前
Spring Cloud Gateway 做统一加解密,我在请求体上栽了三天
java·后端·架构