Phase 6 功能补全: - P1-3: 消息 SSE 实时推送端点 + 前端 EventSource 连接 - P1-6: ServiceTask HTTP 调用能力 (reqwest GET/POST) - P1-7: user.deleted 事件处理 — 终止相关流程实例 - P1-8: 任务认领 (claim) 端点 + handler - P1-9: 超时检查器发布 task.timeout 事件 - P1-15: 组织/部门名称唯一性校验 (create + update) - P1-18: 消息群发 fan-out (role/department/all 批量投递) Phase 7 P3-P4 收尾: - PluginAdmin purge 按钮状态修复 - ChangePassword 最小 8 字符 + 新旧密码不同验证 - AuditLogViewer 用户名缓存 + 扩展资源类型 - InstanceMonitor 通过 definition 缓存解析 node_name - NotificationPreferences DND 时间范围校验
219 lines
8.4 KiB
Markdown
219 lines
8.4 KiB
Markdown
---
|
||
title: 架构决策记录
|
||
updated: 2026-04-25
|
||
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(原生 Rust,18 实体/14 权限/13 页面)★ 已实现
|
||
└── 可选插件: crm, inventory, freelance, itops(WASM)
|
||
```
|
||
|
||
### 为什么 erp-health 用原生模块?
|
||
|
||
医疗业务需要 18 强类型实体、自定义 API(趋势分析/统计报表)、PII 数据加密(AES-256-GCM)、文件上传、未来 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 (L2)
|
||
| | | | |
|
||
+--------------+--------------+--------------+-----------+
|
||
|
|
||
erp-server (L3: 唯一组装点)
|
||
|
|
||
erp-plugin (WASM 插件运行时)
|
||
```
|
||
|
||
**禁止**: L2 间直接依赖 / L1 依赖业务模块 / 绕过事件总线
|
||
|
||
### 模块实现状态
|
||
|
||
| 模块 | 状态 | 实体数 | 权限数 | 页面数 |
|
||
|------|------|--------|--------|--------|
|
||
| erp-auth | ✅ 完成 | 11 表 | - | 用户/角色/组织 |
|
||
| erp-config | ✅ 完成 | 6 表 | - | 设置/字典/菜单 |
|
||
| erp-workflow | ✅ 完成 | 5 表 | - | 工作流管理 |
|
||
| erp-message | ✅ 完成 | 3 表 | - | 消息中心 |
|
||
| erp-plugin | ✅ 完成 | 4 表 | - | 插件管理/市场 |
|
||
| erp-health | ✅ 完成 | 18 表 | 14 | 13 页面 + 11 组件 |
|
||
|
||
### 技术选型
|
||
|
||
| 选择 | 理由 |
|
||
|------|------|
|
||
| Axum 0.8 | Tokio 团队维护,tower 生态,类型安全路由 |
|
||
| SeaORM 1.1 | 异步、类型安全、迁移工具完善 |
|
||
| PostgreSQL 18 | 企业级,JSON 支持,扩展丰富 |
|
||
| Redis 7 | 缓存 + 限流 token bucket |
|
||
| React 19 + Ant Design 6 | 企业后台 UI 标配 |
|
||
| Zustand 5 | 极简状态管理 |
|
||
| Wasmtime 43 | WASM 沙箱,Component Model,Fuel 限制 |
|
||
|
||
### 集成契约
|
||
|
||
| 方向 | 模块 | 触发时机 |
|
||
|------|------|---------|
|
||
| 定义 → | [[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
|
||
- CORS — 白名单制,默认拒绝
|
||
- 审计日志 — 所有关键操作记录变更前后状态
|
||
- 动态表 SQL — 使用 `sanitize_identifier` 防注入
|
||
|
||
## 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 — 多租户场景需设计解析策略
|
||
|
||
## 7. 变更记录
|
||
|
||
| 日期 | 变更 |
|
||
|------|------|
|
||
| 2026-04-26 | 从 CLAUDE.md 迁移:目录结构、模块开发规范(§5)、安全注意事项(§7) |
|
||
| 2026-04-25 | 全面更新:6 模块已实现状态表、预约 CAS 决策、PII 加密不变量、健康模块集成 |
|
||
| 2026-04-23 | 重构为 5 节结构,删除 erp-common 引用,精简技术选型表 |
|