Files
erp/docs/superpowers/specs/2026-04-17-platform-maturity-roadmap-design.md
iven b6c4e14b58 docs: 修订成熟度路线图 — 修复规格审查发现的 15 个问题
关键修正:
- ErpModule trait 基于实际签名设计,使用双路由模式
- CI/CD 添加 checkout/cache 步骤
- 环境变量名与现有代码一致 (ERP__SUPER_ADMIN_PASSWORD)
- .test_token 标记为需 BFG 清理
- Q4 拆分为 Q4a(测试) + Q4b(插件)
- 热更新添加回滚策略
- 添加 Windows Testcontainers 兼容性风险
2026-04-17 16:07:37 +08:00

457 lines
20 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.

# ERP 平台底座 — 全面成熟度提升路线图
> 创建日期2026-04-17
> 状态:审查修订完成
> 范围:安全、架构、测试、前端体验、插件生态 — 3 季度分层推进
---
## 1. 背景与目标
### 1.1 项目现状
ERP 平台底座已完成 Phase 1-6 基础设施建设 + WASM 插件系统集成 + CRM 客户管理插件。当前具备:
- 6 个业务模块auth, config, workflow, message, plugin, server
- 36 个数据库迁移
- 完整的 WASM 插件运行时
- Schema 驱动的动态前端6 种页面类型)
- React 19 + Ant Design 6 + Zustand 5 前端 SPA
### 1.2 分析发现摘要
| 维度 | 评分 | 关键问题 |
|------|------|---------|
| 架构健壮性 | 8/10 | ErpModule trait 死代码、路由注册未自动化 |
| 代码质量 | 7/10 | N+1 查询、错误映射过宽、 oversized 组件 |
| 安全性 | 5/10 | 3 个 CRITICAL硬编码密钥/密码、4 个 HIGH |
| 测试覆盖 | 4/10 | 零数据库集成测试、关键流程未覆盖 |
| 前端体验 | 7/10 | 无 i18n、无 Error Boundary、无虚拟滚动 |
| 基础设施 | 4/10 | 无 CI/CD、Wiki 过时、大量未跟踪文件 |
### 1.3 目标
通过 3 个季度的分层改进,将平台从"功能完整"推进到"生产就绪"
- **Q24-5月**:消除安全风险,建立自动化质量门
- **Q36-8月**:强化架构,提升前端工程化水平
- **Q49-11月**:补齐测试覆盖,扩展插件生态
### 1.4 约束
- **独立开发者** + Claude 辅助 — 每季度聚焦单一维度
- **SaaS 优先**部署 — 多租户安全是硬性要求
- **不破坏现有功能** — 所有改进必须向后兼容
---
## 2. Q2安全地基 + CI/CD4-5月
### 2.1 密钥外部化与启动强制检查
**问题:**
- JWT 密钥 `"change-me-in-production"` 硬编码在 `crates/erp-server/config/default.toml`
- 管理员密码 `"Admin@2026"` 硬编码 + fallback
- 数据库凭据 `postgres://erp:erp_dev_2024@...` 硬编码
- `.test_token` 含有效 admin JWT 提交到仓库
**方案:**
1. **配置强制化**`default.toml` 只保留开发环境默认值。生产敏感值通过环境变量 `ERP__` 前缀注入(已有机制)
2. **启动检查**:服务启动时检测 JWT 密钥是否为默认值,若是则 **拒绝启动**(返回错误退出码,不只是警告)
3. **密码初始化**`seed_tenant_auth` 从环境变量 `ERP__SUPER_ADMIN_PASSWORD` 读取初始密码(与现有 `module.rs:149` 中的变量名一致),未设置则拒绝初始化(移除 fallback 到硬编码值的逻辑)
4. **清理 `.test_token`**:立即加入 `.gitignore`。验证该文件是否曾被提交到 git 历史 — 如果曾提交,需使用 BFG Repo-Cleaner 清理历史(因包含用硬编码密钥签名的 admin JWT等同于密钥泄露
5. **`default.toml` 占位符**:敏感字段改为 `"__MUST_SET_VIA_ENV__"` 之类的明显占位值
**验证标准:**
- 默认配置启动时服务拒绝运行
- 环境变量设置后正常启动
- `.test_token` 不再出现在仓库中
### 2.2 Gitea Actions CI/CD
**流水线设计:**
```yaml
name: CI
on: [push, pull_request]
jobs:
rust-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --check --all
- run: cargo clippy -- -D warnings
rust-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env: { POSTGRES_DB: erp_test, POSTGRES_USER: test, POSTGRES_PASSWORD: test }
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace
frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: cd apps/web && pnpm install && pnpm build
security-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo audit
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: cd apps/web && pnpm audit
```
**关键决策:**
- 使用 Gitea Actions与 GitHub Actions 语法兼容)
- 每个 job 包含 `actions/checkout@v4` + 对应语言 toolchain setup
- Rust 使用 `Swatinem/rust-cache@v2` 缓存编译产物,避免每次全量编译
- PostgreSQL 通过 service 容器提供
- 四个 job 并行运行,互不依赖
- 后续可扩展Redis service、Playwright E2E、Docker 镜像构建推送
### 2.3 审计日志补全
**当前缺口与改进:**
| 缺口 | 改进方案 |
|------|---------|
| 登录/登出只发 DomainEvent不写审计日志 | 在 `auth_service` 的 login/logout/change_password 中调用 `audit_service::record()` |
| 审计日志缺少 `old_value`/`new_value` | 关键实体user/role/permission/org的 update 操作添加 `.with_changes(old, new)`。序列化完整的旧模型和新模型为 JSON由审计日志消费者计算 diff — 比应用层计算细粒度 diff 更简单健壮 |
| 缺少 IP 地址和 User-Agent | `AuditLogBuilder::with_request_info()` 在 handler 层传入请求上下文 |
| 插件 CRUD 无审计 | `data_service` 的 create/update/delete 操作添加审计日志记录 |
| 登录失败无记录 | 添加失败登录审计(含尝试的用户名/IP用于入侵检测 |
**验证标准:**
- 登录成功/失败均写入审计日志
- 用户更新操作记录变更前后值
- 审计日志包含 IP 和 User-Agent
### 2.4 Docker 生产化
| 改进项 | 当前 | 目标 |
|--------|------|------|
| PostgreSQL 端口 | `ports: "5432:5432"` 暴露到宿主机 | 移除 `ports:`,使用 Docker 网络内部通信 |
| Redis 端口 | `ports: "6379:6379"` 无认证 | 移除 `ports:`,添加 `--requirepass` |
| 容器资源限制 | 无 | CPU 1核 / 内存 512MB |
| 应用镜像 | 无 Dockerfile | 多阶段构建Rust build → 精简 runtime 镜像 |
| Redis 宕机时限流 | fail-open无限流 | fail-closed拒绝请求 |
**限流 fail-closed 改动:**
`crates/erp-server/src/middleware/rate_limit.rs` 中 Redis 不可用时,返回 `429 Too Many Requests` 而非放行。
### 2.5 多租户安全加固
| 问题 | 改进方案 |
|------|---------|
| 登录使用硬编码 `default_tenant_id` | 登录接口增加租户解析(从子域名/请求头 `X-Tenant-ID` |
| `auth_service::refresh()` 用户查询缺少 tenant_id`auth_service.rs:177` | `find_by_id` 添加 `.filter(user::Column::TenantId.eq(claims.tenant_id))` |
| 内存级 tenant_id 过滤(`user_service.rs``get_by_id`/`update`/`delete` | 改为数据库级 `.filter(Column::TenantId.eq(tenant_id))` 查询。注意:`login`/`list`/`assign_roles` 已正确使用数据库级过滤,无需修改 |
**涉及文件:**
- `crates/erp-auth/src/handler/auth_handler.rs`
- `crates/erp-auth/src/service/auth_service.rs`
- `crates/erp-auth/src/service/user_service.rs`
- `crates/erp-auth/src/middleware/jwt_auth.rs`
---
## 3. Q3架构强化 + 前端体验6-8月
### 3.1 ErpModule Trait 重构
**当前问题:**
- `register_event_handlers` 是死代码 — 所有模块实现为空操作
- 路由注册需在 `main.rs` 手动编辑两处
- 事件订阅在 `main.rs` 中手动调用,绕过 trait
**改进方案:**
基于当前 trait 签名(`erp-core/src/module.rs`),新增双路由注册和权限声明。保持与现有 `ModuleContext` 参数一致,不引入 `AppState` 依赖(避免 `erp-core``erp-server` 反向依赖):
```rust
pub trait ErpModule: Send + Sync + 'static {
// 保留已有方法
fn name(&self) -> &str;
fn version(&self) -> &str;
fn module_type(&self) -> &str { "business" }
fn dependencies(&self) -> Vec<&str> { vec![] }
fn id(&self) -> Uuid { /* 默认实现 */ }
fn as_any(&self) -> &dyn Any;
// 新增:双路由注册(匹配现有 public/protected 分离模式)
fn register_public_routes(&self, router: Router) -> Router { router }
fn register_protected_routes(&self, router: Router) -> Router { router }
// 重构:事件订阅真正生效(当前所有模块实现为空操作)
fn register_event_handlers(&self, bus: &EventBus) {}
// 新增:模块权限声明
fn permissions(&self) -> Vec<PermissionDef> { vec![] }
// 保留已有生命周期钩子(保持 ModuleContext 参数签名)
async fn on_startup(&self, _ctx: &ModuleContext) -> AppResult<()> { Ok(()) }
async fn on_shutdown(&self) -> AppResult<()> { Ok(()) }
async fn on_tenant_created(&self, _tenant_id: Uuid, _db: &DatabaseConnection, _bus: &EventBus) -> AppResult<()> { Ok(()) }
async fn on_tenant_deleted(&self, _tenant_id: Uuid, _db: &DatabaseConnection, _bus: &EventBus) -> AppResult<()> { Ok(()) }
async fn health_check(&self) -> AppResult<serde_json::Value> { Ok(serde_json::json!({})) }
}
```
`ModuleRegistry::build()` 自动收集路由、事件处理器和权限,`main.rs` 简化为:
```rust
let (registry, public_routes, protected_routes) = ModuleRegistry::new()
.register(auth_module)
.register(config_module)
.register(workflow_module)
.register(message_module)
.register(plugin_module)
.build();
// 自动组合public_routes 直接挂载protected_routes 包裹 JWT 中间件
let app = Router::new()
.merge(public_routes)
.merge(protected_routes.layer(jwt_middleware));
```
**迁移策略:** 逐模块迁移 — 每个模块从静态 `public_routes()`/`protected_routes()` 函数改为 trait 方法实现,`main.rs` 逐步简化。
**已知例外:** PluginModule 的两阶段初始化(先注册再启动事件监听器)在初期保持独立处理,不强行纳入自动化。`MessageModule::start_event_listener``WorkflowModule::start_timeout_checker``outbox::start_outbox_relay` 等独立生命周期钩子作为范围排除项,后续迭代再统一。
**迁移策略:** 逐模块迁移 — 每个模块从静态函数改为 trait 方法实现,`main.rs` 逐步简化。
### 3.2 错误映射修正 + N+1 查询优化
**错误映射修正:**
当前 `erp-auth` 服务中直接 `.map_err(|e| AuthError::Validation(e.to_string()))` 将所有 `DbErr` 映射为 `Validation`,绕过了 `erp-core` 中已有的 `From<DbErr> for AppError` 语义映射(该映射已正确处理 `RecordNotFound``NotFound`、重复键 → `Conflict`)。
**修复策略:** `erp-auth` 服务层停止手动包装 `DbErr`,改为通过 `?` 操作符依赖 `DbErr → AppError` 的核心映射,通过现有的 `From<AuthError> for AppError` 转换传播。这样数据库连接错误会正确显示为 `Internal`,唯一约束冲突会正确显示为 `Conflict`
移除 `From<AppError> for AuthError` 的反向映射(当前是 lossy wrapping — `AppError::NotFound` 变为 `AuthError::Validation`,丢失语义信息)。
**N+1 查询优化:**
`user_service.rs``list()` 方法改为批量查询:
1. 先查询当前页用户列表
2. 收集所有 `user_id`
3. 一次 `WHERE user_id IN (...)` 查询 `user_role` + `role`
4. 内存中按 `user_id` 分组组装
从 N+1 查询降为 3 次固定查询(用户列表 + 角色关联 + 角色详情)。
### 3.3 前端 Error Boundary + hooks 提取
**Error Boundary**
- `App.tsx` 根组件包裹全局 Error Boundary捕获未预期崩溃
- 每个懒加载页面外包裹页面级 Error Boundary隔离单页面崩溃
- 失败时展示友好错误页面 + 重试按钮
**hooks 提取:**
| Hook | 提取来源 | 用途 |
|------|---------|------|
| `usePaginatedData<T>` | 6+ 页面的分页加载逻辑 | 统一分页/搜索/加载状态 |
| `useDarkMode` | 8+ 文件的 `token.colorBgContainer` 字符串比较 | 提供可靠的 boolean 暗色模式判断 |
| `useCountUp` | Home.tsx + DashboardWidgets 重复实现 | 计数动画复用 |
| `useDebouncedValue` | Users.tsx 等搜索输入 | 防抖搜索,避免每次按键触发 API |
| `useApiRequest` | 所有页面的 try/catch + message.error | 统一 API 错误处理和消息提示 |
### 3.4 i18n 基础设施搭建
**方案react-i18next**
- 安装 `react-i18next` + `i18next`
- 创建 `locales/zh-CN.json`,提取所有硬编码中文为 key
- 配置 i18next 初始化,默认 `zh-CN`
-`useTranslation()` hook 替换硬编码字符串
**实施策略:** 增量式 — 新页面强制使用 i18n旧页面按模块逐步迁移。不强求一次性替换。
**命名规范:**
- 页面文案:`{module}.{page}.{element}``auth.login.username`
- 通用文案:`common.{action}``common.save`, `common.cancel`
- 错误消息:`error.{type}``error.network`, `error.unauthorized`
### 3.5 行级数据权限接线
**当前状态:** 数据库列、SQL 条件构建器、manifest 声明已就绪handler 层有 TODO 未实现。
**完成步骤:**
1. JWT 中间件注入 `department_ids`(完成 `jwt_auth.rs:50` 的 TODO
2. `data_handler` 查询接口注入 data scope 条件
3. 前端角色权限编辑页添加 `data_scope` 选择控件
4. 端到端验证:创建测试角色 → 设置数据范围 → 验证查询过滤
### 3.6 前端共享类型统一
- `PaginatedResponse<T>``users.ts` 提取到 `api/types.ts`
- 错误提取工具函数 `extractErrorMessage(err: unknown): string``api/errors.ts`
- 插件 Schema 类型定义集中到 `types/plugin.ts`
- 移除 `api/client.ts` 中已废弃的 `CancelToken`,改用 `AbortController`
---
## 4. Q4测试覆盖 + 插件生态9-11月
### 4.1 Q4 范围调整说明
Q4 原始范围较大Testcontainers + Playwright + 进销存插件 + 热更新 + 文档清理)。调整为两个子阶段:
- **Q4a9-10月**:测试基础设施 — Testcontainers 集成测试框架 + Playwright E2E + 文档清理
- **Q4b11月+**:插件生态 — 进销存插件 + 热更新
热更新功能可视 Q4a 进度推迟到 Q1 2027避免在单季度内承载过多工作。
### 4.2 数据库集成测试框架
**方案Testcontainers + PostgreSQL**
创建 `crates/erp-server/tests/integration/` 目录,使用 `testcontainers` crate 启动真实 PostgreSQL 容器。
**测试基座:**
- 每个测试套件共享一个 PostgreSQL 容器
- 自动运行所有迁移
- 提供 `setup_test_db()` 辅助函数返回连接池
- 测试结束自动清理
**覆盖优先级:**
| 优先级 | 模块 | 测试场景 |
|--------|------|---------|
| P0 | erp-auth | 用户 CRUD、角色权限分配、登录/JWT 完整流程 |
| P0 | erp-auth | 多租户隔离 — 租户 A 数据对租户 B 不可见 |
| P0 | erp-plugin | 插件生命周期install→enable→disable→uninstall |
| P1 | erp-auth | 乐观锁并发冲突、软删除恢复 |
| P1 | erp-plugin | 行级数据权限过滤、JSONB 查询/索引 |
| P1 | erp-plugin | 动态表 DDL 正确性generated column、pg_trgm 索引) |
| P1 | erp-workflow | 流程实例启动、任务完成、网关分支 |
| P1 | erp-core | 事件总线发布/订阅端到端、outbox relay 补偿 |
### 4.2 核心流程 E2E 测试
**方案Playwright**
放在 `apps/web/e2e/` 目录CI 中作为独立 job 运行。
**覆盖场景4 个关键旅程):**
| 场景 | 步骤 |
|------|------|
| 完整登录流程 | 打开登录页 → 输入密码 → 验证 token → 刷新 token → 登出 → 验证跳转 |
| 用户管理闭环 | 创建用户 → 分配角色 → 搜索用户 → 编辑 → 软删除 → 验证列表不显示 |
| 插件安装流程 | 上传 WASM → 安装 → 验证菜单出现 → 数据 CRUD → 卸载 → 验证菜单消失 |
| 多租户隔离 | 租户 A 创建用户 → 切换租户 B → 验证查询结果为空 |
### 4.3 第二个行业插件 — 进销存Inventory
**选择理由:**
- 与 CRM 有天然关联(客户 → 订单 → 出库)
- 实体数量适中5-8 个),复杂度可控
- 能验证插件系统的复用性和跨实体关联能力
- 为后续财务模块铺垫
**实体设计:**
| 实体 | 字段 | 关联 |
|------|------|------|
| product 商品 | 名称/编码/规格/单位/分类/售价/成本价 | — |
| warehouse 仓库 | 名称/地址/负责人/状态 | — |
| stock 库存 | 商品/仓库/数量/成本/预警线 | → product, warehouse |
| purchase_order 采购单 | 供应商/总金额/状态/日期 | → supplier(CRM), stock |
| sales_order 销售单 | 客户/总金额/状态/日期 | → customer(CRM), stock |
| supplier 供应商 | 名称/编码/联系方式/地址 | — |
**需要验证的插件能力:**
- 跨实体关联(订单 → 商品 → 库存联动)
- 事务性事件(库存扣减在订单确认时原子执行)
- 页面间导航(从订单跳转客户详情)
- 报表/统计页面(库存汇总、进销存明细)
### 4.4 插件热更新能力
**当前限制:** 更新插件需要完整 uninstall/reinstall。
**改进方案:**
- 新增 `POST /api/v1/admin/plugins/{id}/upgrade` 端点
- 升级流程:上传新 WASM → 对比 manifest schema → 增量 DDLADD COLUMN 等) → 热替换 WASM 模块
- 数据安全:`tenant_id` 数据不丢失
- 版本兼容性检查:新版本必须向后兼容或提供迁移脚本
**回滚策略:** 升级前创建 schema 备份点。升级流程分两步执行:
1. 先暂存新 WASM 并尝试验证初始化(不应用 DDL
2. 初始化成功后,在单事务中执行 DDL 变更 + 状态转换
3. 如果新 WASM 初始化失败,保持旧 WASM 继续运行,回滚暂存状态
4. DDL 已应用但 WASM 运行异常时,保留旧 WASM 可加载作为 fallback
### 4.5 文档更新与清理
| 项目 | 改进 |
|------|------|
| Wiki 文档 | 全面更新到当前状态(前端路由、测试数量、模块能力、插件系统) |
| CLAUDE.md | 版本号修正React 19 / Ant Design 6 |
| 根目录清理 | 删除未跟踪的开发临时文件截图、heap dump、perf trace、agent plan 文件) |
| integration-tests/ | 验证现有测试是否能编译。若已失效则删除,用新的 Testcontainers 框架替代;若仍有效则纳入 Cargo workspace |
| N+1 查询plugin | `plugin_service.rs` 的列表查询也存在 N+1 问题(每条插件单独查询 entities需一并优化 |
---
## 5. 风险与缓解
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| 安全修复引入新 bug | 中 | 高 | 每个修复配有对应的测试用例 |
| ErpModule trait 重构影响所有模块 | 高 | 中 | 逐模块迁移,每步验证 `cargo test` |
| i18n 迁移工作量大 | 中 | 低 | 增量式,不追求一次性完成 |
| Testcontainers 在 CI 环境不稳定 | 低 | 中 | 本地开发可跳过集成测试CI 用 service container 兜底 |
| Testcontainers 在 Windows (WSL2) 上兼容性 | 中 | 中 | 主开发环境为 Windows 11Testcontainers 对 Windows 支持有限。本地开发可依赖 CI service container 运行集成测试,或使用 WSL2 环境 |
| 进销存插件实体设计变更 | 中 | 低 | 先完成最小实体集,后续迭代扩展 |
---
## 6. 成功标准
**Q2 完成标准:**
- [ ] 3 个 CRITICAL 安全问题全部修复
- [ ] Gitea Actions CI/CD 流水线运行通过
- [ ] 默认配置启动被拒绝
- [ ] 登录/登出写入审计日志
- [ ] Docker 生产化配置就绪
**Q3 完成标准:**
- [ ] ErpModule trait 路由注册自动化
- [ ] N+1 查询优化,用户列表查询次数固定为 3
- [ ] 前端 Error Boundary 覆盖全局 + 页面级
- [ ] 5 个自定义 hooks 提取完成
- [ ] i18n 基础设施可用,至少 1 个页面完成迁移
- [ ] 行级数据权限端到端验证通过
**Q4 完成标准:**
- [ ] 集成测试覆盖 auth + plugin 核心流程
- [ ] 4 个 E2E 测试场景通过
- [ ] 进销存插件 6 个实体可用
- [ ] 插件热更新功能可用
- [ ] Wiki 文档与代码同步