docs: 修订成熟度路线图 — 修复规格审查发现的 15 个问题

关键修正:
- ErpModule trait 基于实际签名设计,使用双路由模式
- CI/CD 添加 checkout/cache 步骤
- 环境变量名与现有代码一致 (ERP__SUPER_ADMIN_PASSWORD)
- .test_token 标记为需 BFG 清理
- Q4 拆分为 Q4a(测试) + Q4b(插件)
- 热更新添加回滚策略
- 添加 Windows Testcontainers 兼容性风险
This commit is contained in:
iven
2026-04-17 16:07:37 +08:00
parent 432eb2f9f5
commit b6c4e14b58

View File

@@ -1,7 +1,7 @@
# ERP 平台底座 — 全面成熟度提升路线图
> 创建日期2026-04-17
> 状态:已批准
> 状态:审查修订完成
> 范围:安全、架构、测试、前端体验、插件生态 — 3 季度分层推进
---
@@ -59,8 +59,8 @@ ERP 平台底座已完成 Phase 1-6 基础设施建设 + WASM 插件系统集成
1. **配置强制化**`default.toml` 只保留开发环境默认值。生产敏感值通过环境变量 `ERP__` 前缀注入(已有机制)
2. **启动检查**:服务启动时检测 JWT 密钥是否为默认值,若是则 **拒绝启动**(返回错误退出码,不只是警告)
3. **密码初始化**`seed_tenant_auth` 从环境变量 `ERP__AUTH__ADMIN_PASSWORD` 读取初始密码,未设置则拒绝初始化(移除 fallback 到硬编码值的逻辑)
4. **清理 `.test_token`**:加入 `.gitignore`,从 git 历史中移除
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__"` 之类的明显占位值
**验证标准:**
@@ -80,8 +80,11 @@ jobs:
rust-check:
runs-on: ubuntu-latest
steps:
- cargo fmt --check --all
- cargo clippy -- -D warnings
- 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
@@ -96,22 +99,35 @@ jobs:
--health-timeout 5s
--health-retries 5
steps:
- cargo test --workspace
- 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:
- cd apps/web && pnpm install && pnpm build
- 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:
- cargo audit
- cd apps/web && pnpm audit
- 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 service 容器提供 PostgreSQL
- 使用 Gitea Actions(与 GitHub Actions 语法兼容)
- 每个 job 包含 `actions/checkout@v4` + 对应语言 toolchain setup
- Rust 使用 `Swatinem/rust-cache@v2` 缓存编译产物,避免每次全量编译
- PostgreSQL 通过 service 容器提供
- 四个 job 并行运行,互不依赖
- 后续可扩展Redis service、Playwright E2E、Docker 镜像构建推送
@@ -122,7 +138,7 @@ jobs:
| 缺口 | 改进方案 |
|------|---------|
| 登录/登出只发 DomainEvent不写审计日志 | 在 `auth_service` 的 login/logout/change_password 中调用 `audit_service::record()` |
| 审计日志缺少 `old_value`/`new_value` | 关键实体user/role/permission/org的 update 操作添加 `.with_changes(old, new)` |
| 审计日志缺少 `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用于入侵检测 |
@@ -150,8 +166,8 @@ jobs:
| 问题 | 改进方案 |
|------|---------|
| 登录使用硬编码 `default_tenant_id` | 登录接口增加租户解析(从子域名/请求头 `X-Tenant-ID` |
| `auth_service::refresh()` 用户查询缺少 tenant_id | `find_by_id` 添加 `.filter(user::Column::TenantId.eq(claims.tenant_id))` |
| 内存级 tenant_id 过滤 | 改为数据库级 `.filter(Column::TenantId.eq(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`
@@ -172,59 +188,69 @@ jobs:
**改进方案:**
重构 `ErpModule` 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;
// 新增:路由注册自动化
fn register_routes(&self, router: Router) -> Router { router }
// 新增:路由注册(匹配现有 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![] }
// 保留已有生命周期钩子
async fn on_startup(&self, _state: &AppState) -> AppResult<()> { Ok(()) }
// 保留已有生命周期钩子(保持 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, _state: &AppState) -> AppResult<()> { Ok(()) }
async fn on_tenant_deleted(&self, _tenant_id: Uuid, _state: &AppState) -> AppResult<()> { Ok(()) }
async fn health_check(&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()` 自动收集路由、事件处理器和权限:
`ModuleRegistry::build()` 自动收集路由、事件处理器和权限`main.rs` 简化为
```rust
let (registry, router) = ModuleRegistry::new()
let (registry, public_routes, protected_routes) = ModuleRegistry::new()
.register(auth_module)
.register(config_module)
.register(workflow_module)
.register(message_module)
.register(plugin_module)
.build(router);
.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 查询优化
**错误映射修正:**
当前 `DbErr` 统一映射为 `AuthError::Validation`,掩盖了连接失败和约束冲突
当前 `erp-auth` 服务中直接 `.map_err(|e| AuthError::Validation(e.to_string()))` 将所有 `DbErr` 映射为 `Validation`,绕过了 `erp-core` 中已有的 `From<DbErr> for AppError` 语义映射(该映射已正确处理 `RecordNotFound``NotFound`、重复键 → `Conflict`
改为语义化映射:
- `DbErr::ConnectionErr``AppError::Internal("数据库连接失败")`
- `DbErr::RecordNotFound``AppError::NotFound`
- `DbErr` + 消息含 `"duplicate key"``AppError::Conflict("记录已存在")`
- `DbErr` + 消息含 `"foreign key"``AppError::Validation("关联数据不存在")`
**修复策略:** `erp-auth` 服务层停止手动包装 `DbErr`,改为通过 `?` 操作符依赖 `DbErr → AppError` 的核心映射,通过现有的 `From<AuthError> for AppError` 转换传播。这样数据库连接错误会正确显示为 `Internal`,唯一约束冲突会正确显示为 `Conflict`
移除 `From<AppError> for AuthError` 的反向映射lossy wrapping
移除 `From<AppError> for AuthError` 的反向映射(当前是 lossy wrapping`AppError::NotFound` 变为 `AuthError::Validation`,丢失语义信息)。
**N+1 查询优化:**
@@ -290,7 +316,16 @@ let (registry, router) = ModuleRegistry::new()
## 4. Q4测试覆盖 + 插件生态9-11月
### 4.1 数据库集成测试框架
### 4.1 Q4 范围调整说明
Q4 原始范围较大Testcontainers + Playwright + 进销存插件 + 热更新 + 文档清理)。调整为两个子阶段:
- **Q4a9-10月**:测试基础设施 — Testcontainers 集成测试框架 + Playwright E2E + 文档清理
- **Q4b11月+**:插件生态 — 进销存插件 + 热更新
热更新功能可视 Q4a 进度推迟到 Q1 2027避免在单季度内承载过多工作。
### 4.2 数据库集成测试框架
**方案Testcontainers + PostgreSQL**
@@ -365,6 +400,12 @@ let (registry, router) = ModuleRegistry::new()
- 数据安全:`tenant_id` 数据不丢失
- 版本兼容性检查:新版本必须向后兼容或提供迁移脚本
**回滚策略:** 升级前创建 schema 备份点。升级流程分两步执行:
1. 先暂存新 WASM 并尝试验证初始化(不应用 DDL
2. 初始化成功后,在单事务中执行 DDL 变更 + 状态转换
3. 如果新 WASM 初始化失败,保持旧 WASM 继续运行,回滚暂存状态
4. DDL 已应用但 WASM 运行异常时,保留旧 WASM 可加载作为 fallback
### 4.5 文档更新与清理
| 项目 | 改进 |
@@ -372,8 +413,8 @@ let (registry, router) = ModuleRegistry::new()
| Wiki 文档 | 全面更新到当前状态(前端路由、测试数量、模块能力、插件系统) |
| CLAUDE.md | 版本号修正React 19 / Ant Design 6 |
| 根目录清理 | 删除未跟踪的开发临时文件截图、heap dump、perf trace、agent plan 文件) |
| integration-tests/ | 纳入 Cargo workspace 或合并到 `crates/erp-server/tests/` |
| `erp-common` 引用 | 从文档中移除不存在的 crate 引用 |
| integration-tests/ | 验证现有测试是否能编译。若已失效则删除,用新的 Testcontainers 框架替代;若仍有效则纳入 Cargo workspace |
| N+1 查询plugin | `plugin_service.rs` 的列表查询也存在 N+1 问题(每条插件单独查询 entities需一并优化 |
---
@@ -385,6 +426,7 @@ let (registry, router) = ModuleRegistry::new()
| ErpModule trait 重构影响所有模块 | 高 | 中 | 逐模块迁移,每步验证 `cargo test` |
| i18n 迁移工作量大 | 中 | 低 | 增量式,不追求一次性完成 |
| Testcontainers 在 CI 环境不稳定 | 低 | 中 | 本地开发可跳过集成测试CI 用 service container 兜底 |
| Testcontainers 在 Windows (WSL2) 上兼容性 | 中 | 中 | 主开发环境为 Windows 11Testcontainers 对 Windows 支持有限。本地开发可依赖 CI service container 运行集成测试,或使用 WSL2 环境 |
| 进销存插件实体设计变更 | 中 | 低 | 先完成最小实体集,后续迭代扩展 |
---