Files
erp/wiki/architecture.md
iven 5901ee82f0 feat: complete Phase 1 infrastructure
- erp-core: error types, shared types, event bus, ErpModule trait
- erp-server: config loading, database/Redis connections, migrations
- erp-server/migration: tenants table with SeaORM
- apps/web: Vite + React 18 + TypeScript + Ant Design 5 + TailwindCSS
- Web frontend: main layout with sidebar, header, routing
- Docker: PostgreSQL 16 + Redis 7 development environment
- All workspace crates compile successfully (cargo check passes)
2026-04-11 01:07:31 +08:00

85 lines
3.7 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.

# 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 文档,零额外维护 |
## 关联模块
- **[[erp-core]]** — 架构契约的定义者
- **[[erp-server]]** — 架构的组装执行者
- **[[database]]** — 多租户隔离的物理实现