Phase A (P1 production blockers): - A1: Apply IP rate limiting to public routes (login/refresh) - A2: Publish domain events for workflow instance state transitions (completed/suspended/resumed/terminated) via outbox pattern - A3: Replace hardcoded nil UUID default tenant with dynamic DB lookup - A4: Add GET /api/v1/audit-logs query endpoint with pagination - A5: Enhance CORS wildcard warning for production environments Phase B (P2 functional gaps): - B1: Remove dead erp-common crate (zero references in codebase) - B2: Refactor 5 settings pages to use typed API modules instead of direct client calls; create api/themes.ts; delete dead errors.ts - B3: Add resume/suspend buttons to InstanceMonitor page - B4: Remove unused EventHandler trait from erp-core - B5: Handle task.completed events in message module (send notifications) - B6: Wire TimeoutChecker as 60s background task - B7: Auto-skip ServiceTask nodes instead of crashing the process - B8: Remove empty register_routes() from ErpModule trait and modules
12 KiB
ERP 平台系统性功能审计计划
Context
ERP 平台底座 Phase 1-6 全部标记完成,包含 7 个 Rust crate、31 个数据库迁移、1 个 React SPA 前端。在进入下一阶段(行业模块插接)之前,需要系统性审计验证所有已实现功能的完整性、一致性和可用性,确保底座稳固可靠。
审计发现总览
| 类别 | 严重程度 | 数量 |
|---|---|---|
| 死代码/未使用模块 | P2 | 4 项 |
| 事件总线断线 | P1 | 5 项 |
| 前后端不一致 | P2 | 4 项 |
| 规格未实现功能 | P2-P3 | 8 项 |
| 安全隐患 | P1 | 3 项 |
| 架构缺陷 | P2 | 2 项 |
阶段 A: 生产阻塞问题 (P1)
A1. 修复登录端点无限流保护
问题: 公共路由 (/auth/login, /auth/refresh) 未应用 rate_limit_by_ip 中间件,只有受保护路由有 rate_limit_by_user。登录接口可被暴力破解。
修改文件:
- main.rs — 将
rate_limit_by_ip中间件应用到public_routes
验证: 使用 curl 快速发送 20 次登录请求,第 11 次起应返回 429。
A2. 补全工作流实例状态变更事件
问题: 实例完成 (completed)、挂起 (suspended)、恢复 (resumed)、终止 (terminated) 时未发布领域事件。只有 process_instance.started 被发布。
修改文件:
- instance_service.rs — 在
change_status()方法中补全事件发布 - flow_executor.rs — 在
check_instance_completion()中发布process_instance.completed
验证: 启动流程实例 → 挂起 → 恢复 → 完成,检查 domain_events 表应有 4 条对应事件。
A3. 消除硬编码默认租户 ID
问题: state.rs:49 使用 nil UUID 作为 default_tenant_id,auth_handler.rs:30 在登录时直接使用。实际租户是 UUID v7,nil UUID 不对应任何真实租户。
修改文件:
- state.rs — 从数据库或配置读取真实的默认租户 ID
- auth_handler.rs — 支持动态租户解析
验证: 启动服务后检查日志,确认 default_tenant_id 为种子数据中的实际租户 ID。
A4. 实现审计日志查询 API
问题: 43 处审计日志写入覆盖所有 CRUD 操作,但无任何读取接口。audit_logs 表数据不可访问。
新增文件:
crates/erp-core/src/handler/audit_handler.rs— 分页查询处理器- 在 main.rs 注册
GET /api/v1/audit-logs路由
查询参数: resource_type, user_id, from, to, page, page_size
验证: 通过 API 查询审计日志,返回分页结果。
A5. 修复 CORS 生产环境配置
问题: 默认配置允许 "*" 来源,生产环境不安全。
修改文件:
- main.rs — 在 CORS 为
"*"且非开发模式时发出警告或拒绝启动
阶段 B: 功能完整性修复 (P2)
B1. 清理 erp-common 死代码 crate
问题: erp-common crate 导出了 4 个工具函数,但全代码库中零引用 (use erp_common 无匹配)。erp-server 和 erp-auth 的 Cargo.toml 声明了依赖但从未使用。
操作:
- 从根
Cargo.toml移除 workspace member - 从
erp-server/Cargo.toml和erp-auth/Cargo.toml移除依赖 - 删除
crates/erp-common/目录 cargo build验证
B2. 修复前端 API 层绕行问题
问题: 5 个设置子页面 (DictionaryManager, MenuConfig, NumberingRules, SystemSettings, ThemeSettings) 和 NotificationPreferences 直接调用 client.get/put,绕过了已存在的类型化 API 模块。导致 api/ 目录下多个导出成为死代码。
死代码清单:
api/errors.ts—extractErrorMessage()从未被导入api/dictionaries.ts—listItemsByCode()从未被导入api/menus.ts—getMenus(),batchSaveMenus()从未被导入api/settings.ts—getSetting(),updateSetting()从未被导入api/numberingRules.ts— 所有导出函数从未被导入
操作: 重构所有页面使用对应的 api/ 模块函数,删除未使用的直接调用。
B3. 添加流程实例恢复/挂起按钮
问题: 后端有 POST /instances/{id}/suspend 和 POST /instances/{id}/resume,但前端 InstanceMonitor 只有"终止"按钮。workflowInstances.ts 导出了 suspendInstance 但没有 resumeInstance。
修改文件:
- workflowInstances.ts — 添加
resumeInstance() - InstanceMonitor.tsx — 根据状态显示挂起/恢复按钮
B4. 消除 EventHandler 死 trait
问题: EventHandler trait 定义在 events.rs 但全代码库零实现 (impl EventHandler 无匹配)。所有模块的 register_event_handlers() 方法体为空。消息模块通过独立的 start_event_listener() 静态方法处理事件。
操作: 两种方案二选一:
- 方案 A (推荐): 删除
EventHandlertrait,让register_event_handlers()接收&EventBus引用,各模块自行订阅 - 方案 B: 实际在消息模块实现该 trait,作为示范
B5. 补全任务完成通知
问题: 消息模块收到 task.completed 事件后跳过处理(仅输出 debug 日志)。工作流任务完成后无通知。
修改文件:
- module.rs — 在
handle_workflow_event()中处理task.completed
B6. 接线 TimeoutChecker 后台任务
问题: timeout.rs 实现了 TimeoutChecker::find_overdue_tasks() 但从未被调用。无后台定时任务检查超时。
修改文件:
- main.rs — 添加定时调用 TimeoutChecker 的后台任务(参考 outbox relay 模式)
B7. 处理 ServiceTask 节点
问题: flow_executor.rs 遇到 ServiceTask 节点时直接返回错误 "ServiceTask not yet implemented",导致包含 ServiceTask 的流程无法运行。
操作: 两种方案二选一:
- 方案 A: 实现 HTTP 调用类型的 ServiceTask
- 方案 B: 在设计器中禁止放置 ServiceTask 节点,并在引擎中给出更友好的错误提示
B8. 修复 ErpModule trait 的 register_routes() 空实现
问题: 所有 4 个模块的 register_routes() 都原样返回传入的 Router。实际路由通过 public_routes() / protected_routes() 静态方法注册。ModuleRegistry::build_router() 调用 trait 方法但无效。
修改文件:
- module.rs — 重新设计 trait 接口,使
register_routes()有实际作用,或删除并改用静态方法
阶段 C: 规格合规补全 (P2-P3)
C1. 实现审计日志前端页面
依赖: A4 (审计日志查询 API)
新增文件:
apps/web/src/api/auditLogs.tsapps/web/src/pages/settings/AuditLogViewer.tsx- 在 Settings.tsx 添加"审计日志"标签页
C2. 实现语言管理前端页面
问题: 后端有 GET /config/languages 和 PUT /config/languages/{code},无前端。
新增文件:
apps/web/src/api/languages.tsapps/web/src/pages/settings/LanguageManager.tsx- 在 Settings.tsx 添加"语言管理"标签页
C3. 创建 Theme API 模块
问题: ThemeSettings.tsx 直接调用 client,无 api/themes.ts 模块。
新增文件:
apps/web/src/api/themes.ts- 重构 ThemeSettings.tsx 使用该模块
C4. 评估 JWT 存储安全
问题: 规格要求 "httpOnly cookie (web)",实际使用 localStorage。XSS 可窃取 token。
操作: 评估迁移到 httpOnly cookie 的可行性,或文档化安全权衡。
C5. WebSocket 实时推送 (P3)
问题: 规格要求 WS /ws/v1/messages 实时推送,实际使用 HTTP 轮询 (60s 间隔)。无后端 WebSocket 端点,无前端 WebSocket 客户端。
操作: 实现基础 WebSocket 升级 + JWT 认证 + 前端连接。
C6. 全局搜索 (P3)
问题: 规格要求顶部导航栏搜索框,实际未实现。
C7. 多标签页切换 (P3)
问题: 规格要求浏览器式多标签页,实际使用单页路由。
C8. 浏览器通知 (P3)
问题: 规格要求 Web Notification API 集成,未实现。
阶段 D: 端到端验证测试
D1. 用户生命周期 E2E
注册 → 分配角色 → 登录 → 执行操作 → 验证审计日志 → 删除用户
检查点: Argon2 哈希、access token TTL、refresh 轮换、软删除
D2. 审批流程 E2E
创建定义 → 发布 → 启动实例 → 完成首任务 → 条件分支 → 第二任务 → 完成
检查点: 表达式求值、并行网关 fork/join、任务委派、流程变量
D3. 多租户隔离验证
创建两个租户 → 各创建用户 → 验证数据隔离
检查点: 所有查询含 tenant_id、中间件注入正确、跨租户访问被拒
D4. 通知流程 E2E
启动流程实例 → 验证消息创建 → 标记已读 → 验证未读计数
检查点: 模板渲染、订阅偏好、未读计数、全部标记已读
5 种差距模式检测结果
| 模式 | 发现 | 示例 |
|---|---|---|
| 写了没接 | 6 处 | TimeoutChecker 实现但未接线、EventHandler trait 定义但未使用 |
| 接了没传 | 3 处 | task.completed 事件有订阅但处理器跳过、register_routes() 被调用但所有模块返回空 |
| 传了没存 | 0 处 | 未发现 |
| 存了没用 | 2 处 | audit_logs 写入 43 处但无查询 API、erp-common crate 完整但从未引用 |
| 双系统不同步 | 4 处 | 前端缺 resume 按钮后端有、前端缺语言管理后端有、前端 settings 页面绕过 api 模块、JWT 存储方式与规格不符 |
10 项审计清单结果
| # | 审计项 | 状态 | 说明 |
|---|---|---|---|
| 1 | 代码存在性 | ⚠️ 部分缺失 | ServiceTask/TimeoutChecker 存在但未接线 |
| 2 | 调用链连通 | ⚠️ 部分断裂 | EventHandler→register_event_handlers 断线 |
| 3 | 配置传递 | ✅ 正常 | config-rs + env 覆盖工作正常 |
| 4 | 降级策略 | ❌ 缺失 | 无断路器、无数据库连接重试 |
| 5 | 多租户隔离 | ⚠️ 有风险 | 默认 tenant ID 硬编码 nil UUID |
| 6 | 审计追溯 | ⚠️ 部分缺失 | 写入完整但无查询接口 |
| 7 | 事件传播 | ⚠️ 大量断裂 | 24 种事件仅 2 种被消费 |
| 8 | 前后端一致 | ⚠️ 部分缺失 | 5 个后端端点无前端消费者 |
| 9 | 死代码检测 | ❌ 存在 | erp-common 整个 crate 未使用 |
| 10 | 安全合规 | ⚠️ 有风险 | 登录无限流、JWT 存 localStorage、CORS 默认 * |
关键文件索引
| 文件 | 审计关联 |
|---|---|
| main.rs | 模块注册、路由组装、事件总线、后台任务 |
| state.rs | 硬编码 tenant_id、AppState 定义 |
| module.rs | ErpModule trait、ModuleRegistry |
| events.rs | EventBus、EventHandler trait |
| instance_service.rs | 缺失事件发布 |
| module.rs (message) | 唯一的事件订阅者 |
| timeout.rs | 未接线的超时检查 |
| flow_executor.rs | ServiceTask 未实现 |
| InstanceMonitor.tsx | 缺 resume/suspend 按钮 |
执行优先级
Phase A (生产阻塞) → Phase B (功能完整) → Phase C (规格合规) → Phase D (E2E 验证)
每个 Phase 完成后运行 cargo check && cargo test --workspace && pnpm build 确认无回归。