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
20 KiB
架构反思实施计划
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:
[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.toml 的 workspace.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.rs(on_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.toml(workspace 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-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
# 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
// 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 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 消费者
// 在 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(事件消费者)应在技术债批次 B(EventBus dead-letter)之后执行
- Chunk 2(透析拆分)应在技术债批次 A(安全修复)之后执行,避免合并冲突
- Chunk 1(WASM 插件)完全独立,随时可执行