- Run cargo fmt on all Rust crates for consistent formatting - Update CLAUDE.md with WASM plugin commands and dev.ps1 instructions - Update wiki: add WASM plugin architecture, rewrite dev environment docs - Minor frontend cleanup (unused imports)
126 lines
6.3 KiB
Markdown
126 lines
6.3 KiB
Markdown
# architecture (架构决策记录)
|
||
|
||
## 设计思想
|
||
|
||
ERP 平台采用 **模块化单体 + 渐进式拆分** 架构。核心原则:模块间零直接依赖,所有跨模块通信通过事件总线和 trait 接口。
|
||
|
||
## 关键架构决策
|
||
|
||
### Q: 为什么用模块化单体而非微服务?
|
||
|
||
**A:** ERP 系统的模块间数据一致性要求高,分布式事务成本大。单体起步,模块边界清晰,未来需要时可按模块拆分为微服务。`ErpModule` trait 天然支持这种渐进式迁移。
|
||
|
||
### Q: 为什么用 UUIDv7 而不是自增 ID?
|
||
|
||
**A:** UUIDv7 是时间排序的,既有 UUID 的分布式唯一性,又有接近自增 ID 的索引性能。对多租户 SaaS 尤其重要——不同租户的数据不会因为 ID 冲突而互相影响。
|
||
|
||
### Q: 为什么用 broadcast channel 做事件总线?
|
||
|
||
**A:** `tokio::sync::broadcast` 提供多消费者发布/订阅,语义匹配模块间事件通知。设计规格要求 outbox 模式(持久化到 domain_events 表),但当前 Phase 1 先用内存 broadcast,后续再补持久化。
|
||
|
||
### Q: 为什么错误类型跨 crate 边界必须用 thiserror?
|
||
|
||
**A:** `anyhow` 的错误没有类型信息,无法在 API 层做精确的 HTTP 状态码映射。`thiserror` 定义明确的错误变体,`AppError` 可以精确映射到 400/401/403/404/409/500。crate 内部可以用 `anyhow` 简化,但对外必须转 `AppError`。
|
||
|
||
### Q: 为什么 tenant_id 不在 API 路径中?
|
||
|
||
**A:** 从 JWT token 中提取 tenant_id,通过中间件注入 `TenantContext`。这防止了:
|
||
- 用户手动修改 URL 访问其他租户数据
|
||
- API 路径暴露租户信息
|
||
- 开发者忘记检查租户权限
|
||
|
||
管理员接口例外,可以通过路径指定 tenant_id。
|
||
|
||
### Q: 为什么前端用 HashRouter 而非 BrowserRouter?
|
||
|
||
**A:** 部署时可能不在根路径下,HashRouter 不需要服务端配置 fallback 路由。对 SPA 来说更稳健。
|
||
|
||
## 模块依赖铁律
|
||
|
||
```
|
||
erp-core (L1)
|
||
erp-common (L1)
|
||
|
|
||
+--------------+--------------+--------------+
|
||
| | | |
|
||
erp-auth erp-config erp-workflow erp-message (L2)
|
||
| | | |
|
||
+--------------+--------------+--------------+
|
||
|
|
||
erp-server (L3: 唯一组装点)
|
||
```
|
||
|
||
**禁止:**
|
||
- L2 模块之间直接依赖
|
||
- L1 模块依赖任何业务模块
|
||
- 绕过事件总线直接调用其他模块
|
||
|
||
## 多租户隔离策略
|
||
|
||
**当前策略:共享数据库 + tenant_id 列过滤**
|
||
|
||
所有业务表包含 `tenant_id` 列,查询时通过中间件自动注入过滤条件。这是最简单的 SaaS 多租户方案,未来可扩展为:
|
||
- Schema 隔离 — 每个租户独立 schema
|
||
- 数据库隔离 — 每个租户独立数据库(私有化部署)
|
||
|
||
`ErpModule::on_tenant_created()` 和 `on_tenant_deleted()` 钩子确保模块能在租户创建/删除时初始化/清理数据。
|
||
|
||
## 技术选型理由
|
||
|
||
| 选择 | 理由 |
|
||
|------|------|
|
||
| Axum 0.8 | Tokio 团队维护,与 tower 生态无缝集成,类型安全路由 |
|
||
| SeaORM 1.1 | 异步、类型安全、Rust 原生 ORM,迁移工具完善 |
|
||
| PostgreSQL 16 | 企业级关系型数据库,JSON 支持好,扩展丰富 |
|
||
| Redis 7 | 高性能缓存,会话存储,限流 token bucket |
|
||
| React 19 + Ant Design 6 | 成熟的组件库,企业后台 UI 标配 |
|
||
| Zustand | 极简状态管理,无 boilerplate |
|
||
| utoipa | Rust 代码生成 OpenAPI 文档,零额外维护 |
|
||
| Wasmtime 43 | WASM 沙箱运行时,Component Model 支持,Fuel 资源限制 |
|
||
|
||
## 插件扩展架构
|
||
|
||
### Q: 为什么用 WASM 而不是 Lua / gRPC / dylib?
|
||
|
||
**A:**
|
||
|
||
| 方案 | 安全性 | 隔离性 | 性能 | 复杂度 |
|
||
|------|--------|--------|------|--------|
|
||
| **WASM** | 高(沙箱) | 进程内隔离 | 接近原生(JIT) | 中 |
|
||
| Lua 脚本 | 中 | 无隔离 | 快 | 低 |
|
||
| 进程外 gRPC | 高 | 进程级隔离 | 网络开销 | 高 |
|
||
| dylib | 低(直接内存) | 无隔离 | 原生 | 低 |
|
||
|
||
WASM 在安全性和性能之间取得最佳平衡。Wasmtime v43 的 Component Model 提供了类型安全的 Host-Plugin 接口,Fuel 机制防止恶意/有缺陷的插件消耗过多资源。
|
||
|
||
### 插件架构拓扑
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────┐
|
||
│ erp-server │
|
||
│ ┌──────────────┐ ┌──────────────────────────┐ │
|
||
│ │ EventBus │ │ PluginRuntime (Wasmtime) │ │
|
||
│ │ (broadcast) │ │ ┌──────┐ ┌──────┐ │ │
|
||
│ └──────┬───────┘ │ │插件 A│ │插件 B│ │ │
|
||
│ │ │ └──┬───┘ └──┬───┘ │ │
|
||
│ │ │ │ Host API │ │ │
|
||
│ │ │ ┌──┴────────┴──┐ │ │
|
||
│ │ │ │ Host Bridge │ │ │
|
||
│ │ │ └──┬───────────┘ │ │
|
||
│ │ └─────┼────────────────────┘ │
|
||
│ │ │ │
|
||
│ ┌──────┴───────┐ ┌────┴─────┐ │
|
||
│ │ DB (SeaORM) │ │ EventBus │ │
|
||
│ └──────────────┘ └──────────┘ │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|
||
|
||
插件通过 Host Bridge 调用系统功能(db_insert、event_publish 等),Host Bridge 自动注入多租户隔离(tenant_id)和权限检查。详见 [[wasm-plugin]]。
|
||
|
||
## 关联模块
|
||
|
||
- **[[erp-core]]** — 架构契约的定义者
|
||
- **[[erp-server]]** — 架构的组装执行者
|
||
- **[[database]]** — 多租户隔离的物理实现
|
||
- **[[wasm-plugin]]** — 插件扩展架构的实现
|