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:
@@ -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 + 进销存插件 + 热更新 + 文档清理)。调整为两个子阶段:
|
||||
|
||||
- **Q4a(9-10月)**:测试基础设施 — Testcontainers 集成测试框架 + Playwright E2E + 文档清理
|
||||
- **Q4b(11月+)**:插件生态 — 进销存插件 + 热更新
|
||||
|
||||
热更新功能可视 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 11,Testcontainers 对 Windows 支持有限。本地开发可依赖 CI service container 运行集成测试,或使用 WSL2 环境 |
|
||||
| 进销存插件实体设计变更 | 中 | 低 | 先完成最小实体集,后续迭代扩展 |
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user