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(¤t, 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, ®istry);
// ==================================================================
// 第二幕:一张工单走完十二个部门(骨架试跑 + 端到端落库)
// ==================================================================
let (first_outcomes, real_trace) = act_two_single_order(
&mut report,
&dispatcher,
&container,
&service,
®istry,
&gateway,
);
// ==================================================================
// 第三幕:十张工单的端到端结果(失败也是结果,不许静默跳过)
// ==================================================================
act_three_all_orders(
&mut report,
&dispatcher,
&container,
&service,
®istry,
&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,
®istry,
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(),
}
}
输出:
