Files
hms/docs/superpowers/plans/2026-04-28-architecture-retrospective-plan.md
iven ade8497c2d
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
docs(plan): 架构反思实施计划 — WASM 评估量表 + 透析拆分 + P1 事件消费者
8 Tasks / 3 Chunks:
- Chunk 1: WASM 评估量表插件 (PHQ-9) — crate 骨架 + 默认数据 + WASM 编译
- Chunk 2: 透析模块拆分 erp-dialysis — 8 文件 ~1100 行迁移
- Chunk 3: P1 事件消费者补全 — patient.created / appointment 通知 / follow_up.overdue
2026-04-28 11:58:01 +08:00

20 KiB
Raw Blame History

架构反思实施计划

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 落地架构反思三个结论 — WASM 评估量表插件、透析模块独立、P1 事件消费者补全。

Architecture: 三条独立工作线可并行推进。WASM 插件遵循 erp-plugin-test-sample 模式;透析模块拆分参照 erp-points 拆 crate 模式;事件消费者补全遵循现有 subscribe_filtered + tokio::spawn 模式。

Tech Stack: Rust/SeaORM/Axum/WASMwit-bindgen 0.55

Spec: docs/discussions/2026-04-28-architecture-retrospective.md


Chunk 1: WASM 评估量表插件PHQ-9

Task 1: 创建插件 crate 骨架

Files:

  • Create: crates/erp-plugin-assessment/Cargo.toml

  • Create: crates/erp-plugin-assessment/src/lib.rs

  • Create: crates/erp-plugin-assessment/plugin.toml

  • Step 1: 创建 Cargo.toml

参照 crates/erp-plugin-test-sample/Cargo.toml

[package]
name = "erp-plugin-assessment"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.55"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
  • Step 2: 创建 plugin.toml
[metadata]
id = "assessment"
name = "评估量表"
version = "0.1.0"
description = "标准化医学评估量表PHQ-9、GAD-7 等)"
author = "HMS"
min_platform_version = "0.1.0"

[[permissions]]
code = "assessment_scale.list"
name = "查看评估量表"
description = "查看评估量表列表和详情"

[[permissions]]
code = "assessment_scale.manage"
name = "管理评估量表"
description = "创建、编辑、删除评估量表"

[[permissions]]
code = "assessment_response.list"
name = "查看评估结果"
description = "查看患者评估答卷"

[[permissions]]
code = "assessment_response.manage"
name = "管理评估结果"
description = "提交、编辑评估答卷"

[[schema.entities]]
name = "assessment_scale"
display_name = "评估量表"

[[schema.entities.fields]]
name = "scale_code"
field_type = "string"
required = true
display_name = "量表编码"
unique = true
ui_widget = "select"
options = ["PHQ-9", "GAD-7", "SF-36", "MMSE", "ADL", "IADL"]

[[schema.entities.fields]]
name = "title"
field_type = "string"
required = true
display_name = "量表名称"
searchable = true

[[schema.entities.fields]]
name = "description"
field_type = "string"
display_name = "描述"
ui_widget = "textarea"

[[schema.entities.fields]]
name = "questions_json"
field_type = "json"
required = true
display_name = "题目定义JSON"

[[schema.entities.fields]]
name = "scoring_rules_json"
field_type = "json"
required = true
display_name = "评分规则JSON"

[[schema.entities.fields]]
name = "status"
field_type = "string"
required = true
display_name = "状态"
default = "active"
ui_widget = "select"
options = ["active", "inactive"]

[[schema.entities]]
name = "assessment_response"
display_name = "评估答卷"

[[schema.entities.fields]]
name = "scale_id"
field_type = "uuid"
required = true
display_name = "量表"
ui_widget = "entity_select"
ref_entity = "assessment_scale"
ref_plugin = "assessment"

[[schema.entities.fields]]
name = "patient_id"
field_type = "uuid"
required = true
display_name = "患者 ID"

[[schema.entities.fields]]
name = "answers_json"
field_type = "json"
required = true
display_name = "答案JSON"

[[schema.entities.fields]]
name = "total_score"
field_type = "integer"
required = true
display_name = "总分"

[[schema.entities.fields]]
name = "severity_level"
field_type = "string"
required = true
display_name = "严重程度"
ui_widget = "select"
options = ["normal", "mild", "moderate", "severe"]

[[schema.entities.fields]]
name = "assessed_by"
field_type = "uuid"
display_name = "评估人"

[[schema.entities.fields]]
name = "status"
field_type = "string"
required = true
display_name = "状态"
default = "completed"
ui_widget = "select"
options = ["draft", "completed", "reviewed"]

[[schema.entities.relations]]
entity = "assessment_scale"
foreign_key = "scale_id"
on_delete = "restrict"
name = "scale"
type = "belongs_to"
display_field = "title"

[[trigger_events]]
name = "assessment_completed"
display_name = "评估完成"
description = "患者完成评估量表,触发评分计算和后续流程"
entity = "assessment_response"
on = "create"

[[ui.pages]]
type = "crud"
label = "评估量表"
icon = "FormOutlined"
  • Step 3: 创建 src/lib.rs
// crates/erp-plugin-assessment/src/lib.rs
//! 评估量表插件 — 标准化医学评估PHQ-9, GAD-7 等)

wit_bindgen::generate!({
    path: "../../crates/erp-plugin/src/wit/plugin.wit",
    world: "plugin-world",
});

use crate::exports::erp::plugin::plugin_api::Guest;
use crate::erp::plugin::host_api::*;

struct AssessmentPlugin;

impl Guest for AssessmentPlugin {
    fn init() -> Result<(), String> {
        log_write("info", "AssessmentPlugin initialized");
        Ok(())
    }

    fn on_tenant_created(tenant_id: String) -> Result<(), String> {
        log_write("info", &format!("AssessmentPlugin: tenant {} created", tenant_id));
        // 可以为新租户插入默认量表PHQ-9、GAD-7
        Ok(())
    }

    fn handle_event(
        event_type: String,
        _event_id: String,
        _tenant_id: String,
        _payload: String,
    ) -> Result<(), String> {
        log_write("debug", &format!("AssessmentPlugin received: {}", event_type));
        Ok(())
    }
}

export!(AssessmentPlugin);
  • Step 4: 注册到 workspace

在根 Cargo.tomlworkspace.members 中添加 "crates/erp-plugin-assessment"

  • Step 5: 编译验证
cargo check -p erp-plugin-assessment
  • Step 6: 提交
git add crates/erp-plugin-assessment/ Cargo.toml
git commit -m "feat(plugin): 评估量表插件骨架 — assessment_scale + assessment_response"

Task 2: PHQ-9 默认量表数据 + 评分逻辑

Files:

  • Modify: crates/erp-plugin-assessment/src/lib.rson_tenant_created 插入默认量表)

  • Step 1: 在 on_tenant_created 中插入 PHQ-9 默认数据

PHQ-9 的 9 道题(每题 0-3 分)和评分规则:

// questions_json
[
  {"id": 1, "text": "做事时提不起劲或没有兴趣", "options": [{"label": "完全不会", "score": 0}, {"label": "好几天", "score": 1}, {"label": "一半以上的天数", "score": 2}, {"label": "几乎每天", "score": 3}]},
  // ... 共 9 题
]

// scoring_rules_json
[
  {"min": 0, "max": 4, "level": "normal", "label": "无抑郁症状"},
  {"min": 5, "max": 9, "level": "mild", "label": "轻度抑郁"},
  {"min": 10, "max": 14, "level": "moderate", "label": "中度抑郁"},
  {"min": 15, "max": 19, "level": "moderate_severe", "label": "中重度抑郁"},
  {"min": 20, "max": 27, "level": "severe", "label": "重度抑郁"}
]

通过 db_insert host API 在 on_tenant_created 中插入。

  • Step 2: 编译 + 验证

  • Step 3: 提交

git commit -m "feat(plugin): PHQ-9 默认量表数据 + 评分规则"

Task 3: 编译 WASM + 注册到 erp-server

Files:

  • Modify: crates/erp-server/src/main.rs(插件注册,如需手动加载)

  • Verify: WASM 编译输出

  • Step 1: 编译为 WASM Component

cd crates/erp-plugin-assessment
cargo build --target wasm32-unknown-unknown --release
# 或使用项目内的 WASM 编译脚本
  • Step 2: 验证插件加载

启动后端,确认插件系统识别 assessment 插件,动态表创建成功。

  • Step 3: 通过 API 测试评估量表 CRUD
# 创建量表
curl -X POST /api/v1/plugin/assessment/assessment_scale \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"scale_code": "PHQ-9", ...}'

# 提交答卷
curl -X POST /api/v1/plugin/assessment/assessment_response \
  -d '{"scale_id": "...", "patient_id": "...", "answers_json": [...]}'
  • Step 4: 提交
git commit -m "feat(plugin): 评估量表 WASM 编译 + 端到端验证"

Chunk 2: 透析模块拆分为 erp-dialysis

Task 4: 创建 erp-dialysis crate 骨架

Files:

  • Create: crates/erp-dialysis/Cargo.toml

  • Create: crates/erp-dialysis/src/{lib,module,state,error}.rs

  • Modify: Cargo.tomlworkspace members

  • Step 1: 创建 Cargo.toml

[package]
name = "erp-dialysis"
version.workspace = true
edition.workspace = true

[dependencies]
erp-core.workspace = true
sea-orm.workspace = true
tokio.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
uuid.workspace = true
chrono.workspace = true
axum.workspace = true
utoipa.workspace = true
validator.workspace = true
async-trait.workspace = true
tracing.workspace = true
rust_decimal.workspace = true
  • Step 2: 创建标准模块文件
// crates/erp-dialysis/src/lib.rs
pub mod dto;
pub mod entity;
pub mod error;
pub mod event;
pub mod handler;
pub mod module;
pub mod service;
pub mod state;

pub use module::DialysisModule;
pub use state::DialysisState;
// crates/erp-dialysis/src/module.rs
//! ErpModule trait 实现

use async_trait::async_trait;
use erp_core::module::{ErpModule, ModuleContext, ModuleType, PermissionDescriptor};
use erp_core::events::EventBus;
use crate::state::DialysisState;

pub struct DialysisModule {
    state: DialysisState,
}

impl DialysisModule {
    pub fn new(db: sea_orm::DatabaseConnection, event_bus: EventBus) -> Self {
        Self { state: DialysisState { db, event_bus } }
    }
}

#[async_trait]
impl ErpModule for DialysisModule {
    fn name(&self) -> &str { "透析管理" }
    fn id(&self) -> &str { "erp-dialysis" }
    fn version(&self) -> &str { "0.1.0" }
    fn module_type(&self) -> ModuleType { ModuleType::Builtin }

    fn permissions(&self) -> Vec<PermissionDescriptor> {
        vec![
            PermissionDescriptor { code: "dialysis.record.list".into(), name: "查看透析记录".into() },
            PermissionDescriptor { code: "dialysis.record.manage".into(), name: "管理透析记录".into() },
            PermissionDescriptor { code: "dialysis.prescription.list".into(), name: "查看透析处方".into() },
            PermissionDescriptor { code: "dialysis.prescription.manage".into(), name: "管理透析处方".into() },
        ]
    }

    fn on_startup(&self, ctx: &ModuleContext) {
        crate::event::register_handlers_with_state(self.state.clone());
    }

    fn as_any(&self) -> &dyn std::any::Any { self }
}
  • Step 3: 注册到 workspace

在根 Cargo.toml 的 members 中添加 "crates/erp-dialysis"

  • Step 4: 编译验证
cargo check -p erp-dialysis
  • Step 5: 提交
git commit -m "feat(dialysis): 创建 erp-dialysis crate 骨架 + ErpModule 实现"

Task 5: 迁移透析 Entity + Service + Handler + DTO

Files:

  • Move: 6 个文件从 erp-healtherp-dialysis
  • Modify: crates/erp-health/src/{entity,service,handler,dto}/mod.rs(删除透析导出)
  • Modify: 迁移文件中的 crate:: 引用改为 erp_core:: 或 erp-dialysis 内部引用

待迁移文件清单:

来源 目标 行数
erp-health/src/entity/dialysis_record.rs erp-dialysis/src/entity/ 82
erp-health/src/entity/dialysis_prescription.rs erp-dialysis/src/entity/ 78
erp-health/src/service/dialysis_service.rs erp-dialysis/src/service/ 333
erp-health/src/service/dialysis_prescription_service.rs erp-dialysis/src/service/ 274
erp-health/src/handler/dialysis_handler.rs erp-dialysis/src/handler/ 145
erp-health/src/handler/dialysis_prescription_handler.rs erp-dialysis/src/handler/ 120
erp-health/src/dto/dialysis_dto.rs erp-dialysis/src/dto/ 125
erp-health/src/dto/dialysis_prescription_dto.rs erp-dialysis/src/dto/ 107
  • Step 1: 复制文件到 erp-dialysis
# Entity
cp crates/erp-health/src/entity/dialysis_record.rs crates/erp-dialysis/src/entity/
cp crates/erp-health/src/entity/dialysis_prescription.rs crates/erp-dialysis/src/entity/
# Service
cp crates/erp-health/src/service/dialysis_service.rs crates/erp-dialysis/src/service/
cp crates/erp-health/src/service/dialysis_prescription_service.rs crates/erp-dialysis/src/service/
# Handler
cp crates/erp-health/src/handler/dialysis_handler.rs crates/erp-dialysis/src/handler/
cp crates/erp-health/src/handler/dialysis_prescription_handler.rs crates/erp-dialysis/src/handler/
# DTO
cp crates/erp-health/src/dto/dialysis_dto.rs crates/erp-dialysis/src/dto/
cp crates/erp-health/src/dto/dialysis_prescription_dto.rs crates/erp-dialysis/src/dto/
  • Step 2: 更新 crate 内引用

全局替换:

  • crate::state::HealthStatecrate::state::DialysisState

  • crate::error::{HealthError, HealthResult}crate::error::{DialysisError, DialysisResult}

  • crate::entity:: → 保持不变(同 crate 内)

  • crate::dto:: → 保持不变

  • crate::service:: → 保持不变

  • Step 3: 创建 error.rs

// crates/erp-dialysis/src/error.rs
use erp_core::error::AppError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DialysisError {
    #[error("透析记录未找到: {0}")]
    RecordNotFound(uuid::Uuid),
    #[error("处方未找到: {0}")]
    PrescriptionNotFound(uuid::Uuid),
    #[error("状态转换无效: {0} → {1}")]
    InvalidStatusTransition(String, String),
    #[error("版本冲突")]
    VersionConflict,
}

impl From<DialysisError> for AppError {
    fn from(e: DialysisError) -> Self { AppError::Business(e.to_string()) }
}

pub type DialysisResult<T> = Result<T, DialysisError>;
  • Step 4: 从 erp-health 删除透析代码

从以下 mod.rs 中移除透析相关 pub mod 声明:

  • crates/erp-health/src/entity/mod.rs

  • crates/erp-health/src/service/mod.rs

  • crates/erp-health/src/handler/mod.rs

  • crates/erp-health/src/dto/mod.rs

  • Step 5: 在 erp-server 注册新模块

crates/erp-server/src/main.rs 中:

  • 添加 use erp_dialysis::DialysisModule;

  • 在 registry 链中 .register(dialysis_module)

  • 在路由 merge 中 .merge(erp_dialysis::DialysisModule::protected_routes())

  • Step 6: 编译 + 全链路验证

cargo check --workspace
# 启动后端,验证透析相关 API 正常
  • Step 7: 提交
git commit -m "refactor: 透析模块拆分为独立 erp-dialysis crate2 Entity + 2 Service"

Chunk 3: P1 事件消费者补全

Task 6: patient.created → 欢迎消息 + 默认随访

Files:

  • Modify: crates/erp-health/src/event.rs(添加消费者)

  • Step 1: 在 register_handlers_with_state 中添加 patient.created 消费者

// 在 register_handlers_with_state() 中新增:
let (mut patient_rx, _) = state.event_bus.subscribe_filtered("patient.".to_string());
let patient_db = state.db.clone();
tokio::spawn(async move {
    loop {
        match patient_rx.recv().await {
            Some(event) if event.event_type == PATIENT_CREATED => {
                if erp_core::events::is_event_processed(&patient_db, event.id, "patient_welcome").await.unwrap_or(false) {
                    continue;
                }
                let patient_id = event.payload.get("patient_id")
                    .and_then(|v| v.as_str())
                    .and_then(|s| Uuid::parse_str(s).ok());
                if let Some(pid) = patient_id {
                    // 1. 发布欢迎消息事件(消息模块消费后发送站内通知)
                    let welcome_event = DomainEvent::new(
                        "message.send",
                        event.tenant_id,
                        erp_core::events::build_event_payload(serde_json::json!({
                            "template": "patient_welcome",
                            "recipient_type": "patient",
                            "recipient_id": pid,
                        })),
                    );
                    // 2. TODO: 创建默认随访计划(后续迭代)
                    tracing::info!(patient_id = %pid, "新患者欢迎流程触发");
                }
                let _ = erp_core::events::mark_event_processed(&patient_db, event.id, "patient_welcome").await;
            }
            Some(_) => {}
            None => break,
        }
    }
});
  • Step 2: 编译验证
cargo check -p erp-health
  • Step 3: 提交
git commit -m "feat(health): patient.created 消费者 — 新患者欢迎消息"

Task 7: appointment.confirmed/cancelled → 通知 + 号源

Files:

  • Modify: crates/erp-health/src/event.rs

  • Step 1: 添加 appointment 事件消费者

let (mut appt_rx, _) = state.event_bus.subscribe_filtered("appointment.".to_string());
let appt_db = state.db.clone();
tokio::spawn(async move {
    loop {
        match appt_rx.recv().await {
            Some(event) if event.event_type == "appointment.confirmed" => {
                if erp_core::events::is_event_processed(&appt_db, event.id, "appointment_notifier").await.unwrap_or(false) {
                    continue;
                }
                // 通知医生
                let doctor_id = event.payload.get("doctor_id").and_then(|v| v.as_str());
                let patient_id = event.payload.get("patient_id").and_then(|v| v.as_str());
                if let (Some(did), Some(pid)) = (doctor_id, patient_id) {
                    tracing::info!(doctor_id = did, patient_id = pid, "预约确认通知触发");
                    // 发布通知事件
                }
                let _ = erp_core::events::mark_event_processed(&appt_db, event.id, "appointment_notifier").await;
            }
            Some(event) if event.event_type == "appointment.cancelled" => {
                // 释放号源 + 通知排队患者
                tracing::info!(event_id = %event.id, "预约取消,号源释放");
            }
            Some(_) => {}
            None => break,
        }
    }
});
  • Step 2: 编译 + 提交
cargo check -p erp-health
git commit -m "feat(health): appointment 事件消费者 — 预约确认/取消通知"

Task 8: follow_up.overdue → 升级通知

Files:

  • Modify: crates/erp-health/src/event.rs

  • Step 1: 添加 follow_up.overdue 消费者

// 在 register_handlers_with_state 中:
// 注意follow_up 事件的前缀是 "follow_up."
let (mut fu_rx, _) = state.event_bus.subscribe_filtered("follow_up.".to_string());
let fu_db = state.db.clone();
tokio::spawn(async move {
    loop {
        match fu_rx.recv().await {
            Some(event) if event.event_type == FOLLOW_UP_OVERDUE => {
                if erp_core::events::is_event_processed(&fu_db, event.id, "follow_up_escalator").await.unwrap_or(false) {
                    continue;
                }
                let task_id = event.payload.get("task_id").and_then(|v| v.as_str());
                let assigned_to = event.payload.get("assigned_to").and_then(|v| v.as_str());
                if let (Some(tid), Some(uid)) = (task_id, assigned_to) {
                    // 通知随访负责人 + 科室主管
                    tracing::warn!(task_id = tid, assigned_to = uid, "随访逾期升级通知");
                }
                let _ = erp_core::events::mark_event_processed(&fu_db, event.id, "follow_up_escalator").await;
            }
            Some(_) => {}
            None => break,
        }
    }
});
  • Step 2: 编译 + 提交
cargo check -p erp-health
git commit -m "feat(health): follow_up.overdue 消费者 — 逾期随访升级通知"

执行摘要

Chunk Tasks 内容 预估
1 T1-T3 WASM 评估量表插件PHQ-9 1-2 天
2 T4-T5 透析模块拆 erp-dialysis 1 天
3 T6-T8 P1 事件消费者补全3 个) 0.5-1 天

总计 8 个 Task预估 2.5-4 天。

依赖关系:

  • T1→T2→T3 串行WASM 插件逐层构建)
  • T4→T5 串行(先骨架再迁移)
  • T6/T7/T8 可并行(独立消费者)
  • Chunk 1/2/3 相互独立,可完全并行

与技术债计划的关系:

  • 本计划的 Chunk 3事件消费者应在技术债批次 BEventBus dead-letter之后执行
  • Chunk 2透析拆分应在技术债批次 A安全修复之后执行避免合并冲突
  • Chunk 1WASM 插件)完全独立,随时可执行