16 KiB
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 个季度的分层改进,将平台从"功能完整"推进到"生产就绪":
- Q2(4-5月):消除安全风险,建立自动化质量门
- Q3(6-8月):强化架构,提升前端工程化水平
- Q4(9-11月):补齐测试覆盖,扩展插件生态
1.4 约束
- 独立开发者 + Claude 辅助 — 每季度聚焦单一维度
- SaaS 优先部署 — 多租户安全是硬性要求
- 不破坏现有功能 — 所有改进必须向后兼容
2. Q2:安全地基 + CI/CD(4-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 提交到仓库
方案:
- 配置强制化:
default.toml只保留开发环境默认值。生产敏感值通过环境变量ERP__前缀注入(已有机制) - 启动检查:服务启动时检测 JWT 密钥是否为默认值,若是则 拒绝启动(返回错误退出码,不只是警告)
- 密码初始化:
seed_tenant_auth从环境变量ERP__AUTH__ADMIN_PASSWORD读取初始密码,未设置则拒绝初始化(移除 fallback 到硬编码值的逻辑) - 清理
.test_token:加入.gitignore,从 git 历史中移除 default.toml占位符:敏感字段改为"__MUST_SET_VIA_ENV__"之类的明显占位值
验证标准:
- 默认配置启动时服务拒绝运行
- 环境变量设置后正常启动
.test_token不再出现在仓库中
2.2 Gitea Actions CI/CD
流水线设计:
name: CI
on: [push, pull_request]
jobs:
rust-check:
runs-on: ubuntu-latest
steps:
- cargo fmt --check --all
- 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:
- cargo test --workspace
frontend-build:
runs-on: ubuntu-latest
steps:
- cd apps/web && pnpm install && pnpm build
security-audit:
runs-on: ubuntu-latest
steps:
- cargo audit
- cd apps/web && pnpm audit
关键决策:
- 使用 Gitea Actions service 容器提供 PostgreSQL
- 四个 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) |
| 缺少 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 |
find_by_id 添加 .filter(user::Column::TenantId.eq(claims.tenant_id)) |
| 内存级 tenant_id 过滤 | 改为数据库级 .filter(Column::TenantId.eq(tenant_id)) 查询 |
涉及文件:
crates/erp-auth/src/handler/auth_handler.rscrates/erp-auth/src/service/auth_service.rscrates/erp-auth/src/service/user_service.rscrates/erp-auth/src/middleware/jwt_auth.rs
3. Q3:架构强化 + 前端体验(6-8月)
3.1 ErpModule Trait 重构
当前问题:
register_event_handlers是死代码 — 所有模块实现为空操作- 路由注册需在
main.rs手动编辑两处 - 事件订阅在
main.rs中手动调用,绕过 trait
改进方案:
重构 ErpModule trait,新增路由注册和权限声明:
pub trait ErpModule: Send + Sync + 'static {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn dependencies(&self) -> Vec<&str> { vec![] }
// 新增:路由注册自动化
fn register_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(()) }
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(()) }
}
ModuleRegistry::build() 自动收集路由、事件处理器和权限:
let (registry, router) = ModuleRegistry::new()
.register(auth_module)
.register(config_module)
.register(workflow_module)
.register(message_module)
.register(plugin_module)
.build(router);
迁移策略: 逐模块迁移 — 每个模块从静态函数改为 trait 方法实现,main.rs 逐步简化。
3.2 错误映射修正 + N+1 查询优化
错误映射修正:
当前 DbErr 统一映射为 AuthError::Validation,掩盖了连接失败和约束冲突。
改为语义化映射:
DbErr::ConnectionErr→AppError::Internal("数据库连接失败")DbErr::RecordNotFound→AppError::NotFoundDbErr+ 消息含"duplicate key"→AppError::Conflict("记录已存在")DbErr+ 消息含"foreign key"→AppError::Validation("关联数据不存在")
移除 From<AppError> for AuthError 的反向映射(lossy wrapping)。
N+1 查询优化:
user_service.rs 的 list() 方法改为批量查询:
- 先查询当前页用户列表
- 收集所有
user_id - 一次
WHERE user_id IN (...)查询user_role+role - 内存中按
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 未实现。
完成步骤:
- JWT 中间件注入
department_ids(完成jwt_auth.rs:50的 TODO) data_handler查询接口注入 data scope 条件- 前端角色权限编辑页添加
data_scope选择控件 - 端到端验证:创建测试角色 → 设置数据范围 → 验证查询过滤
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 数据库集成测试框架
方案: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 → 增量 DDL(ADD COLUMN 等) → 热替换 WASM 模块
- 数据安全:
tenant_id数据不丢失 - 版本兼容性检查:新版本必须向后兼容或提供迁移脚本
4.5 文档更新与清理
| 项目 | 改进 |
|---|---|
| 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 引用 |
5. 风险与缓解
| 风险 | 概率 | 影响 | 缓解措施 |
|---|---|---|---|
| 安全修复引入新 bug | 中 | 高 | 每个修复配有对应的测试用例 |
| ErpModule trait 重构影响所有模块 | 高 | 中 | 逐模块迁移,每步验证 cargo test |
| i18n 迁移工作量大 | 中 | 低 | 增量式,不追求一次性完成 |
| Testcontainers 在 CI 环境不稳定 | 低 | 中 | 本地开发可跳过集成测试,CI 用 service container 兜底 |
| 进销存插件实体设计变更 | 中 | 低 | 先完成最小实体集,后续迭代扩展 |
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 文档与代码同步