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

6.3 KiB
Raw Blame History

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

关联模块