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      : department_catalog.rs
//! 部门字典:**中文名与说明在这一层,不在领域层**。
//!
//! ★ 为什么显示名不能进 `domain`:
//! 换一套叫法("质检"改成"品质检验")是**展示口径**的变化,
//! 若名字写在领域里,改名就要动核心------那表示层就没起到隔离作用。
//!
//! ★ 为什么是"可登记的目录"而不是 `match`:
//! 工程外新增第十三个部门时,它要能**带着自己的中文名一起登记**,
//! 而不是回来改这个文件。判据与开放型标签一致:**会不会不断增加** → 会。

use crate::domain::DepartmentCode;

/// 一个部门的登记条目。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DepartmentEntry {
    /// 部门标识。
    code: DepartmentCode,
    /// 中文名。
    name: &'static str,
    /// 说明(报表里解释"这个部门做什么")。
    note: &'static str,
    /// 来源(`内置` / `扩展`)。
    source: &'static str,
}

impl DepartmentEntry {
    /// 内置条目。
    pub const fn builtin(code: DepartmentCode, name: &'static str, note: &'static str) -> Self {
        DepartmentEntry {
            code,
            name,
            note,
            source: "内置",
        }
    }

    /// 扩展条目(**来源写"扩展"**:报表要能数出"有几个是工程外加的")。
    pub const fn extension(code: DepartmentCode, name: &'static str, note: &'static str) -> Self {
        DepartmentEntry {
            code,
            name,
            note,
            source: "扩展",
        }
    }

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

    /// 中文名。
    pub const fn name(&self) -> &'static str {
        self.name
    }

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

    /// 来源。
    pub const fn source(&self) -> &'static str {
        self.source
    }
}

/// 部门目录。
pub struct DepartmentCatalog {
    /// 已登记条目(**按登记顺序**)。
    entries: Vec<DepartmentEntry>,
}

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

    /// ★ 登记一个新部门(**扩展区走这里**,既有分层文件一行不改)。
    pub fn register(&mut self, entry: DepartmentEntry) {
        // 重复登记直接替换:同一个部门登记两次是装配错误,
        // 静默留两条会让"部门数"虚高,而报表上看不出原因。
        if let Some(position) = self
            .entries
            .iter()
            .position(|item| item.code() == entry.code())
        {
            self.entries[position] = entry;
            return;
        }
        self.entries.push(entry);
    }

    /// 全部条目(按登记顺序)。
    pub fn entries(&self) -> &[DepartmentEntry] {
        self.entries.as_slice()
    }

    /// 条目数。
    pub fn entry_total(&self) -> usize {
        self.entries.len()
    }

    /// 扩展条目数(**报表要能数出来**)。
    pub fn extension_total(&self) -> usize {
        self.entries
            .iter()
            .filter(|item| item.source() == "扩展")
            .count()
    }

    /// 找一条。
    pub fn entry_of(&self, code: DepartmentCode) -> Option<&DepartmentEntry> {
        self.entries.iter().find(|item| item.code() == code)
    }

    /// ★ 显示名:找不到时给**显式缺失标记**,而不是回退成部门编码。
    ///
    /// 回退成编码会让"没登记"与"登记了但名字就叫 PROCUREMENT"长得一模一样。
    pub fn display_name_of(&self, code: DepartmentCode) -> String {
        match self.entry_of(code) {
            Some(entry) => String::from(entry.name()),
            None => format!("<未登记:{}>", code.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_catalog.rs
//! 骨架步骤字典:**七步的中文名与归属**。
//!
//! ★ 归属(骨架 / 部门 / 钩子)是这张表最有用的一列:
//! 读者最想知道的就是"我要扩展一个部门,得写哪几步"。
//! 答案只有三步(准入 / 作业 / 自检),而这张表把它**数出来**了。

use crate::common::template_method::StepCode;
use crate::domain::SkeletonStepCode;
use crate::domain::SKELETON_STEP_SEQUENCE;

/// 一步的登记条目。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StepEntry {
    /// 步骤码。
    code: StepCode,
    /// 中文名。
    name: &'static str,
    /// 归属(`骨架` / `部门` / `钩子`)。
    owner: &'static str,
    /// 说明。
    note: &'static str,
}

impl StepEntry {
    /// 构造一条。
    pub const fn new(code: StepCode, name: &'static str, owner: &'static str, note: &'static str) -> Self {
        StepEntry {
            code,
            name,
            owner,
            note,
        }
    }

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

    /// 中文名。
    pub const fn name(&self) -> &'static str {
        self.name
    }

    /// 归属。
    pub const fn owner(&self) -> &'static str {
        self.owner
    }

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

/// 步骤目录(**封闭**:骨架七步是业务骨架,不开放扩展------
/// 能扩展的是"部门",不是"骨架有几步")。
pub struct StepCatalog {
    /// 条目(顺序即骨架顺序)。
    entries: Vec<StepEntry>,
}

impl StepCatalog {
    /// 内置目录。
    pub fn builtin() -> Self {
        StepCatalog {
            entries: vec![
                StepEntry::new(SkeletonStepCode::LOAD, "领单", "骨架", "骨架读上下文,部门看不到"),
                StepEntry::new(SkeletonStepCode::ENTRY, "准入", "部门", "部门必须实现:这一步不过就没必要往下走"),
                StepEntry::new(SkeletonStepCode::PERFORM, "作业", "部门", "部门必须实现:唯一可以改金额的一步"),
                StepEntry::new(SkeletonStepCode::EXTERNAL, "外呼", "钩子", "部门声明要不要调外部系统;不用时记跳过"),
                StepEntry::new(SkeletonStepCode::INSPECT, "自检", "部门", "部门必须实现:自己给自己把关"),
                StepEntry::new(SkeletonStepCode::EVIDENCE, "留痕", "骨架", "骨架统一留痕,部门改不掉"),
                StepEntry::new(SkeletonStepCode::HANDOFF, "移交", "骨架", "骨架统一移交,部门改不掉"),
            ],
        }
    }

    /// 全部条目(顺序即骨架顺序)。
    pub fn entries(&self) -> &[StepEntry] {
        self.entries.as_slice()
    }

    /// 条目数(**必须等于骨架声明的步数**)。
    pub fn entry_total(&self) -> usize {
        self.entries.len()
    }

    /// ★ 自检:目录条数 == 骨架声明的步数。
    ///
    /// 少了说明某一步没有中文名(报表会显示缺失标记);
    /// 多了说明有人往目录里加了骨架根本没有的步骤------两种情况都不该静默。
    pub fn covers_declared_sequence(&self) -> bool {
        if self.entries.len() != SKELETON_STEP_SEQUENCE.len() {
            return false;
        }
        SKELETON_STEP_SEQUENCE
            .iter()
            .enumerate()
            .all(|(index, code)| self.entries[index].code() == *code)
    }

    /// 找一条。
    pub fn entry_of(&self, code: StepCode) -> Option<&StepEntry> {
        self.entries.iter().find(|item| item.code() == code)
    }

    /// 显示名(找不到时给**显式缺失标记**)。
    pub fn display_name_of(&self, code: StepCode) -> String {
        match self.entry_of(code) {
            Some(entry) => String::from(entry.name()),
            None => format!("<未登记:{}>", code.code()),
        }
    }

    /// 归属(找不到时给**显式缺失标记**)。
    pub fn display_owner_of(&self, code: StepCode) -> String {
        match self.entry_of(code) {
            Some(entry) => String::from(entry.owner()),
            None => String::from("<未登记>"),
        }
    }

    /// 部门必须实现的步数(**"扩展一个部门要写几步"的答案**)。
    pub fn department_owned_total(&self) -> usize {
        self.entries
            .iter()
            .filter(|item| item.owner() == "部门")
            .count()
    }
}
//!# 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      : command.rs
//! 命令对象:**入口层只发命令,不自己编排**。
//!
//! ★ 为什么要命令这一层:
//! 入口(调度器 / 接口端点 / 组合根)都要"让某个部门对某张单作业一次"。
//! 若各写一遍调用,就会长出三种略有不同的调用姿势,
//! 而"某个部门没走骨架"这种错误只会在其中一种姿势里出现。

use crate::domain::DepartmentCode;

/// 一次「某部门对某工单作业」的命令。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdvanceCommand {
    /// 工单号。
    order_number_text: String,
    /// 部门。
    department_code: DepartmentCode,
}

impl AdvanceCommand {
    /// 声明一条命令。
    pub fn new(order_number_text: String, department_code: DepartmentCode) -> Self {
        AdvanceCommand {
            order_number_text,
            department_code,
        }
    }

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

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

/// 一批命令(**顺序即执行顺序**)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdvanceBatchCommand {
    /// 命令列表。
    commands: Vec<AdvanceCommand>,
}

impl AdvanceBatchCommand {
    /// 声明一批命令。
    pub fn new(commands: Vec<AdvanceCommand>) -> Self {
        AdvanceBatchCommand { commands }
    }

    /// 命令列表。
    pub fn commands(&self) -> &[AdvanceCommand] {
        self.commands.as_slice()
    }

    /// 命令条数。
    pub fn command_total(&self) -> usize {
        self.commands.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      : handler.rs
//! 命令处理器:**命令 → 用例 → 日志**。
//!
//! ★ 为什么日志在这里而不是在各入口:
//! 「一次作业成功了没有」这句话只有一处该说。
//! 若调度器、端点、组合根各写一句,口径会慢慢分叉
//! (一处写"跑了几步",另一处写"是否成功"),报表上就对不上了。

use std::sync::Arc;

use crate::application::DepartmentRegistry;
use crate::application::ProcessApplicationService;
use crate::application::RunOutcome;
use crate::common::logging::AppLogger;
use crate::domain::DepartmentCode;
use crate::domain::RemoteGateway;

/// 命令处理器。
pub struct CommandHandler {
    /// 应用服务。
    service: Arc<ProcessApplicationService>,
    /// 部门注册表。
    registry: Arc<DepartmentRegistry>,
    /// 日志对象。
    logger: Arc<dyn AppLogger>,
}

impl CommandHandler {
    /// 建一个处理器。
    pub fn new(
        service: Arc<ProcessApplicationService>,
        registry: Arc<DepartmentRegistry>,
        logger: Arc<dyn AppLogger>,
    ) -> Self {
        CommandHandler {
            service,
            registry,
            logger,
        }
    }

    /// ★ 处理一批命令(**顺序执行**,返回与命令**一一对应**的产出)。
    ///
    /// ⚠️ 产出条数必须等于命令条数:
    /// "某个部门没装执行器"也要占一行(结论列写明原因),
    /// 比悄悄跳过好------跳过会让"部门数"与"行数"对不上。
    pub fn handle_batch(
        &self,
        batch: &crate::application::AdvanceBatchCommand,
        gateway: Option<&dyn RemoteGateway>,
    ) -> Vec<RunOutcome> {
        let mut outcomes: Vec<RunOutcome> = Vec::new();
        for (position, command) in batch.commands().iter().enumerate() {
            self.logger.debug(
                format!(
                    "处理命令 #{}:工单 {} / 部门 {}",
                    position,
                    command.order_number_text(),
                    command.department_code().code()
                )
                .as_str(),
            );
            let outcome: RunOutcome = match self.registry.runner_of(command.department_code()) {
                Some(runner) => {
                    self.service
                        .run_department(command.order_number_text(), runner, gateway)
                }
                None => {
                    self.logger.error(
                        format!(
                            "部门 {} 没有登记执行器,命令 #{} 未执行",
                            command.department_code().code(),
                            position
                        )
                        .as_str(),
                    );
                    registry_missing_outcome(command.order_number_text(), command.department_code())
                }
            };
            outcomes.push(outcome);
        }
        outcomes
    }
}

/// 构造一个"注册表里没有这个部门"的产出(**必须占一行**)。
fn registry_missing_outcome(
    order_number_text: &str,
    department_code: DepartmentCode,
) -> RunOutcome {
    RunOutcome::new(
        String::from(order_number_text),
        department_code,
        Vec::<crate::application::OutcomeStep>::new(),
        0,
        0,
        0,
        0,
        false,
        false,
        "-",
        0,
        true,
        String::from("未执行:注册表里没有这个部门的执行器"),
        String::new(),
        false,
        false,
    )
}
//!# 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_dto.rs
//! 部门作业的 DTO:**一次骨架执行 = 一行**,外加**每一步 = 一行**。
//!
//! ★ 为什么要两级:
//! 「这个部门跑没跑通」与「跑到第几步停的」是两个不同的问题。
//! 只有汇总行时,"跑了 5 步"与"跑了 7 步里有 2 步跳过"在报表上长得一样。

use crate::application::DepartmentCatalog;
use crate::application::RunOutcome;
use crate::application::StepCatalog;
use crate::common::template_method::StepOutcomeKind;

/// 一次骨架执行的汇总行。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepartmentRunDto {
    /// 工单号。
    order_number_text: String,
    /// 部门编码。
    department_code: &'static str,
    /// 部门中文名(**由装配层字典给出**,DTO 不自己翻译)。
    department_name: String,
    /// 声明步数。
    declared_step_total: usize,
    /// 实跑步数。
    recorded_step_total: usize,
    /// 已执行步数。
    performed_total: usize,
    /// 已跳过度数。
    skipped_total: usize,
    /// 已失败步数。
    failed_total: usize,
    /// 是否中止。
    aborted: bool,
    /// 是否跑通。
    succeeded: bool,
    /// 失败策略编码。
    failure_policy_code: &'static str,
    /// 金额变更(分,**只有核心作业会产生**)。
    money_delta_minor: i64,
    /// 金额变更是否合法(核心作业没跑成功时必须是 0)。
    money_change_legal: bool,
    /// 结论文本。
    conclusion: String,
    /// 外部调用详情(没调时为空串)。
    external_detail: String,
}

impl DepartmentRunDto {
    /// 构造一行(字段多是因为"报表要什么就给什么",不做二次翻译)。
    pub fn new(
        order_number_text: String,
        department_code: &'static str,
        department_name: String,
        declared_step_total: usize,
        recorded_step_total: usize,
        performed_total: usize,
        skipped_total: usize,
        failed_total: usize,
        aborted: bool,
        succeeded: bool,
        failure_policy_code: &'static str,
        money_delta_minor: i64,
        money_change_legal: bool,
        conclusion: String,
        external_detail: String,
    ) -> Self {
        DepartmentRunDto {
            order_number_text,
            department_code,
            department_name,
            declared_step_total,
            recorded_step_total,
            performed_total,
            skipped_total,
            failed_total,
            aborted,
            succeeded,
            failure_policy_code,
            money_delta_minor,
            money_change_legal,
            conclusion,
            external_detail,
        }
    }

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

    /// 部门编码。
    pub const fn department_code(&self) -> &'static str {
        self.department_code
    }

    /// 部门中文名。
    pub fn department_name(&self) -> &str {
        self.department_name.as_str()
    }

    /// 声明步数。
    pub const fn declared_step_total(&self) -> usize {
        self.declared_step_total
    }

    /// 实跑步数。
    pub const fn recorded_step_total(&self) -> usize {
        self.recorded_step_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 aborted(&self) -> bool {
        self.aborted
    }

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

    /// 失败策略编码。
    pub const fn failure_policy_code(&self) -> &'static str {
        self.failure_policy_code
    }

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

    /// 金额变更是否合法。
    pub const fn money_change_legal(&self) -> bool {
        self.money_change_legal
    }

    /// 结论文本。
    pub fn conclusion(&self) -> &str {
        self.conclusion.as_str()
    }

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

/// 骨架里**一步**的行。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkeletonStepDto {
    /// 工单号。
    order_number_text: String,
    /// 部门编码。
    department_code: &'static str,
    /// 第几步。
    order_index: usize,
    /// 步骤编码。
    step_code: &'static str,
    /// 步骤中文名。
    step_name: String,
    /// 归属(`骨架` / `部门` / `钩子`)。
    owner: &'static str,
    /// 结果三态。
    outcome_kind: StepOutcomeKind,
    /// 说明。
    detail: String,
}

impl SkeletonStepDto {
    /// 构造一行。
    pub fn new(
        order_number_text: String,
        department_code: &'static str,
        order_index: usize,
        step_code: &'static str,
        step_name: String,
        owner: &'static str,
        outcome_kind: StepOutcomeKind,
        detail: String,
    ) -> Self {
        SkeletonStepDto {
            order_number_text,
            department_code,
            order_index,
            step_code,
            step_name,
            owner,
            outcome_kind,
            detail,
        }
    }

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

    /// 部门编码。
    pub const fn department_code(&self) -> &'static str {
        self.department_code
    }

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

    /// 步骤编码。
    pub const fn step_code(&self) -> &'static str {
        self.step_code
    }

    /// 步骤中文名。
    pub fn step_name(&self) -> &str {
        self.step_name.as_str()
    }

    /// 归属。
    pub const fn owner(&self) -> &'static str {
        self.owner
    }

    /// 结果三态。
    pub const fn outcome_kind(&self) -> StepOutcomeKind {
        self.outcome_kind
    }

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

/// ★ 汇总行的装配:**报表不该自己拼字段**。
///
/// 放在 DTO 旁边而不是报表里,是因为"这一列从哪个字段来"属于数据形状,
/// 属于"这一列多宽、怎么对齐"才属于表示层。
pub fn build_run_dtos(outcomes: &[RunOutcome], catalog: &DepartmentCatalog) -> Vec<DepartmentRunDto> {
    let mut rows: Vec<DepartmentRunDto> = Vec::new();
    for outcome in outcomes.iter() {
        rows.push(DepartmentRunDto::new(
            String::from(outcome.order_number_text()),
            outcome.department_code().code(),
            catalog.display_name_of(outcome.department_code()),
            // 声明步数来自**骨架**,不是来自这次跑了多久。
            crate::domain::SKELETON_STEP_SEQUENCE.len(),
            outcome.recorded_step_total(),
            outcome.performed_total(),
            outcome.skipped_total(),
            outcome.failed_total(),
            outcome.aborted(),
            outcome.succeeded(),
            outcome.failure_policy_code(),
            outcome.money_delta_minor(),
            outcome.money_change_legal(),
            String::from(outcome.conclusion()),
            String::from(outcome.external_detail()),
        ));
    }
    rows
}

/// ★ 逐步行的装配(**一行 = 一次执行 × 一步**)。
pub fn build_step_dtos(
    outcomes: &[RunOutcome],
    step_catalog: &StepCatalog,
) -> Vec<SkeletonStepDto> {
    let mut rows: Vec<SkeletonStepDto> = Vec::new();
    for outcome in outcomes.iter() {
        for step in outcome.steps().iter() {
            let code: crate::common::template_method::StepCode =
                crate::common::template_method::StepCode::new(step.step_code());
            rows.push(SkeletonStepDto::new(
                String::from(outcome.order_number_text()),
                outcome.department_code().code(),
                step.order_index(),
                step.step_code(),
                step_catalog.display_name_of(code),
                owner_text(step_catalog.display_owner_of(code)),
                step.outcome_kind(),
                String::from(step.detail()),
            ));
        }
    }
    rows
}

/// 归属文本收口成 `&'static str`(**DTO 不持有 `String` 做枚举值**)。
fn owner_text(display: String) -> &'static str {
    match display.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      : gateway_call_dto.rs
//! 外部系统调用的 DTO:**一次调用最终怎么样**,不是某一次尝试的结果。
//!
//! ★ 三个字段缺一不可:
//! - `attempt_total`:重试了几次;
//! - `succeeded`:最终成没成;
//! - `circuit_blocked`:**被熔断挡下**(根本没发出去)。
//!
//! ⚠️ 被熔断挡下的那次**不**算下游失败:
//! 它没发出去,算进失败会让下游显得更糟,而熔断器会因此更难恢复(雪球越滚越大)。

/// 一次外部系统调用的结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayCallDto {
    /// 外部系统名。
    system_name: &'static str,
    /// 操作名。
    operation: &'static str,
    /// 尝试次数。
    attempt_total: usize,
    /// 最终是否成功。
    succeeded: bool,
    /// 是否被熔断挡下。
    circuit_blocked: bool,
    /// 累计退避毫秒。
    backoff_total: u64,
    /// 结论说明。
    note: String,
}

impl GatewayCallDto {
    /// 构造一条调用结果。
    pub fn new(
        system_name: &'static str,
        operation: &'static str,
        attempt_total: usize,
        succeeded: bool,
        circuit_blocked: bool,
        backoff_total: u64,
        note: String,
    ) -> Self {
        GatewayCallDto {
            system_name,
            operation,
            attempt_total,
            succeeded,
            circuit_blocked,
            backoff_total,
            note,
        }
    }

    /// 外部系统名。
    pub const fn system_name(&self) -> &'static str {
        self.system_name
    }

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

    /// 尝试次数。
    pub const fn attempt_total(&self) -> usize {
        self.attempt_total
    }

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

    /// 是否被熔断挡下。
    pub const fn circuit_blocked(&self) -> bool {
        self.circuit_blocked
    }

    /// 累计退避毫秒。
    pub const fn backoff_total(&self) -> u64 {
        self.backoff_total
    }

    /// 结论说明。
    pub fn note(&self) -> &str {
        self.note.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      : order_dtors
//! 工单一览的 DTO:**一行一张单**。
//!
//! ★ 币种符号由装配层给(`&str`),DTO **不**持有 `CurrencyCode`:
//! 表示层一旦认识 `CurrencyCode`,就形成了 `presentation -> domain` 这条不该有的边。

/// 工单概览行。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderRowDto {
    /// 工单号。
    order_number_text: String,
    /// 作品名。
    product_name: String,
    /// 币种符号。
    currency_symbol: &'static str,
    /// 报价(分)。
    quoted_minor: i64,
    /// 已收(分)。
    received_minor: i64,
    /// 未收(分)。
    outstanding_minor: i64,
    /// 物料成本(分)。
    material_cost_minor: i64,
    /// 人工成本(分)。
    labor_cost_minor: i64,
    /// 期间费用(分)。
    expense_minor: i64,
    /// 成本合计(分)。
    cost_total_minor: i64,
    /// 毛利(分)。
    profit_minor: i64,
    /// 毛利率(万分点)。
    margin_basis_points: i64,
    /// 已完成部门数。
    completed_department_total: usize,
    /// 累计工时(分钟)。
    work_minutes: i64,
    /// 留痕条数。
    evidence_total: usize,
}

impl OrderRowDto {
    /// 构造一行。
    ///
    /// ⚠️ 参数多是因为"一行报表要什么就给什么":
    /// 收成一个 struct 会让"这一列从哪来"重新变得不透明。
    pub fn new(
        order_number_text: String,
        product_name: String,
        currency_symbol: &'static str,
        quoted_minor: i64,
        received_minor: i64,
        outstanding_minor: i64,
        material_cost_minor: i64,
        labor_cost_minor: i64,
        expense_minor: i64,
        cost_total_minor: i64,
        profit_minor: i64,
        margin_basis_points: i64,
        completed_department_total: usize,
        work_minutes: i64,
        evidence_total: usize,
    ) -> Self {
        OrderRowDto {
            order_number_text,
            product_name,
            currency_symbol,
            quoted_minor,
            received_minor,
            outstanding_minor,
            material_cost_minor,
            labor_cost_minor,
            expense_minor,
            cost_total_minor,
            profit_minor,
            margin_basis_points,
            completed_department_total,
            work_minutes,
            evidence_total,
        }
    }

    /// 工单号。
    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_symbol(&self) -> &'static str {
        self.currency_symbol
    }

    /// 报价(分)。
    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 cost_total_minor(&self) -> i64 {
        self.cost_total_minor
    }

    /// 毛利(分)。
    pub const fn profit_minor(&self) -> i64 {
        self.profit_minor
    }

    /// 毛利率(万分点)。
    pub const fn margin_basis_points(&self) -> i64 {
        self.margin_basis_points
    }

    /// 已完成部门数。
    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 evidence_total(&self) -> usize {
        self.evidence_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      : system_dto.rs
//! 系统画像与骨架台账的 DTO。

/// 系统画像:这一跑用了什么时钟、装了哪些落地、产生了多少条日志。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemProfileDto {
    /// 时钟名字。
    clock_name: &'static str,
    /// 已产生日志总条数。
    log_total: u64,
    /// 各等级条数(顺序与 `LogLevel::ALL` 一致)。
    level_totals: [usize; 4],
    /// 日志目录。
    log_directory: String,
    /// 仓储实现名。
    repository_name: &'static str,
    /// 注册了几条骨架执行器。
    runner_total: usize,
}

impl SystemProfileDto {
    /// 构造一份画像。
    pub fn new(
        clock_name: &'static str,
        log_total: u64,
        level_totals: [usize; 4],
        log_directory: String,
        repository_name: &'static str,
        runner_total: usize,
    ) -> Self {
        SystemProfileDto {
            clock_name,
            log_total,
            level_totals,
            log_directory,
            repository_name,
            runner_total,
        }
    }

    /// 时钟名字。
    pub const fn clock_name(&self) -> &'static str {
        self.clock_name
    }

    /// 日志总条数。
    pub const fn log_total(&self) -> u64 {
        self.log_total
    }

    /// 各等级条数。
    pub const fn level_totals(&self) -> [usize; 4] {
        self.level_totals
    }

    /// 某一等级的条数。
    pub const fn level_total(&self, index: usize) -> usize {
        self.level_totals[index]
    }

    /// 日志目录。
    pub fn log_directory(&self) -> &str {
        self.log_directory.as_str()
    }

    /// 仓储实现名。
    pub const fn repository_name(&self) -> &'static str {
        self.repository_name
    }

    /// 骨架执行器条数。
    pub const fn runner_total(&self) -> usize {
        self.runner_total
    }
}

/// 骨架台账的 DTO(**快照**:只增不减的量只能这样读)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SkeletonLedgerDto {
    /// 跑过几次。
    run_total: usize,
    /// 中止几次。
    aborted_total: usize,
    /// 已执行步数。
    performed_total: usize,
    /// 已跳过度数。
    skipped_total: usize,
    /// 已失败步数。
    failed_total: usize,
}

impl SkeletonLedgerDto {
    /// 构造。
    pub const fn new(
        run_total: usize,
        aborted_total: usize,
        performed_total: usize,
        skipped_total: usize,
        failed_total: usize,
    ) -> Self {
        SkeletonLedgerDto {
            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
    }
}
//!# 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_registry.rs
//! 部门注册表:**部门实现 → 装上骨架**。
//!
//! ★ 为什么"装骨架"这一步要单独存在:
//! 部门实现(`DepartmentSteps`)自己**跑不起来**------它没有骨架。
//! 于是"某个部门被漏装了"是一个可检查的状态(注册表里没有它),
//! 而不是"它自己跑了一遍别的流程"。
//!
//! ★ 扩展区从这里挂新部门:`register(Arc<dyn DepartmentSteps>)`,
//! 骨架由本文件统一套上,扩展方**碰不到**骨架。

use std::sync::Arc;

use crate::domain::DepartmentCode;
use crate::domain::DepartmentProcessRunner;
use crate::domain::DepartmentSteps;

/// 部门注册表。
pub struct DepartmentRegistry {
    /// 已装配的骨架执行器(**按登记顺序**)。
    runners: Vec<Arc<DepartmentProcessRunner>>,
}

impl DepartmentRegistry {
    /// 空注册表。
    pub const fn new() -> Self {
        DepartmentRegistry {
            runners: Vec::new(),
        }
    }

    /// ★ 登记一个部门实现(**骨架在内部套上**)。
    pub fn register(&mut self, steps: Arc<dyn DepartmentSteps>) {
        let code: DepartmentCode = steps.department_code();
        // 重复登记替换而不是追加:同一个部门装两次会让"部门数"虚高,
        // 而报表上只读得出"多了一行"。
        if let Some(position) = self
            .runners
            .iter()
            .position(|runner| runner.department_code() == code)
        {
            self.runners[position] = Arc::new(DepartmentProcessRunner::new(steps));
            return;
        }
        self.runners
            .push(Arc::new(DepartmentProcessRunner::new(steps)));
    }

    /// 全部执行器(按登记顺序)。
    pub fn runners(&self) -> &[Arc<DepartmentProcessRunner>] {
        self.runners.as_slice()
    }

    /// 执行器条数。
    pub fn runner_total(&self) -> usize {
        self.runners.len()
    }

    /// 按部门号找执行器(**没找到返回 `None`**,绝不给一个默认实现凑数)。
    pub fn runner_of(&self, department_code: DepartmentCode) -> Option<&Arc<DepartmentProcessRunner>> {
        self.runners
            .iter()
            .find(|runner| runner.department_code() == department_code)
    }

    /// 全部部门号(按登记顺序)。
    pub fn department_codes(&self) -> Vec<DepartmentCode> {
        let mut codes: Vec<DepartmentCode> = Vec::new();
        for runner in self.runners.iter() {
            codes.push(runner.department_code());
        }
        codes
    }
}

impl Default for DepartmentRegistry {
    fn default() -> Self {
        DepartmentRegistry::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      : process_orchestrator.rs
//! 端到端编排:**一张工单依次走过全部已登记部门**。
//!
//! ★ 本文件是"模板方法"的驱动侧,它做的事只有三件:
//!   1. 按登记顺序取骨架执行器;
//!   2. 把工单拍成只读快照交给它;
//!   3. 拿回**变更**,交给聚合根落笔(聚合根是唯一写者)。
//!
//! ⚠️ 顺序来自**注册表**(装配层决定),不是硬编码在本文件里:
//! 于是"加一个部门"只改装配点,不改编排逻辑。

use std::sync::Arc;

use crate::common::template_method::SkeletonLedger;
use crate::common::template_method::StepOutcomeKind;
use crate::domain::DepartmentProcessRunner;
use crate::domain::DepartmentRunResult;
use crate::domain::DepartmentCode;
use crate::domain::ProductionOrder;
use crate::domain::RemoteGateway;

/// 一次骨架执行里**一步**的留痕。
///
/// ★ 为什么编排层要留逐步明细而不是只留汇总:
/// 「这个部门跑了 7 步」回答不了「停在第几步」。
/// 只有汇总时,"跑了 5 步"与"7 步里跳过 2 步"在报表上长得一样。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutcomeStep {
    /// 步骤编码。
    step_code: &'static str,
    /// 第几步。
    order_index: usize,
    /// 结果三态。
    outcome_kind: StepOutcomeKind,
    /// 说明。
    detail: String,
}

impl OutcomeStep {
    /// 步骤编码。
    pub const fn step_code(&self) -> &'static str {
        self.step_code
    }

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

    /// 结果三态。
    pub const fn outcome_kind(&self) -> StepOutcomeKind {
        self.outcome_kind
    }

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

/// 一次部门作业的产出(编排层关心的那部分)。
#[derive(Debug, Clone)]
pub struct RunOutcome {
    /// 工单号。
    order_number_text: String,
    /// 部门号。
    department_code: DepartmentCode,
    /// ★ 逐步留痕(**顺序即发生顺序**)。
    steps: Vec<OutcomeStep>,
    /// 跑通的步数。
    recorded_step_total: usize,
    /// 已执行步数。
    performed_total: usize,
    /// 已跳过度数。
    skipped_total: usize,
    /// 已失败步数。
    failed_total: usize,
    /// 是否被中止。
    aborted: bool,
    /// 是否成功。
    succeeded: bool,
    /// ★ 失败策略编码(**从结果里带出来**,不由报表猜)。
    failure_policy_code: &'static str,
    /// 金额变更(分)。
    money_delta_minor: i64,
    /// 金额变更是否合法。
    money_change_legal: bool,
    /// 结论文本。
    conclusion: String,
    /// 外部调用详情。
    external_detail: String,
    /// 是否真的找到了这个部门的执行器。
    runner_found: bool,
    /// ★ 是否"这个部门已经做过了"(重复作业被跳过)。
    ///
    /// 为什么单独一个字段:重复作业既不是成功也不是失败,
    /// 并入任何一边都会让"完成部门数"与"作业次数"对不上。
    duplicated: bool,
}

impl RunOutcome {
    /// 构造一个产出。
    pub fn new(
        order_number_text: String,
        department_code: DepartmentCode,
        steps: Vec<OutcomeStep>,
        recorded_step_total: usize,
        performed_total: usize,
        skipped_total: usize,
        failed_total: usize,
        aborted: bool,
        succeeded: bool,
        failure_policy_code: &'static str,
        money_delta_minor: i64,
        money_change_legal: bool,
        conclusion: String,
        external_detail: String,
        runner_found: bool,
        duplicated: bool,
    ) -> Self {
        RunOutcome {
            order_number_text,
            department_code,
            steps,
            recorded_step_total,
            performed_total,
            skipped_total,
            failed_total,
            aborted,
            succeeded,
            failure_policy_code,
            money_delta_minor,
            money_change_legal,
            conclusion,
            external_detail,
            runner_found,
            duplicated,
        }
    }

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

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

    /// 逐步留痕(顺序即发生顺序)。
    pub fn steps(&self) -> &[OutcomeStep] {
        self.steps.as_slice()
    }

    /// 跑通的步数。
    pub const fn recorded_step_total(&self) -> usize {
        self.recorded_step_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 aborted(&self) -> bool {
        self.aborted
    }

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

    /// 失败策略编码。
    pub const fn failure_policy_code(&self) -> &'static str {
        self.failure_policy_code
    }

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

    /// 金额变更是否合法。
    pub const fn money_change_legal(&self) -> bool {
        self.money_change_legal
    }

    /// 结论文本。
    pub fn conclusion(&self) -> &str {
        self.conclusion.as_str()
    }

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

    /// 是否找到了执行器(**没找到一定要看得见**)。
    pub const fn runner_found(&self) -> bool {
        self.runner_found
    }

    /// 是否重复作业被跳过。
    pub const fn duplicated(&self) -> bool {
        self.duplicated
    }
}

/// ★ 把一次骨架执行的结果转成编排层产出。
///
/// 为什么放在这里:`DepartmentRunResult` 是**领域**的形状,
/// `RunOutcome` 是编排层(报表消费)的形状,两者不该互相迁就。
pub fn outcome_from_result(order_number_text: &str, result: &DepartmentRunResult) -> RunOutcome {
    let mut steps: Vec<OutcomeStep> = Vec::new();
    for step in result.trace().steps().iter() {
        steps.push(OutcomeStep {
            step_code: step.step_code().code(),
            order_index: step.order_index(),
            outcome_kind: step.outcome().kind(),
            detail: String::from(step.outcome().text()),
        });
    }
    RunOutcome::new(
        String::from(order_number_text),
        result.department_code(),
        steps,
        result.trace().recorded_total(),
        result.trace().count_of(StepOutcomeKind::Performed),
        result.trace().count_of(StepOutcomeKind::Skipped),
        result.trace().count_of(StepOutcomeKind::Failed),
        result.trace().aborted(),
        result.succeeded(),
        result.trace().failure_policy().code(),
        result.mutation().money_delta_minor(),
        result.money_change_is_legal(),
        result.trace().conclusion_text(),
        String::from(result.external_detail()),
        true,
        false,
    )
}

/// 构造一个"这个部门做过了"的产出(**不跑骨架**)。
pub fn duplicated_outcome(order_number_text: &str, department_code: DepartmentCode) -> RunOutcome {
    RunOutcome::new(
        String::from(order_number_text),
        department_code,
        Vec::new(),
        0,
        0,
        0,
        0,
        false,
        true,
        "-",
        0,
        true,
        String::from("该部门已作业过,本次跳过(不重复计费)"),
        String::new(),
        true,
        true,
    )
}

/// 编排器:**无状态**(可跨线程共享)。
pub struct ProcessOrchestrator {
    /// 骨架台账(只增不减)。
    ledger: Arc<SkeletonLedger>,
}

impl ProcessOrchestrator {
    /// 建一个编排器。
    pub fn new(ledger: Arc<SkeletonLedger>) -> Self {
        ProcessOrchestrator { ledger }
    }

    /// 台账。
    pub fn ledger(&self) -> &Arc<SkeletonLedger> {
        &self.ledger
    }

    /// ★ 跑一个部门。
    ///
    /// ⚠️ 返回的是**产出**而不是直接改库:落笔由调用方(应用服务)做,
    /// 因为乐观并发要在"读 → 算 → 写"之外统一处理。
    /// ⚠️ 返回的是**领域的结果**(含变更)而不是直接改库:
    /// 落笔与乐观并发由应用服务统一处理,编排器不碰仓储。
    pub fn run_department(
        &self,
        order: &ProductionOrder,
        runner: &Arc<DepartmentProcessRunner>,
        gateway: Option<&dyn RemoteGateway>,
    ) -> DepartmentRunResult {
        let context = order.context();
        let result: DepartmentRunResult = runner.execute(&context, gateway);
        // ★ 台账只增不减,记在**跑完之后**(顺序:先跑 → 后记账)。
        self.ledger.record_trace(result.trace());
        result
    }
}
//!# 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      : scheduled_task.rs
//! 调度任务分派:**任务怎么分给 worker**。
//!
//! ★ 分派方式(轮转)与"结果长什么样"是两件事:
//! 轮转只决定"谁去做",结果一律按**任务下标**排序后才出报表,
//! 于是 worker 数从 1 变 8,输出一个字节都不变。

use crate::domain::DepartmentTask;

/// 一个已分派的任务。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduledTask {
    /// 任务下标(**排序键**)。
    task_index: usize,
    /// 分给第几个 worker。
    worker_index: usize,
}

impl ScheduledTask {
    /// 构造一个分派。
    pub const fn new(task_index: usize, worker_index: usize) -> Self {
        ScheduledTask {
            task_index,
            worker_index,
        }
    }

    /// 任务下标。
    pub const fn task_index(&self) -> usize {
        self.task_index
    }

    /// worker 下标。
    pub const fn worker_index(&self) -> usize {
        self.worker_index
    }

    /// ★ 轮转分派:第 i 个任务给第 `i % worker_total` 个 worker。
    ///
    /// 为什么是轮转而不是"按段切分":
    /// 按段切分时若某段任务特别重,其它 worker 早早空闲,
    /// 而**结果与谁做无关**,所以更均匀的轮转更好。
    pub fn assign_round_robin(tasks: &[DepartmentTask], worker_total: usize) -> Vec<ScheduledTask> {
        let mut out: Vec<ScheduledTask> = Vec::new();
        let divisor: usize = if worker_total == 0 { 1 } else { worker_total };
        for (index, _task) in tasks.iter().enumerate() {
            out.push(ScheduledTask::new(index, index % divisor));
        }
        out
    }

    /// 每个 worker 分到几条(报表要展示"分得匀不匀")。
    pub fn workload_histogram(assignments: &[ScheduledTask], worker_total: usize) -> Vec<usize> {
        let mut histogram: Vec<usize> = vec![0; worker_total];
        for item in assignments.iter() {
            if item.worker_index() < histogram.len() {
                histogram[item.worker_index()] += 1;
            }
        }
        histogram
    }
}
//!# 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      : scheduler.rs
//! 调度器:把「工单 × 部门」任务**并发**跑完。
//!
//! ★★ 高并发与"两次运行逐字节一致"为什么能同时成立:
//!
//! 1. 任务进队列前**先排序**(`DepartmentTask` 的 `Ord` 显式定义,
//!    优先级 → 部门 → 工单号三键,没有并列歧义);
//! 2. 各 worker 只做「取下一个下标 → 跑 → 把结果 push 进结果集」;
//! 3. **结果按下标排序后**才交给报表------线程调度只影响谁先抢到,不影响最终行序;
//! 4. 日志由分发器统一按序号排序落盘(见 `common::logging::dispatcher`)。
//!
//! 于是线程数从 1 改成 8,控制台与日志文件**一个字节都不变**。

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

use crate::domain::DepartmentTask;
use crate::application::ScheduledTask;

/// 一条调度结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerReportRow {
    /// worker 名。
    worker_name: String,
    /// 任务下标(**排序键**:与线程调度无关)。
    task_index: usize,
    /// 工单号。
    order_number_text: String,
    /// 部门编码。
    department_code: &'static str,
    /// 是否成功。
    succeeded: bool,
    /// 跑了几步。
    step_total: usize,
}

impl SchedulerReportRow {
    /// worker 名。
    pub fn worker_name(&self) -> &str {
        self.worker_name.as_str()
    }

    /// 任务下标。
    pub const fn task_index(&self) -> usize {
        self.task_index
    }

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

    /// 部门编码。
    pub const fn department_code(&self) -> &'static str {
        self.department_code
    }

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

    /// 跑了几步。
    pub const fn step_total(&self) -> usize {
        self.step_total
    }
}

/// 调度器。
pub struct Scheduler {
    /// worker 线程数。
    worker_total: usize,
}

impl Scheduler {
    /// 声明一个调度器。
    pub const fn new(worker_total: usize) -> Self {
        Scheduler { worker_total }
    }

    /// worker 数。
    pub const fn worker_total(&self) -> usize {
        self.worker_total
    }

    /// ★ 并发跑一批任务。
    ///
    /// `body` 是"跑一个任务"的动作,返回 `(是否成功, 跑了几步)`。
    /// ⚠️ `body` 必须是 `Send + Sync + 'static`:它会被**多个**线程同时调用,
    /// 于是它内部用到的仓储/日志必须自己保证线程安全(本工程都是 `Arc` + 锁)。
    pub fn run_parallel<F>(&self, tasks: &[DepartmentTask], body: F) -> Vec<SchedulerReportRow>
    where
        F: Fn(&DepartmentTask) -> (bool, usize) + Send + Sync + 'static,
    {
        let assignments: Vec<ScheduledTask> = ScheduledTask::assign_round_robin(tasks, self.worker_total);
        // ★ 先排序:行序与"谁先抢到"无关。
        let mut sorted: Vec<ScheduledTask> = assignments;
        sorted.sort_by_key(|item| item.task_index());

        let shared_tasks: Arc<Vec<DepartmentTask>> = Arc::new(tasks.to_vec());
        let shared_body: Arc<F> = Arc::new(body);
        let cursor: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
        let results: Arc<Mutex<Vec<SchedulerReportRow>>> = Arc::new(Mutex::new(Vec::new()));

        let mut handles: Vec<thread::JoinHandle<()>> = Vec::new();
        for worker_index in 0..self.worker_total {
            let worker_name: String = format!("worker-{}", worker_index);
            let tasks_clone: Arc<Vec<DepartmentTask>> = Arc::clone(&shared_tasks);
            let body_clone: Arc<F> = Arc::clone(&shared_body);
            let cursor_clone: Arc<AtomicUsize> = Arc::clone(&cursor);
            let results_clone: Arc<Mutex<Vec<SchedulerReportRow>>> = Arc::clone(&results);
            let name_for_thread: String = worker_name.clone();
            let name_for_row: String = worker_name;
            let handle: thread::JoinHandle<()> = thread::Builder::new()
                .name(name_for_thread)
                .spawn(move || loop {
                    let index: usize = cursor_clone.fetch_add(1, Ordering::Relaxed);
                    if index >= tasks_clone.len() {
                        break;
                    }
                    let task: &DepartmentTask = &tasks_clone[index];
                    let (succeeded, step_total): (bool, usize) = body_clone(task);
                    let row: SchedulerReportRow = SchedulerReportRow {
                        worker_name: name_for_row.clone(),
                        task_index: index,
                        order_number_text: String::from(task.order_number_text()),
                        department_code: task.department_code().code(),
                        succeeded,
                        step_total,
                    };
                    match results_clone.lock() {
                        Ok(mut guard) => guard.push(row),
                        Err(poisoned) => poisoned.into_inner().push(row),
                    }
                })
                .expect("创建 worker 线程失败");
            handles.push(handle);
        }

        for handle in handles {
            handle.join().expect("worker 线程 panic");
        }

        let mut rows: Vec<SchedulerReportRow> = match results.lock() {
            Ok(guard) => guard.clone(),
            Err(poisoned) => poisoned.into_inner().clone(),
        };
        // ★ 结果按下标排序:这才让"线程数变了输出不变"成立。
        rows.sort_by_key(|row| row.task_index());
        rows
    }
}
//!# 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_dispatch_service.rs
//! 支撑部门派发服务:一个部门作业**完成**之后要带出来的那些小事。
//!
//! ★ 为什么它要单独存在:
//! 「作业完成」之后要做的事(工时分摊、留痕归档、开票申请...)是**跟随部门的**,
//! 但**不属于**骨架七步------把它们塞进骨架会让骨架为每个部门长出不同的分支,
//! 那骨架就不再是骨架了。
//!
//! ⚠️ 派发表用 `match` 而不是散在各处:这样"这个部门带出几件事"是一张可读的表,
//! 而且**工程外新增的部门**自动落到默认那两件,不需要回来改本文件。

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

use crate::common::logging::AppLogger;
use crate::domain::DepartmentCode;

/// 支撑部门派发服务。
pub struct DepartmentDispatchService {
    /// 日志对象。
    logger: Arc<dyn AppLogger>,
    /// 累计派发条数(只增不减)。
    dispatch_total: AtomicUsize,
}

impl DepartmentDispatchService {
    /// 建一个服务。
    pub fn new(logger: Arc<dyn AppLogger>) -> Self {
        DepartmentDispatchService {
            logger,
            dispatch_total: AtomicUsize::new(0),
        }
    }

    /// ★ 某个部门完成后要带出哪些支撑任务。
    ///
    /// 默认那两件(工时分摊 / 留痕归档)适用于**任何**部门,
    /// 于是扩展区新增的部门无需改本文件就能参与统计。
    pub fn support_tasks_for(department_code: DepartmentCode) -> Vec<&'static str> {
        let mut tasks: Vec<&'static str> = vec!["工时分摊", "留痕归档"];
        if department_code == DepartmentCode::PROCUREMENT {
            tasks.push("采购单归档");
        } else if department_code == DepartmentCode::QUALITY {
            tasks.push("不合格品登记");
        } else if department_code == DepartmentCode::LOGISTICS {
            tasks.push("运单同步");
        } else if department_code == DepartmentCode::FINANCE {
            tasks.push("开票申请");
        }
        tasks
    }

    /// 派发:记日志并累计条数。
    pub fn dispatch(&self, order_number_text: &str, department_code: DepartmentCode) -> usize {
        let tasks: Vec<&'static str> = Self::support_tasks_for(department_code);
        for task in tasks.iter() {
            self.logger.info(
                format!(
                    "[{}] 部门 {} 派发支撑任务:{}",
                    order_number_text,
                    department_code.code(),
                    task
                )
                .as_str(),
            );
        }
        self.dispatch_total
            .fetch_add(tasks.len(), Ordering::Relaxed);
        tasks.len()
    }

    /// 累计派发条数。
    pub fn dispatch_total(&self) -> usize {
        self.dispatch_total.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      : process_application_service.rs
//! 工单应用服务:**用例的唯一入口**。
//!
//! ★ 本服务负责"读 → 算 → 写"这三步里的**并发语义**,
//! 业务规则在领域层,骨架在领域层,这里只做编排与落库。
//!
//! ★★ 两个用例分开(本工程刻意的设计):
//!   - `run_department`:**部门作业**(走骨架七步,改工序/成本/费用);
//!   - `collect_payment`:**收款**(不走骨架,不改任何部门作业状态)。
//!
//! 混在一起会让"已结算"与"已收齐"变成同一件事,而它们不是------
//! 客户可以先补款再结算,也可以结算时一次收齐。

use std::sync::Arc;

use crate::application::orchestration::duplicated_outcome;
use crate::application::orchestration::outcome_from_result;
use crate::application::orchestration::OutcomeStep;
use crate::application::orchestration::ProcessOrchestrator;
use crate::application::orchestration::RunOutcome;
use crate::application::DepartmentRegistry;
use crate::application::OrderRowDto;
use crate::common::logging::AppLogger;
use crate::domain::DepartmentCode;
use crate::domain::DepartmentProcessRunner;
use crate::domain::Money;
use crate::domain::OrderRepository;
use crate::domain::ProductionOrder;
use crate::domain::RemoteGateway;

/// 乐观并发的最大尝试次数。
const MAX_ATTEMPTS: u32 = 4;

/// 收款用例的结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PaymentResult {
    /// 工单号。
    order_number_text: String,
    /// 是否找到了工单。
    found: bool,
    /// 本次收款金额(分)。
    amount_minor: i64,
    /// 是否真的入账。
    applied: bool,
    /// 收款之后的未收(分)。
    outstanding_minor: i64,
    /// 说明。
    note: String,
    /// 操作人。
    actor: String,
}

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

    /// 是否找到了工单。
    pub const fn found(&self) -> bool {
        self.found
    }

    /// 本次收款金额(分)。
    pub const fn amount_minor(&self) -> i64 {
        self.amount_minor
    }

    /// 是否真的入账。
    pub const fn applied(&self) -> bool {
        self.applied
    }

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

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

    /// 操作人。
    pub fn actor(&self) -> &str {
        self.actor.as_str()
    }
}

/// 工单应用服务。
pub struct ProcessApplicationService {
    /// 仓储(端口,实现由组合根注入)。
    repository: Arc<dyn OrderRepository>,
    /// 编排器。
    orchestrator: Arc<ProcessOrchestrator>,
    /// 日志对象。
    logger: Arc<dyn AppLogger>,
}

impl ProcessApplicationService {
    /// 建一个服务。
    pub fn new(
        repository: Arc<dyn OrderRepository>,
        orchestrator: Arc<ProcessOrchestrator>,
        logger: Arc<dyn AppLogger>,
    ) -> Self {
        ProcessApplicationService {
            repository,
            orchestrator,
            logger,
        }
    }

    /// 仓储(只读,报表要展示"用的是哪个实现")。
    pub fn repository(&self) -> &Arc<dyn OrderRepository> {
        &self.repository
    }

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

    /// ★ 跑一个部门。
    pub fn run_department(
        &self,
        order_number_text: &str,
        runner: &Arc<DepartmentProcessRunner>,
        gateway: Option<&dyn RemoteGateway>,
    ) -> RunOutcome {
        let department_code: DepartmentCode = runner.department_code();
        let Some(order) = self.repository.find_by_number(order_number_text) else {
            self.logger.error(
                format!(
                    "工单 {} 不存在,部门 {} 无法作业",
                    order_number_text,
                    department_code.code()
                )
                .as_str(),
            );
            return not_found_outcome(order_number_text, department_code);
        };

        // ★ 重复作业:跳过并**说清楚**,绝不静默再跑一遍(那会把成本记两遍)。
        if order.is_department_completed(department_code) {
            self.logger.warn(
                format!(
                    "[{}] 部门 {} 已作业过,本次跳过(不重复计费)",
                    order_number_text,
                    department_code.code()
                )
                .as_str(),
            );
            return duplicated_outcome(order_number_text, department_code);
        }

        let mut attempt: u32 = 0;
        let mut last: RunOutcome = duplicated_outcome(order_number_text, department_code);
        while attempt < MAX_ATTEMPTS {
            attempt += 1;
            let current: ProductionOrder = match self.repository.find_by_number(order_number_text) {
                Some(found) => found,
                None => return not_found_outcome(order_number_text, department_code),
            };
            let expected_count: usize = current.completed_department_total();
            let result = self
                .orchestrator
                .run_department(&current, runner, gateway);

            let mut updated: ProductionOrder = current.clone();
            updated.apply_mutation(result.mutation());
            if result.succeeded() {
                updated.record_department_completion(department_code);
            }

            // ★ 乐观并发:判断与写入在同一把写锁内(仓储实现负责)。
            if !self
                .repository
                .save_expecting_completed_count(expected_count, updated)
            {
                self.logger.warn(
                    format!(
                        "[{}] 部门 {} 第 {} 次写入被并发挡下(重读再算)",
                        order_number_text,
                        department_code.code(),
                        attempt
                    )
                    .as_str(),
                );
                last = outcome_from_result(order_number_text, &result);
                continue;
            }

            let outcome: RunOutcome = outcome_from_result(order_number_text, &result);
            if outcome.succeeded() {
                self.logger.info(
                    format!(
                        "[{}] 部门 {} 作业成功(第 {} 次尝试):{}",
                        order_number_text,
                        department_code.code(),
                        attempt,
                        outcome.conclusion()
                    )
                    .as_str(),
                );
            } else {
                self.logger.warn(
                    format!(
                        "[{}] 部门 {} 作业未通过(第 {} 次尝试):{}",
                        order_number_text,
                        department_code.code(),
                        attempt,
                        outcome.conclusion()
                    )
                    .as_str(),
                );
            }
            return outcome;
        }

        self.logger.error(
            format!(
                "[{}] 部门 {} 连续 {} 次写入都被并发挡下,放弃本次作业",
                order_number_text,
                department_code.code(),
                MAX_ATTEMPTS
            )
            .as_str(),
        );
        last
    }

    /// ★ 端到端:按注册表顺序跑完全部部门。
    ///
    /// `stop_on_failure` 为 `true` 时,某个部门跑不通就停(真实业务里"不合格就退回")。
    pub fn run_end_to_end(
        &self,
        order_number_text: &str,
        registry: &DepartmentRegistry,
        gateway: Option<&dyn RemoteGateway>,
        stop_on_failure: bool,
    ) -> Vec<RunOutcome> {
        let mut outcomes: Vec<RunOutcome> = Vec::new();
        for runner in registry.runners().iter() {
            let outcome: RunOutcome = self.run_department(order_number_text, runner, gateway);
            let succeeded: bool = outcome.succeeded();
            outcomes.push(outcome);
            if !succeeded && stop_on_failure {
                break;
            }
        }
        outcomes
    }

    /// ★ 收款用例:**不走骨架**。
    pub fn collect_payment(
        &self,
        order_number_text: &str,
        amount: Money,
        actor: &str,
    ) -> PaymentResult {
        let Some(mut order) = self.repository.find_by_number(order_number_text) else {
            self.logger
                .error(format!("收款失败:工单 {} 不存在", order_number_text).as_str());
            return PaymentResult {
                order_number_text: String::from(order_number_text),
                found: false,
                amount_minor: amount.minor(),
                applied: false,
                outstanding_minor: 0,
                note: String::from("工单不存在"),
                actor: String::from(actor),
            };
        };

        let expected_count: usize = order.completed_department_total();
        order.receive_payment(amount);
        let outstanding: i64 = order.outstanding_amount().minor();
        if !self
            .repository
            .save_expecting_completed_count(expected_count, order)
        {
            self.logger.warn(
                format!("[{}] 收款写入被并发挡下,重读再收", order_number_text).as_str(),
            );
            let mut retried: ProductionOrder = match self.repository.find_by_number(order_number_text)
            {
                Some(found) => found,
                None => {
                    return PaymentResult {
                        order_number_text: String::from(order_number_text),
                        found: false,
                        amount_minor: amount.minor(),
                        applied: false,
                        outstanding_minor: 0,
                        note: String::from("重读时工单已不存在"),
                        actor: String::from(actor),
                    }
                }
            };
            let second_expected: usize = retried.completed_department_total();
            retried.receive_payment(amount);
            let applied: bool = self
                .repository
                .save_expecting_completed_count(second_expected, retried);
            return PaymentResult {
                order_number_text: String::from(order_number_text),
                found: true,
                amount_minor: amount.minor(),
                applied,
                outstanding_minor: outstanding,
                note: String::from("并发重试一次后写入"),
                actor: String::from(actor),
            };
        }

        self.logger.info(
            format!(
                "[{}] 收款 {} 分已入账,未收 {} 分",
                order_number_text,
                amount.minor(),
                outstanding
            )
            .as_str(),
        );
        PaymentResult {
            order_number_text: String::from(order_number_text),
            found: true,
            amount_minor: amount.minor(),
            applied: true,
            outstanding_minor: outstanding,
            note: String::from("已入账"),
            actor: String::from(actor),
        }
    }

    /// 工单概览行(**排序后**返回:两次运行的报表行序必须一致)。
    pub fn order_rows(&self) -> Vec<OrderRowDto> {
        let mut orders: Vec<ProductionOrder> = self.repository.all();
        orders.sort_by(|left, right| left.order_number_text().cmp(right.order_number_text()));
        let mut rows: Vec<OrderRowDto> = Vec::new();
        for order in orders.iter() {
            rows.push(OrderRowDto::new(
                String::from(order.order_number_text()),
                String::from(order.product_name()),
                order.currency().symbol(),
                order.quoted_amount().minor(),
                order.received_amount().minor(),
                order.outstanding_amount().minor(),
                order.material_cost().minor(),
                order.labor_cost().minor(),
                order.expense_amount().minor(),
                order.cost_total().minor(),
                order.profit_amount().minor(),
                order.margin_basis_points(),
                order.completed_department_total(),
                order.work_minutes().minutes(),
                order.evidences().len(),
            ));
        }
        rows
    }
}

/// 构造一个"工单不存在 / 执行器没找到"的产出(**必须看得见**)。
fn not_found_outcome(order_number_text: &str, department_code: DepartmentCode) -> RunOutcome {
    RunOutcome::new(
        String::from(order_number_text),
        department_code,
        Vec::<OutcomeStep>::new(),
        0,
        0,
        0,
        0,
        false,
        false,
        "-",
        0,
        true,
        String::from("未执行:工单不存在"),
        String::new(),
        false,
        false,
    )
}

调用:

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      : main.rs
//! 模板方法模式(Rust 版)· 可运行示例。
//!
//! ★ 本文件是**组合根**:它负责把日志、仓储、夹具、编排器、部门实现装配起来,
//! 然后按幕展示这个工程真正想证明的四件事:
//!
//! 1. **骨架不可覆盖**:骨架是 `DepartmentProcessRunner::execute`(inherent 方法),
//!    不是 trait 的默认方法------后者会被实现类整个换掉(第四幕用反例对照)。
//! 2. **部门只填三个空**:`check_entry` / `perform` / `inspect`,外加四个可选钩子。
//! 3. **每一步都有物证**:七步三态(已执行 / 已跳过 / 已失败)全部进轨迹,
//!    自检规则对轨迹逐条核对,坏样本必须让规则变红(第五幕)。
//! 4. **数字可复算**:金额只用整数分,舍入口径只有一处(`common::numeric`)。
//!
//! 运行:
//! ```text
//! cargo run
//! ```
//!
//! 输出同时写到控制台与 `Logs/2026-07-06/app-*.log`,两边共用同一个格式化器。

// 本文件是演示用的二进制入口:各层统一出口会把公开 API 全部重导出,
// 而演示只走其中一条主路径。为了不让「未使用导入 / 未走到的扩展点」
// 淹没真正的编译错误,这里只抑制这两类噪音。
#![allow(dead_code)]
#![allow(unused_imports)]

mod application;
mod common;
mod domain;
mod infrastructure;
mod presentation;

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

use application::{
    DepartmentCatalog, DepartmentDispatchService, DepartmentRegistry, OrderRowDto,
    ProcessApplicationService, ProcessOrchestrator, RunOutcome, SkeletonLedgerDto, StepCatalog,
    SystemProfileDto,
};
use common::clock::FixedClock;
use common::logging::{AppLogger, LogLevel, LoggingDispatcher};
use common::numeric::{divide_rounded, divide_toward_zero};
use common::resilience::{CircuitPolicy, RetryPolicy};
use common::template_method::{
    audit_skeleton_trace, SkeletonLedger, SkeletonTrace, SkeletonProbe,
};
use domain::{
    builtin_departments, DepartmentCode, DepartmentProcessRunner, MaterialProcurementDepartment,
    OrderRepository, RemoteGateway, SKELETON_STEP_SEQUENCE,
};
use infrastructure::{
    seed_orders, shared_repository, ConsoleLogSink, FaultMode, FaultPlan, FileLogSink,
    LogDirectoryLayout, LoggerFactory, OrderServiceCategory, ReportCategory, ServiceContainer,
    SimulatedGateway,
};
use presentation::{
    bool_text, money_text, percent_text, render_table, section_header, ColumnSpec, ReportConsole,
    TableSpec,
};

/// 演示用的主工单:夹具里的第一张单(人民币、定金 40%、成色全部合格)。
const DEMO_ORDER: &str = "PO-2026-0001";
/// 夹具日期(日志目录按它分文件夹,固定值保证两次运行一致)。
const DEMO_DATE: &str = "2026-07-06";

fn main() {
    // ------------------------------------------------------------------
    // 组合根 1/3:日志基础设施(一个分发器 + 两个落地)
    // ------------------------------------------------------------------
    let dispatcher: Arc<LoggingDispatcher> = Arc::new(LoggingDispatcher::new(Arc::new(
        FixedClock::default_clock(),
    )));

    let console_sink: ConsoleLogSink = ConsoleLogSink::new();
    let console_lines = console_sink.shared_handle();
    // 控制台落地:与文件落地共用 `common::logging::format_line`。
    dispatcher.register_sink(Box::new(console_sink));

    let file_sink: FileLogSink = FileLogSink::new(LogDirectoryLayout::default_layout());
    let file_statistics = file_sink.statistics_handle();
    // 文件落地:按(日期,等级)分流到 Logs/<日期>/app-*.log。
    dispatcher.register_sink(Box::new(file_sink));

    // ------------------------------------------------------------------
    // 组合根 2/3:仓储、夹具、编排器、支撑服务、DI 容器
    // ------------------------------------------------------------------
    let container: ServiceContainer = assemble(&dispatcher);

    // ------------------------------------------------------------------
    // 组合根 3/3:用例入口与报表通道
    // ------------------------------------------------------------------
    let service: Arc<ProcessApplicationService> =
        container.order_service::<OrderServiceCategory>();
    let report_logger: Arc<dyn AppLogger> = container.logger_as::<ReportCategory>();
    let mut report: ReportConsole = ReportConsole::new(report_logger);

    let registry: DepartmentRegistry = build_registry();
    let gateway: SimulatedGateway = build_gateway();

    // ==================================================================
    // 第一幕:骨架长什么样(七步,部门只填三步)
    // ==================================================================
    act_one_skeleton(&mut report, &dispatcher, &registry);

    // ==================================================================
    // 第二幕:一张工单走完十二个部门(骨架试跑 + 端到端落库)
    // ==================================================================
    let (first_outcomes, real_trace) = act_two_single_order(
        &mut report,
        &dispatcher,
        &container,
        &service,
        &registry,
        &gateway,
    );

    // ==================================================================
    // 第三幕:十张工单的端到端结果(失败也是结果,不许静默跳过)
    // ==================================================================
    act_three_all_orders(
        &mut report,
        &dispatcher,
        &container,
        &service,
        &registry,
        &gateway,
        first_outcomes.as_slice(),
    );

    // ==================================================================
    // 第四幕:反例------把骨架写成 trait 默认方法,它会被覆盖
    // ==================================================================
    act_four_counterexamples(&mut report, &dispatcher, &container);

    // ==================================================================
    // 第五幕:机制自检------四条规则 + 四个必须变红的坏样本
    // ==================================================================
    act_five_audit(&mut report, &dispatcher, &real_trace);

    // ==================================================================
    // 第六幕:系统画像------日志、台账、注册清单
    // ==================================================================
    let console_total: usize = count_console_lines(&console_lines);
    let file_total: usize = count_file_lines(&file_statistics);
    act_six_system_profile(
        &mut report,
        &dispatcher,
        &container,
        &registry,
        console_total,
        file_total,
    );

    // 收尾:先 flush(让最后几行真正落到文件),再 close。
    let _ = dispatcher.shutdown();
}

// ======================================================================
// 装配
// ======================================================================

/// 装配容器:仓储 + 夹具 + 编排器 + 支撑服务。
fn assemble(dispatcher: &Arc<LoggingDispatcher>) -> ServiceContainer {
    // 端口:业务层只认 `Arc<dyn OrderRepository>`,不认具体实现。
    let repository = shared_repository();
    for order in seed_orders() {
        repository.save(order);
    }

    // 骨架台账:只增不减,跨多次执行累计。
    let ledger: Arc<SkeletonLedger> = Arc::new(SkeletonLedger::new());
    let orchestrator: Arc<ProcessOrchestrator> = Arc::new(ProcessOrchestrator::new(ledger));

    // 日志对象只由工厂生产;容器自己不 new。
    let logger_factory: LoggerFactory = LoggerFactory::new(Arc::clone(dispatcher));
    let dispatch_logger: Arc<dyn AppLogger> =
        Arc::new(logger_factory.create::<ReportCategory>());
    let dispatch: Arc<DepartmentDispatchService> =
        Arc::new(DepartmentDispatchService::new(dispatch_logger));

    ServiceContainer::new(
        repository,
        orchestrator,
        dispatch,
        Arc::clone(dispatcher),
        RetryPolicy::default_policy(),
        CircuitPolicy::default_policy(),
    )
}

/// 注册十二个内置部门:骨架由注册表统一套上,部门自己碰不到骨架。
fn build_registry() -> DepartmentRegistry {
    let mut registry = DepartmentRegistry::new();
    for steps in builtin_departments() {
        registry.register(steps);
    }
    registry
}

/// 模拟外部系统:只支持三个真实会被调到的操作。
///
/// ★ 为什么不用随机故障:本工程要求两次运行逐字节一致,
/// 故障剧本必须是确定性的(`FaultMode::Never` 表示这次演示不注入故障)。
fn build_gateway() -> SimulatedGateway {
    SimulatedGateway::new(
        "珠宝产业协同平台",
        vec!["TRACE_SHIPMENT", "RECONCILE", "OPEN_ACCOUNT"],
        "已回执",
        "接口超时",
        FaultPlan::new(FaultMode::Never),
    )
}

// ======================================================================
// 第一幕:骨架与分工
// ======================================================================

fn act_one_skeleton(
    report: &mut ReportConsole,
    dispatcher: &LoggingDispatcher,
    registry: &DepartmentRegistry,
) {
    let step_catalog: StepCatalog = StepCatalog::builtin();
    let mut lines: Vec<String> = section_header(
        1,
        "骨架:七步固定;Rust 用 inherent 方法把「不可覆盖」写进语言",
    );

    lines.push(String::from(
        "骨架位置:department::services::process_runner::DepartmentProcessRunner::execute",
    ));
    lines.push(format!(
        "声明序列:{}",
        SKELETON_STEP_SEQUENCE
            .iter()
            .map(|code| code.code())
            .collect::<Vec<&str>>()
            .join(" → ")
    ));
    lines.push(format!(
        "步骤字典与声明序列一致:{}(字典 {} 条 / 声明 {} 条)",
        bool_text(step_catalog.covers_declared_sequence()),
        step_catalog.entry_total(),
        SKELETON_STEP_SEQUENCE.len()
    ));
    lines.push(format!(
        "已注册部门执行器:{} 个(每个执行器 = 一个部门实现 + 同一份骨架)",
        registry.runner_total()
    ));

    let mut table: TableSpec = TableSpec::new(vec![
        ColumnSpec::left("步骤", 10),
        ColumnSpec::left("中文名", 8),
        ColumnSpec::center("归属", 6),
        ColumnSpec::left("说明", 54),
        ColumnSpec::center("开放给部门", 10),
    ]);
    for entry in step_catalog.entries() {
        let openness: &str = if entry.owner() == "部门" {
            "必须实现"
        } else if entry.owner() == "钩子" {
            "可覆写"
        } else {
            "不可改"
        };
        table.push_row(vec![
            String::from(entry.code().code()),
            String::from(entry.name()),
            String::from(entry.owner()),
            String::from(entry.note()),
            String::from(openness),
        ]);
    }
    table.push_note(format!(
        "部门只实现 {} 步(准入 / 作业 / 自检);领单、留痕、移交由骨架自己做;外呼由钩子决定做不做。",
        step_catalog.department_owned_total()
    ));
    table.push_note(String::from(
        "外呼不启用时记「已跳过」而不是从序列里消失------否则「没调外部系统」与「调了但失败」在报表上长得一样。",
    ));
    lines.append(&mut render_table(&table));

    emit(report, dispatcher, lines);
}

// ======================================================================
// 第二幕:单张工单的完整轨迹
// ======================================================================

fn act_two_single_order(
    report: &mut ReportConsole,
    dispatcher: &LoggingDispatcher,
    container: &ServiceContainer,
    service: &ProcessApplicationService,
    registry: &DepartmentRegistry,
    gateway: &SimulatedGateway,
) -> (Vec<RunOutcome>, SkeletonTrace) {
    let repository = Arc::clone(container.repository());
    let initial_order = repository
        .find_by_number(DEMO_ORDER)
        .expect("夹具里应当存在演示工单 PO-2026-0001");
    let initial_context = initial_order.context();
    let symbol: &str = initial_order.currency().symbol();

    let mut lines: Vec<String> = section_header(
        2,
        "单张工单:先看骨架试跑的七步,再让十二个部门依次作业",
    );
    lines.push(format!(
        "工单 {}|{}|报价 {}|定金 {}({} 万分点)|批次 {} 个",
        initial_order.order_number_text(),
        initial_order.product_name(),
        money_text(initial_order.quoted_amount().minor(), symbol),
        money_text(initial_order.deposit_amount().minor(), symbol),
        initial_order.deposit_ratio_basis_points(),
        initial_order.lot_total()
    ));

    // ------ 2.1 骨架试跑(不落库):只为了把七步轨迹完整展示出来 ------
    let runner: DepartmentProcessRunner =
        DepartmentProcessRunner::new(Arc::new(MaterialProcurementDepartment));
    let dry_run = runner.execute(&initial_context, Some(gateway));
    let trace: SkeletonTrace = dry_run.trace().clone();
    lines.push(format!(
        "骨架试跑(不落库):{}|{} 步|{}",
        runner.department_code().code(),
        dry_run.trace().recorded_total(),
        dry_run.trace().conclusion_text()
    ));

    let step_catalog: StepCatalog = StepCatalog::builtin();
    let mut step_table: TableSpec = TableSpec::new(vec![
        ColumnSpec::right("步序", 4),
        ColumnSpec::left("步骤", 10),
        ColumnSpec::center("归属", 6),
        ColumnSpec::center("三态", 8),
        ColumnSpec::left("说明", 96),
    ]);
    for step in trace.steps().iter() {
        step_table.push_row(vec![
            format!("{}", step.order_index()),
            String::from(step.step_code().code()),
            step_catalog.display_owner_of(step.step_code()),
            String::from(step.outcome().kind().chinese_label()),
            String::from(step.outcome().text()),
        ]);
    }
    step_table.push_note(String::from(
        "第 4 步(EXTERNAL)在采购部门不启用,所以是「已跳过」;序列仍是七步。",
    ));
    lines.append(&mut render_table(&step_table));

    // 先把「骨架长什么样」打出来,再让部门作业日志跟在它后面。
    emit(report, dispatcher, lines);
    let lines: Vec<String> = vec![String::from("十二个部门依次作业(真实落库):")];
    emit(report, dispatcher, lines);
    let mut lines: Vec<String> = Vec::new();

    // ------ 2.2 端到端:十二个部门依次作业(真实落库) ------
    let outcomes: Vec<RunOutcome> = run_end_to_end(
        service,
        registry,
        DEMO_ORDER,
        gateway,
        &repository,
        container.dispatch(),
        true,
        true,
    );

    let catalog: DepartmentCatalog = DepartmentCatalog::builtin();
    let mut summary: TableSpec = TableSpec::new(vec![
        ColumnSpec::left("部门", 18),
        ColumnSpec::right("声明", 6),
        ColumnSpec::right("实跑", 6),
        ColumnSpec::right("执行", 6),
        ColumnSpec::right("跳过", 6),
        ColumnSpec::right("失败", 6),
        ColumnSpec::right("金额变更", 14),
        ColumnSpec::center("合法", 6),
        ColumnSpec::left("结论", 48),
    ]);
    for outcome in outcomes.iter() {
        summary.push_row(vec![
            catalog.display_name_of(outcome.department_code()),
            format!("{}", SKELETON_STEP_SEQUENCE.len()),
            format!("{}", outcome.recorded_step_total()),
            format!("{}", outcome.performed_total()),
            format!("{}", outcome.skipped_total()),
            format!("{}", outcome.failed_total()),
            money_text(outcome.money_delta_minor(), symbol),
            String::from(bool_text(outcome.money_change_legal())),
            String::from(outcome.conclusion()),
        ]);
    }
    summary.push_note(String::from(
        "「金额变更」只有核心作业(PERFORM)会产生;准入与自检改了金额会被 money_change_is_legal 立刻判红。",
    ));
    summary.push_note(String::from(
        "物流完成后模拟客户签收(这是领域外的动作),财务才具备结算前提------签收不是部门作业。",
    ));
    lines.append(&mut render_table(&summary));

    let final_order = repository
        .find_by_number(DEMO_ORDER)
        .expect("端到端跑完后工单应当仍在仓储里");
    lines.push(format!(
        "本单收尾:完成 {}/{} 部门|成本合计 {}|已收 {}|毛利 {}|毛利率 {}|留痕 {} 条",
        final_order.completed_department_total(),
        registry.runner_total(),
        money_text(final_order.cost_total().minor(), symbol),
        money_text(final_order.received_amount().minor(), symbol),
        money_text(final_order.profit_amount().minor(), symbol),
        percent_text(final_order.margin_basis_points()),
        final_order.evidences().len()
    ));

    emit(report, dispatcher, lines);
    (outcomes, trace)
}

// ======================================================================
// 第三幕:十张工单的端到端结果
// ======================================================================

fn act_three_all_orders(
    report: &mut ReportConsole,
    dispatcher: &LoggingDispatcher,
    container: &ServiceContainer,
    service: &ProcessApplicationService,
    registry: &DepartmentRegistry,
    gateway: &SimulatedGateway,
    first_outcomes: &[RunOutcome],
) {
    let repository = Arc::clone(container.repository());
    let seeded_total: usize = seed_orders().len();

    let mut intro: Vec<String> = section_header(
        3,
        "十张工单:端到端结果(跑不通的订单停在哪一步,也算结果)",
    );
    intro.push(format!(
        "第二幕已完成 1 张;本幕再跑 {} 张,共 {} 张。失败订单不会被静默跳过。",
        seeded_total.saturating_sub(1),
        seeded_total
    ));
    intro.push(format!(
        "第二幕工单 {} 的部门数复核:{} 个部门({})",
        DEMO_ORDER,
        first_outcomes.len(),
        if first_outcomes.iter().all(|outcome| outcome.succeeded()) {
            "全部通过"
        } else {
            "存在未通过部门"
        }
    ));
    emit(report, dispatcher, intro);

    for order in seed_orders() {
        let order_number_text = String::from(order.order_number_text());
        if order_number_text == DEMO_ORDER {
            // 第二幕已经跑过,直接复用结果(重复作业会被应用服务挡下)。
            continue;
        }
        let _ = run_end_to_end(
            service,
            registry,
            order_number_text.as_str(),
            gateway,
            &repository,
            container.dispatch(),
            true,
            false,
        );
    }

    let rows: Vec<OrderRowDto> = service.order_rows();
    let mut lines: Vec<String> = vec![String::from("十张工单汇总(按工单号排序):")];

    let mut table: TableSpec = TableSpec::new(vec![
        ColumnSpec::left("工单号", 13),
        ColumnSpec::left("作品名", 16),
        ColumnSpec::center("币种", 4),
        ColumnSpec::right("报价", 13),
        ColumnSpec::right("已收", 12),
        ColumnSpec::right("尾款", 12),
        ColumnSpec::right("成本合计", 12),
        ColumnSpec::right("毛利", 13),
        ColumnSpec::right("毛利率", 8),
        ColumnSpec::center("完成部门", 8),
        ColumnSpec::right("工时", 8),
    ]);
    for row in rows.iter() {
        table.push_row(vec![
            String::from(row.order_number_text()),
            String::from(row.product_name()),
            String::from(row.currency_symbol()),
            money_text(row.quoted_minor(), row.currency_symbol()),
            money_text(row.received_minor(), row.currency_symbol()),
            money_text(row.outstanding_minor(), row.currency_symbol()),
            money_text(row.cost_total_minor(), row.currency_symbol()),
            money_text(row.profit_minor(), row.currency_symbol()),
            percent_text(row.margin_basis_points()),
            format!(
                "{}/{}",
                row.completed_department_total(),
                registry.runner_total()
            ),
            format!("{}", row.work_minutes()),
        ]);
    }
    table.push_note(String::from(
        "「尾款 = 报价 − 已收」「毛利 = 已收 − 成本」「毛利率 = 毛利 / 报价」是三条恒等式,报表不重新定义口径。",
    ));
    table.push_note(String::from(
        "港币单不并入人民币合计:跨币种求和会得到一个谁都对不上的数,所以两组合计分开列。",
    ));
    lines.append(&mut render_table(&table));

    // ------ 完成度分布 ------
    let full_success: usize = rows
        .iter()
        .filter(|row| row.completed_department_total() == registry.runner_total())
        .count();
    let partial: usize = rows.len() - full_success;
    lines.push(format!(
        "完成度:全流程 {} 张;未跑完 {} 张(中止位置由各单的准入/自检决定)。",
        full_success, partial
    ));

    // ------ 分币种合计(只在同币种内求和) ------
    let mut cny_quoted: i128 = 0;
    let mut cny_cost: i128 = 0;
    let mut cny_profit: i128 = 0;
    let mut cny_total: usize = 0;
    let mut hkd_quoted: i128 = 0;
    let mut hkd_cost: i128 = 0;
    let mut hkd_profit: i128 = 0;
    let mut hkd_total: usize = 0;
    for row in rows.iter() {
        if row.currency_symbol() == "¥" {
            cny_quoted += i128::from(row.quoted_minor());
            cny_cost += i128::from(row.cost_total_minor());
            cny_profit += i128::from(row.profit_minor());
            cny_total += 1;
        } else if row.currency_symbol() == "HK$" {
            hkd_quoted += i128::from(row.quoted_minor());
            hkd_cost += i128::from(row.cost_total_minor());
            hkd_profit += i128::from(row.profit_minor());
            hkd_total += 1;
        }
    }
    lines.push(format!(
        "人民币 {} 张:报价合计 {}|成本合计 {}|毛利合计 {}(程序内累加,单位分)",
        cny_total,
        money_text(cny_quoted as i64, "¥"),
        money_text(cny_cost as i64, "¥"),
        money_text(cny_profit as i64, "¥")
    ));
    lines.push(format!(
        "港币 {} 张:报价合计 {}|成本合计 {}|毛利合计 {}(不并入上一行)",
        hkd_total,
        money_text(hkd_quoted as i64, "HK$"),
        money_text(hkd_cost as i64, "HK$"),
        money_text(hkd_profit as i64, "HK$")
    ));

    // ------ 舍入口径分叉:用夹具里除不尽的那笔定金证明「口径不是装饰」 ------
    let quoted: i128 = 7_777_775;
    let bps: i128 = 3_000;
    let half_up: i64 = divide_rounded(quoted * bps, 10_000);
    let truncated: i64 = divide_toward_zero(quoted * bps, 10_000);
    lines.push(format!(
        "舍入口径对照(PO-2026-0005 的 30% 定金):远离零四舍五入 = {}|向零截断 = {}|差额 = {} 分",
        money_text(half_up, "¥"),
        money_text(truncated, "¥"),
        half_up - truncated
    ));
    lines.push(String::from(
        "这一分钱的分叉不崩、不告警,看起来完全合理------所以正式口径只能有 divide_rounded 一处。",
    ));

    emit(report, dispatcher, lines);
}

// ======================================================================
// 第四幕:反例对照
// ======================================================================

fn act_four_counterexamples(
    report: &mut ReportConsole,
    dispatcher: &LoggingDispatcher,
    container: &ServiceContainer,
) {
    let repository = Arc::clone(container.repository());
    let order = repository
        .find_by_number(DEMO_ORDER)
        .expect("演示工单应当存在");
    let context = order.context();

    let (obedient_steps, rogue_steps) = domain::demonstrate_override(&context);
    let missing: Vec<&'static str> = domain::missing_steps_of_override(&context);
    let department_total: usize = domain::builtin_departments().len();
    let copy_cost_for_departments = domain::SkeletonCopyCost::new(
        department_total,
        SKELETON_STEP_SEQUENCE.len(),
    );

    let mut lines: Vec<String> = section_header(
        4,
        "反例:trait 默认方法能被覆盖;inherent 方法不能------这是语言层保证",
    );
    lines.push(String::from(
        "下面两个实现都能编译通过。区别不在「谁更规范」,而在「骨架放在哪里」。",
    ));

    let mut table: TableSpec = TableSpec::new(vec![
        ColumnSpec::left("写法", 20),
        ColumnSpec::left("骨架在哪", 28),
        ColumnSpec::left("能否被覆盖", 32),
        ColumnSpec::right("实跑步数", 10),
        ColumnSpec::left("漏掉的步骤", 40),
    ]);
    table.push_row(vec![
        String::from("trait 默认方法"),
        String::from("trait 的 run 默认方法"),
        String::from("能(实现类照抄签名即可覆盖)"),
        format!("{}", rogue_steps),
        if missing.is_empty() {
            String::from("无")
        } else {
            missing.join("、")
        },
    ]);
    table.push_row(vec![
        String::from("inherent 方法"),
        String::from("结构体 inherent 方法"),
        String::from("不能(语言层无覆盖入口)"),
        format!("{}", obedient_steps),
        String::from("无"),
    ]);
    table.push_note(format!(
        "守规矩的实现跑了 {} 步;覆盖骨架的实现只跑了 {} 步,漏掉 {} 步。",
        obedient_steps,
        rogue_steps,
        missing.len()
    ));
    table.push_note(String::from(
        "「覆盖版少跑几步」在报表上只是一个数字差;没有任何地方会说「这个部门的骨架被改过」------这正是把骨架放进 trait 默认方法的代价。",
    ));
    lines.append(&mut render_table(&table));

    lines.push(String::from(
        "为什么类型系统挡不住覆盖:trait 默认方法只是「默认行为」,实现类可以照抄签名整个替换;",
    ));
    lines.push(String::from(
        "而结构体的 inherent 方法没有可以被 trait 实现覆盖的入口------这才是「骨架不可变」的语言保证。",
    ));
    lines.push(format!(
        "复制骨架的代价:{}",
        copy_cost_for_departments.conclusion_text()
    ));
    lines.push(format!(
        "(口径:{} 个部门 × {} 步;复制版写 {} 处,模板方法版写 1 处。)",
        department_total,
        SKELETON_STEP_SEQUENCE.len(),
        department_total * SKELETON_STEP_SEQUENCE.len()
    ));

    emit(report, dispatcher, lines);
}

// ======================================================================
// 第五幕:机制自检
// ======================================================================

fn act_five_audit(
    report: &mut ReportConsole,
    dispatcher: &LoggingDispatcher,
    trace: &SkeletonTrace,
) {
    let audit = audit_skeleton_trace(trace);
    let all_passed: bool = audit.iter().all(|line| line.passed());
    let probe: SkeletonProbe = SkeletonProbe::run();

    let mut lines: Vec<String> = section_header(
        5,
        "机制自检:四条规则必须能抓住真实的坏轨迹,而不是只对正常轨迹点头",
    );
    lines.push(format!(
        "真实轨迹自检:{} 条规则,全部通过 = {}",
        audit.len(),
        bool_text(all_passed)
    ));

    let mut table: TableSpec = TableSpec::new(vec![
        ColumnSpec::left("规则", 10),
        ColumnSpec::left("标题", 40),
        ColumnSpec::center("结果", 8),
        ColumnSpec::left("实测", 62),
    ]);
    for line in audit.iter() {
        table.push_row(vec![
            String::from(line.rule_code()),
            String::from(line.title()),
            String::from(if line.passed() { "通过" } else { "失败" }),
            String::from(line.detail()),
        ]);
    }
    table.push_note(String::from(
        "规则不是注释:A1 管顺序、A2 管中止有据、A3 管序号连续、A4 管失败策略兑现。",
    ));
    lines.append(&mut render_table(&table));

    lines.push(format!(
        "负向探针:{} 个坏样本,预期变红 {} 条,实际变红 {} 条,全部变红 = {}",
        probe.lines().len(),
        probe.lines().len(),
        probe.red_total(),
        bool_text(probe.all_turned_red())
    ));
    let mut probe_table: TableSpec = TableSpec::new(vec![
        ColumnSpec::left("坏样本", 28),
        ColumnSpec::left("预期变红的规则", 16),
        ColumnSpec::center("实际变红", 10),
    ]);
    for line in probe.lines().iter() {
        probe_table.push_row(vec![
            String::from(line.sample_name()),
            String::from(line.expected_rule()),
            String::from(if line.turned_red() { "是" } else { "否" }),
        ]);
    }
    probe_table.push_note(String::from(
        "四个样本全部变红,才说明这四条规则真的在检查东西;全绿的检查是恒真的,等于没有。",
    ));
    lines.append(&mut render_table(&probe_table));

    emit(report, dispatcher, lines);
}

// ======================================================================
// 第六幕:系统画像
// ======================================================================

fn act_six_system_profile(
    report: &mut ReportConsole,
    dispatcher: &LoggingDispatcher,
    container: &ServiceContainer,
    registry: &DepartmentRegistry,
    console_total: usize,
    file_total: usize,
) {
    let ledger = container.orchestrator().ledger().snapshot();
    let ledger_dto: SkeletonLedgerDto = SkeletonLedgerDto::new(
        ledger.run_total(),
        ledger.aborted_total(),
        ledger.performed_total(),
        ledger.skipped_total(),
        ledger.failed_total(),
    );
    let level_totals: [usize; 4] = [
        dispatcher.level_total(LogLevel::Debug),
        dispatcher.level_total(LogLevel::Info),
        dispatcher.level_total(LogLevel::Warn),
        dispatcher.level_total(LogLevel::Error),
    ];
    let profile: SystemProfileDto = SystemProfileDto::new(
        dispatcher.clock_name(),
        dispatcher.emitted_total(),
        level_totals,
        format!("Logs/{}/app-*.log(按等级分流)", DEMO_DATE),
        container.repository().implementation_name(),
        registry.runner_total(),
    );

    let mut lines: Vec<String> = section_header(
        6,
        "系统画像:日志、台账、注册清单------「跑了什么」不靠读代码猜",
    );

    let mut table: TableSpec = TableSpec::new(vec![
        ColumnSpec::left("项目", 28),
        ColumnSpec::left("值", 96),
    ]);
    table.push_row(vec![
        String::from("时钟"),
        String::from(profile.clock_name()),
    ]);
    table.push_row(vec![
        String::from("日志总条数"),
        format!("{}", profile.log_total()),
    ]);
    table.push_row(vec![
        String::from("日志等级分布"),
        format!(
            "调试 {} / 信息 {} / 警告 {} / 错误 {}",
            profile.level_total(0),
            profile.level_total(1),
            profile.level_total(2),
            profile.level_total(3)
        ),
    ]);
    table.push_row(vec![
        String::from("控制台行数(截至上一幕)"),
        format!("{}", console_total),
    ]);
    table.push_row(vec![
        String::from("文件行数(截至上一幕)"),
        format!("{}", file_total),
    ]);
    table.push_row(vec![
        String::from("日志目录"),
        String::from(profile.log_directory()),
    ]);
    table.push_row(vec![
        String::from("仓储实现"),
        String::from(profile.repository_name()),
    ]);
    table.push_row(vec![
        String::from("注册的骨架执行器"),
        format!("{} 个(十二个内置部门)", profile.runner_total()),
    ]);
    table.push_row(vec![
        String::from("骨架台账"),
        format!(
            "跑 {} 次 / 中止 {} 次 / 累计 {} 步(执行 {} / 跳过 {} / 失败 {})",
            ledger_dto.run_total(),
            ledger_dto.aborted_total(),
            ledger_dto.step_total(),
            ledger_dto.performed_total(),
            ledger_dto.skipped_total(),
            ledger_dto.failed_total()
        ),
    ]);
    table.push_note(String::from(
        "控制台与文件共用 common::logging::format_line:同一行日志在两个落地上的字节相同,区别只在「写到哪」和「按等级分流」。",
    ));
    table.push_note(String::from(
        "日志时间戳由序号推导(固定时钟 + 137ms/条),不是墙上时钟------所以两次运行的输出可以逐字节一致。",
    ));
    lines.append(&mut render_table(&table));

    lines.push(String::from("注册清单(容器实际注入了什么):"));
    for item in container.registration_list() {
        lines.push(format!("  · {}", item));
    }

    emit(report, dispatcher, lines);
}

// ======================================================================
// 用例驱动:十二个部门依次作业
// ======================================================================

/// 按注册顺序把十二个部门跑完。
///
/// ★ 两个演示细节:
/// 1. 物流成功后模拟客户签收------签收不是部门作业,但财务的准入依赖它;
/// 2. `dispatch_support` 为真时才派发支撑任务,避免十张单的输出被支撑日志淹没。
#[allow(clippy::too_many_arguments)]
fn run_end_to_end(
    service: &ProcessApplicationService,
    registry: &DepartmentRegistry,
    order_number_text: &str,
    gateway: &dyn RemoteGateway,
    repository: &Arc<dyn OrderRepository>,
    dispatch: &DepartmentDispatchService,
    stop_on_failure: bool,
    dispatch_support: bool,
) -> Vec<RunOutcome> {
    let mut outcomes: Vec<RunOutcome> = Vec::new();
    for runner in registry.runners().iter() {
        let outcome: RunOutcome =
            service.run_department(order_number_text, runner, Some(gateway));
        let succeeded: bool = outcome.succeeded();
        let department_code: DepartmentCode = outcome.department_code();

        if succeeded && dispatch_support {
            dispatch.dispatch(order_number_text, department_code);
        }

        outcomes.push(outcome);

        // 物流完成 → 客户签收(模拟领域外动作),财务才具备结算前提。
        if department_code == DepartmentCode::LOGISTICS && succeeded {
            if let Some(mut order) = repository.find_by_number(order_number_text) {
                order.mark_all_delivered();
                repository.save(order);
            }
        }

        if !succeeded && stop_on_failure {
            break;
        }
    }
    outcomes
}

// ======================================================================
// 日志与报表辅助
// ======================================================================

/// 写一段报表,并立刻 flush(让控制台与文件的顺序都可核对)。
fn emit(report: &mut ReportConsole, dispatcher: &LoggingDispatcher, lines: Vec<String>) {
    report.write_lines(lines.as_slice());
    let _ = dispatcher.flush();
}

/// 控制台已写行数。
fn count_console_lines(lines: &Arc<Mutex<Vec<(LogLevel, String)>>>) -> usize {
    match lines.lock() {
        Ok(guard) => guard.len(),
        Err(poisoned) => poisoned.into_inner().len(),
    }
}

/// 文件落地已写行数(所有等级文件之和)。
fn count_file_lines(
    statistics: &Arc<Mutex<BTreeMap<(String, LogLevel), (usize, usize)>>>,
) -> usize {
    match statistics.lock() {
        Ok(guard) => guard.values().map(|(line_total, _)| *line_total).sum(),
        Err(poisoned) => poisoned
            .into_inner()
            .values()
            .map(|(line_total, _)| *line_total)
            .sum(),
    }
}

输出:

相关推荐
外收内放36 分钟前
面向对象高级(封装)
开发语言·python
每天一道题38 分钟前
从单体到微服务:Service、Instance 与服务拆分到底是什么关系?
后端·微服务·云原生·架构
怕浪猫1 小时前
前端对接聊天 API:我用 React 做了一个比 ChatGPT 还好看的界面
前端·后端·面试
devilnumber1 小时前
Java 内部迭代 vs 外部迭代:从原理到实战避坑
java·开发语言
IT_陈寒1 小时前
深入理解Python:核心原理与最佳实践
前端·人工智能·后端
学逆向的1 小时前
C printf原型解析
c语言·开发语言·网络安全
\光辉岁月/1 小时前
5.math类
java·开发语言
不像程序员的程序媛1 小时前
mybatisplus insert dto未指定id时会如何
java·开发语言
BeiGuTing_code1 小时前
C字符输出:一个换行符引发的“失踪案”
c语言·开发语言