From b6c4e14b587c639e180899b633ee987ac62c26b2 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 16:07:37 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=BF=AE=E8=AE=A2=E6=88=90=E7=86=9F?= =?UTF-8?q?=E5=BA=A6=E8=B7=AF=E7=BA=BF=E5=9B=BE=20=E2=80=94=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=A7=84=E6=A0=BC=E5=AE=A1=E6=9F=A5=E5=8F=91=E7=8E=B0?= =?UTF-8?q?=E7=9A=84=2015=20=E4=B8=AA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 关键修正: - ErpModule trait 基于实际签名设计,使用双路由模式 - CI/CD 添加 checkout/cache 步骤 - 环境变量名与现有代码一致 (ERP__SUPER_ADMIN_PASSWORD) - .test_token 标记为需 BFG 清理 - Q4 拆分为 Q4a(测试) + Q4b(插件) - 热更新添加回滚策略 - 添加 Windows Testcontainers 兼容性风险 --- ...-04-17-platform-maturity-roadmap-design.md | 112 ++++++++++++------ 1 file changed, 77 insertions(+), 35 deletions(-) diff --git a/docs/superpowers/specs/2026-04-17-platform-maturity-roadmap-design.md b/docs/superpowers/specs/2026-04-17-platform-maturity-roadmap-design.md index 1a5ab0b..8387771 100644 --- a/docs/superpowers/specs/2026-04-17-platform-maturity-roadmap-design.md +++ b/docs/superpowers/specs/2026-04-17-platform-maturity-roadmap-design.md @@ -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 { 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 { 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 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 for AppError` 转换传播。这样数据库连接错误会正确显示为 `Internal`,唯一约束冲突会正确显示为 `Conflict`。 -移除 `From for AuthError` 的反向映射(lossy wrapping)。 +移除 `From 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 环境 | | 进销存插件实体设计变更 | 中 | 低 | 先完成最小实体集,后续迭代扩展 | ---