Files
erp/wiki/architecture.md
iven 9568dd7875 chore: apply cargo fmt across workspace and update docs
- 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)
2026-04-15 00:49:20 +08:00

126 lines
6.3 KiB
Markdown
Raw Permalink 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.

# 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]]** — 插件扩展架构的实现