Files
hms/wiki/architecture.md
iven 6e8239daf0 docs: V1 测试版本全面端到端测试报告 + 专家评估 + wiki 更新
- 测试报告: 157 端点测试, Health 63% / AI+Dialysis+Plugin 92.4%
- 专家评估: 产品7.3/架构7.6/安全7.0/测试4.1/UX7.6, 综合6.2 B-
- CRITICAL×2: 空标签名500 + 媒体库路由冲突
- CONDITIONAL GO: 修复 P0 问题后可发布

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 06:59:31 +08:00

267 lines
12 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.
---
title: 架构决策记录
updated: 2026-05-07
status: stable
tags: [architecture, decisions, design-principles]
---
# 架构决策记录
> 从 [[index]] 导航。关联: [[erp-core]] [[erp-server]] [[database]] [[wasm-plugin]] [[erp-health]]
## 1. 设计决策
### 模块化单体 + 渐进式拆分
模块间零直接依赖,跨模块通信通过事件总线和 trait 接口。`ErpModule` trait 天然支持未来按模块拆分为微服务。
### HMS 架构:原生模块 + 插件并存
HMS 继承 ERP 底座的所有基础模块,`erp-health` 作为原生 Rust 模块承载医疗业务。WASM 插件系统保留但非 HMS 主要扩展方式。
```
HMS 平台
├── 基础模块(继承 ERP: auth, config, workflow, message, plugin
├── 核心业务模块: erp-health原生 Rust46 实体/39 权限/179 文件/31 事件)★ 已实现
├── AI 模块: erp-ai6 实体/45 文件/22 路由SSE 流式分析4 AI ProviderPhase 1 MVP
├── 透析模块: erp-dialysis已拆分为独立 crate
└── 可选插件: crm, inventory, freelance, itops, assessmentWASM
```
### 为什么 erp-health 用原生模块?
医疗业务需要 46 强类型实体、自定义 API趋势分析/统计报表、PII 数据加密AES-256-GCM + KEK/DEK 分层)、文件上传、未来 AI 集成。WASM 插件的 JSONB 动态存储和 20 实体上限无法满足。详见 [[erp-health]]。
### 为什么用 UUIDv7
时间排序 + UUID 唯一性 + 接近自增 ID 的索引性能。多租户 SaaS 下不同租户数据不会因 ID 冲突互相影响。
### 为什么 tenant_id 不在 API 路径中?
从 JWT 提取,中间件注入 `TenantContext`。防止:手动改 URL 越权 / API 暴露租户信息 / 忘记检查权限。管理员接口例外。
### 为什么错误类型跨 crate 用 thiserror
`anyhow` 无类型信息,无法精确映射 HTTP 状态码。`thiserror``AppError` → 400/401/403/404/409/500。
### 为什么预约用原子 CAS
防止并发创建预约时超额。事务内 `UPDATE current_appointments + 1 WHERE current < max`CAS 成功后才 INSERT 预约记录。
## 2. 项目结构
### 目录布局
```text
hms/
├── crates/ # Rust Workspace
│ ├── erp-core/ # L1: 基础类型、错误、事件、模块 trait
│ ├── erp-auth/ # L2: 身份与权限模块
│ ├── erp-workflow/ # L2: 工作流引擎模块
│ ├── erp-message/ # L2: 消息中心模块
│ ├── erp-config/ # L2: 系统配置模块
│ ├── erp-health/ # L2: 健康管理模块 ★ HMS 核心
│ └── erp-server/ # L3: Axum 服务入口,组装所有模块
│ └── migration/ # SeaORM 数据库迁移
├── apps/
│ └── web/ # Vite + React 19 SPA (主力前端)
├── packages/
│ └── ui-components/ # React 共享组件库
├── desktop/ # (可选) Tauri 桌面端
├── docker/ # Docker 开发环境配置
├── docs/
│ ├── superpowers/specs/ # 设计规格文档
│ └── discussions/ # 讨论记录
├── wiki/ # 项目知识库
└── Cargo.toml # Workspace root
```
### 模块依赖图
```
erp-core (L1)
|
+--------------+--------------+--------------+-----------+
| | | | |
erp-auth erp-config erp-workflow erp-message erp-health erp-ai (L2)
| | | | |
|
erp-dialysis
+--------------+--------------+--------------+-----------+
|
erp-server (L3: 唯一组装点)
|
erp-plugin (WASM 插件运行时)
```
**禁止**: L2 间直接依赖 / L1 依赖业务模块 / 绕过事件总线
### 模块实现状态
| 模块 | 状态 | 实体数 | 权限数 | 页面数 | 测试覆盖 |
|------|------|--------|--------|--------|---------|
| erp-auth | ✅ 完成 | 11 表 | 23种子数据 | 用户/角色/组织 | 41 单元 + 3 集成 |
| erp-config | ✅ 完成 | 6 表 | 18种子数据 | 设置/字典/菜单 | 78 单元 |
| erp-workflow | ✅ 完成 | 5 表 | 8种子数据 | 工作流管理 | 63 单元 + 4 集成 |
| erp-message | ✅ 完成 | 3 表 | 5种子数据 | 消息中心 | 72 单元 |
| erp-plugin | ✅ 完成 | 4 表 | 2种子数据 | 插件管理/市场 | 78 单元 + 2 集成 |
| erp-health | ✅ 完成 | 46 表 | 39 | 25+ 页面 + 工作台 | 159 单元 + 144 集成 |
| erp-ai | 🔄 Phase 1 | 6 表 | 6 | AI 分析/Prompt/建议/风险/用量 | 36 单元 |
| erp-dialysis | 🔄 已拆分 | - | 5 | - | 10 单元 + 15 集成 |
### 技术选型
| 选择 | 理由 |
|------|------|
| Axum 0.8 | Tokio 团队维护tower 生态,类型安全路由 |
| SeaORM 1.1 | 异步、类型安全、迁移工具完善 |
| PostgreSQL 16 | 企业级JSON 支持,扩展丰富 |
| Redis 7 | 缓存 + 限流 token bucket |
| React 19 + Ant Design 6 | 企业后台 UI 标配 |
| Zustand 5 | 极简状态管理 |
| Wasmtime 43 | WASM 沙箱Component ModelFuel 限制 |
### 集成契约
| 方向 | 模块 | 触发时机 |
|------|------|---------|
| 定义 → | [[erp-core]] | 所有模块的 trait 和类型 |
| 组装 ← | [[erp-server]] | 6 模块注册和启动 |
| 扩展 ← | [[wasm-plugin]] | 插件通过 Host Bridge 桥接 |
| 业务 ← | [[erp-health]] | 健康模块原生集成 |
## 3. 模块开发规范
### 新建业务模块清单
每个新模块**必须**包含:
1. `Cargo.toml` — 依赖 `erp-core`
2. `src/lib.rs` — 模块入口,实现 `ErpModule` trait
3. `src/error.rs` — 模块错误类型wrap `AppError`
4. `src/entity/` — SeaORM Entity 定义
5. `src/service/` — 业务逻辑层
6. `src/handler/` — Axum 路由处理器
7. `src/event.rs` — 模块事件定义和处理器
### ErpModule trait 实现
```rust
pub struct AuthModule;
impl ErpModule for AuthModule {
fn name(&self) -> &str { "auth" }
fn version(&self) -> &str { env!("CARGO_PKG_VERSION") }
fn dependencies(&self) -> Vec<&str> { vec![] }
fn register_routes(&self, router: Router) -> Router {
router.nest("/api/v1", auth_routes())
}
fn register_event_handlers(&self, bus: &EventBus) { /* 订阅其他模块事件 */ }
async fn on_tenant_created(&self, tenant_id: Uuid) -> AppResult<()> { Ok(()) }
}
```
### 数据库迁移规范
- 迁移文件放在 `crates/erp-server/migration/src/`
- 命名格式:`m{YYYYMMDD}_{6位序号}_{描述}.rs`
- 必须可回滚(实现 `down` 方法)
- 新增表必须包含所有标准字段id, tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version
- 必须幂等(使用 `if_not_exists`
## 4. 安全注意事项
### 认证安全
- **密码存储**: Argon2 哈希,禁止明文
- **JWT**: access token 15min + refresh token 7d
- **Refresh Token 轮换**: 每次使用后签发新的,旧的作废
- **Token 存储**: 桌面端使用 Tauri secure store
- **密码修改**: 使所有已签发的 JWT 失效
### 多租户安全
- **中间件注入**: `tenant_id` 从 JWT 中提取,应用层不可伪造
- **数据隔离**: 所有查询自动过滤 `tenant_id`
- **越权防护**: 禁止跨租户数据访问
- **租户 provisioning**: `on_tenant_created` 钩子初始化数据
### 通用安全
- 不硬编码密钥 — 使用环境变量或配置文件
- 用户输入验证 — 所有 API 端点验证输入
- SQL 注入防护 — SeaORM 参数化查询
- 限流 — Redis token bucket用户 100/minIP 分级)
- CORS — 白名单制,默认拒绝
- 审计日志 — 所有关键操作记录变更前后状态 + 哈希链防篡改
- 动态表 SQL — 使用 `sanitize_identifier` 防注入
- PII 加密 — AES-256-GCM + KEK/DEK 分层管理(每租户独立 DEK
- HMAC 盲索引 — 支持加密字段的等值查询
- RLS 行级安全 — PostgreSQL Row Level Policy中间件 SET `app.current_tenant_id`
- Dead Letter — 失败事件自动写入 dead_letter_events 表
### DTO 输入校验规范
> **历史教训**: 2026-05-19 全系统 DTO 审计发现 44 处校验缺失(跨 6 个 crate根因是 Update 结构体与 Create 结构体校验不对称。以下为强制规范。
**DTO 编写铁律:**
1. **所有请求结构体必须 `derive(Validate)`** — 包括 `Create*Req``Update*Req``Assign*Req`、查询参数等
2. **Update 与 Create 校验对称**`Create*Req``#[validate(length(...))]` 的字段,`Update*Req` 对应字段也必须有,不允许降级
3. **枚举字段必须自定义校验**`status``type``method``channel` 等有限集合字段,使用 `#[validate(custom(function = "..."))]` 限制合法值
4. **Vec 字段必须检查非空**`role_ids``permission_ids``scopes` 等用 `#[validate(length(min = 1))]`
5. **密码字段加 max 限制**`max = 128`,防止 bcrypt DoS
6. **URL 字段必须防 SSRF** — 禁止 `localhost`/`127.0.0.1`/`0.0.0.0`,仅允许 `http://`/`https://` 协议
7. **数值范围必须限定**`rate_limit``timeout``token_lifetime` 等用 `#[validate(range(min, max))]`
8. **handler 层必须调用 `validate()`** — 每个接收 `Json<T>` 的 handler 函数体内第一行必须 `req.validate().map_err(|e| AppError::Validation(e.to_string()))?`
**校验清单(新增 DTO 时逐项确认):**
- [ ] `derive(Validate)` 已添加
- [ ] 字符串字段有 `length(min, max)` 限制
- [ ] 枚举字段有 `custom` 校验函数
- [ ] 集合字段有 `length(min = 1)` 非空检查
- [ ] 数值字段有 `range(min, max)` 范围检查
- [ ] URL 字段有协议和 SSRF 校验
- [ ] handler 层调用了 `.validate()`
- [ ] dto.rs 文件底部有对应的单元测试
## 5. 代码逻辑
**不变量**: 模块间只通过 EventBus 和 trait 通信,无直接依赖
**不变量**: 所有数据表必须含 `tenant_id`,查询自动过滤
**不变量**: UUID v7 作为主键
**不变量**: 软删除,不硬删除
**不变量**: 所有 API 使用 `/api/v1/` 前缀
**不变量**: 预约创建必须走原子 CAS不能用 read-then-write
**不变量**: PII 数据(身份证、手机号)加密存储 + 脱敏展示
## 6. 活跃问题 + 陷阱
⚠️ 当前共享数据库 + tenant_id 过滤,未来可扩展为 Schema 隔离或数据库隔离
⚠️ EventBus 内存 broadcast 需 outbox 持久化保障(已通过后台任务实现)
⚠️ 微信登录固定到 default_tenant_id — 多租户场景需设计解析策略
### 2026-04-30 审计发现
| 发现 | 严重性 | 说明 |
|------|--------|------|
| 前端权限码拼写错误 | CRITICAL | `health.alert.manage``health.alerts.manage`(缺 s告警管理按钮永远不显示 |
| 56 个基础模块权限码未通过 PermissionDescriptor 声明 | MEDIUM | auth/config/workflow/message/plugin 通过种子数据手动注册,新增易遗漏 |
| 14 个事件无业务消费者 | LOW | 发布到 EventBus 但无后端消费者SSE 推送仍有价值 |
| Health service 层运行时日志极缺 | HIGH | 26 个 service 文件仅 11 处 tracingpatient_service949 行0 处 |
| 基础模块 ErpModule trait 实现度低 | LOW | 5/8 模块使用默认值auth/config/workflow/message/plugin |
## 7. 变更记录
| 日期 | 变更 |
|------|------|
| 2026-05-19 | 新增 §4 DTO 输入校验规范44 处校验缺失修复总结 + 强制铁律 + 新增 DTO 检查清单 |
| 2026-05-01 | 审计数据更新模块状态表刷新772 测试 / 328 路由 / 50 权限码)、审计发现清单 |
| 2026-04-26 | 从 CLAUDE.md 迁移目录结构、模块开发规范§5、安全注意事项§7 |
| 2026-04-25 | 全面更新6 模块已实现状态表、预约 CAS 决策、PII 加密不变量、健康模块集成 |
| 2026-04-23 | 重构为 5 节结构,删除 erp-common 引用,精简技术选型表 |