# 架构反思实施计划 > **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/WASM(wit-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`: ```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** ```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** ```rust // 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.toml` 的 `workspace.members` 中添加 `"crates/erp-plugin-assessment"`。 - [ ] **Step 5: 编译验证** ```bash cargo check -p erp-plugin-assessment ``` - [ ] **Step 6: 提交** ```bash 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.rs`(on_tenant_created 插入默认量表) - [ ] **Step 1: 在 on_tenant_created 中插入 PHQ-9 默认数据** PHQ-9 的 9 道题(每题 0-3 分)和评分规则: ```json // 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: 提交** ```bash 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** ```bash cd crates/erp-plugin-assessment cargo build --target wasm32-unknown-unknown --release # 或使用项目内的 WASM 编译脚本 ``` - [ ] **Step 2: 验证插件加载** 启动后端,确认插件系统识别 assessment 插件,动态表创建成功。 - [ ] **Step 3: 通过 API 测试评估量表 CRUD** ```bash # 创建量表 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: 提交** ```bash 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.toml`(workspace members) - [ ] **Step 1: 创建 Cargo.toml** ```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: 创建标准模块文件** ```rust // 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; ``` ```rust // 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 { 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: 编译验证** ```bash cargo check -p erp-dialysis ``` - [ ] **Step 5: 提交** ```bash git commit -m "feat(dialysis): 创建 erp-dialysis crate 骨架 + ErpModule 实现" ``` --- ### Task 5: 迁移透析 Entity + Service + Handler + DTO **Files:** - Move: 6 个文件从 `erp-health` → `erp-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** ```bash # 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::HealthState` → `crate::state::DialysisState` - `crate::error::{HealthError, HealthResult}` → `crate::error::{DialysisError, DialysisResult}` - `crate::entity::` → 保持不变(同 crate 内) - `crate::dto::` → 保持不变 - `crate::service::` → 保持不变 - [ ] **Step 3: 创建 error.rs** ```rust // 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 for AppError { fn from(e: DialysisError) -> Self { AppError::Business(e.to_string()) } } pub type DialysisResult = Result; ``` - [ ] **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: 编译 + 全链路验证** ```bash cargo check --workspace # 启动后端,验证透析相关 API 正常 ``` - [ ] **Step 7: 提交** ```bash git commit -m "refactor: 透析模块拆分为独立 erp-dialysis crate(2 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 消费者** ```rust // 在 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: 编译验证** ```bash cargo check -p erp-health ``` - [ ] **Step 3: 提交** ```bash git commit -m "feat(health): patient.created 消费者 — 新患者欢迎消息" ``` --- ### Task 7: appointment.confirmed/cancelled → 通知 + 号源 **Files:** - Modify: `crates/erp-health/src/event.rs` - [ ] **Step 1: 添加 appointment 事件消费者** ```rust 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: 编译 + 提交** ```bash 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 消费者** ```rust // 在 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: 编译 + 提交** ```bash 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(事件消费者)应在技术债批次 B(EventBus dead-letter)之后执行 - Chunk 2(透析拆分)应在技术债批次 A(安全修复)之后执行,避免合并冲突 - Chunk 1(WASM 插件)完全独立,随时可执行