diff --git a/docs/superpowers/plans/2026-04-28-architecture-retrospective-plan.md b/docs/superpowers/plans/2026-04-28-architecture-retrospective-plan.md new file mode 100644 index 0000000..fd9e49d --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-architecture-retrospective-plan.md @@ -0,0 +1,714 @@ +# 架构反思实施计划 + +> **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 插件)完全独立,随时可执行