Files
hms/docs/archive/superpowers-completed/2026-04-28-architecture-retrospective-plan.md
iven 18fa6ce6d4 docs: 全局文档梳理归档 — 删除过期文件 + 归档 V1/早期设计 + wiki 数据校正 + CLAUDE.md 规则优化
**根目录清理:**
- 删除 CLAUDE-1.md(ZCLAW 旧项目配置,HMS 已完全脱离)
- 移动 DESIGN.md → docs/archive/(ERP 旧设计系统)
- 删除 plans/ 98 个临时会话计划文件

**归档重组:**
- V1 审计(12 文件)→ docs/archive/audits-v1/
- 早期 CRM/插件迭代设计(13 文件)→ docs/archive/superpowers-early/
- 已完成/已取代设计(28 文件)→ docs/archive/superpowers-completed/
- 早期讨论/测试报告 → docs/archive/discussions-early/ + test-reports-early/
- QA 重复文件清理(3 个旧版 result 文件)

**wiki 数据校正:**
- 迁移数 137→145,源文件 599→649,提交数 720→800+
- 小程序文件 124→163,Web 前端 297→332
- 后端测试 999→943(实际统计),权限码 75+→128
- 文档索引新增归档目录说明

**CLAUDE.md 规则优化:**
- §2.5 闭环工作法:提交+文档+推送三合一 + wiki 更新触发条件
- §2.6 Feature DoD:新增文档一致性检查项
- §6 反模式:新增 wiki 更新滞后/推送不及时警告
2026-05-15 09:29:04 +08:00

715 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构反思实施计划
> **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`
```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<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: 编译验证**
```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<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: 编译 + 全链路验证**
```bash
cargo check --workspace
# 启动后端,验证透析相关 API 正常
```
- [ ] **Step 7: 提交**
```bash
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 消费者**
```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事件消费者应在技术债批次 BEventBus dead-letter之后执行
- Chunk 2透析拆分应在技术债批次 A安全修复之后执行避免合并冲突
- Chunk 1WASM 插件)完全独立,随时可执行