Files
erp/plans/rosy-frolicking-naur.md
iven 14f431efff feat: systematic functional audit — fix 18 issues across Phase A/B
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
2026-04-12 15:22:28 +08:00

12 KiB
Raw Permalink Blame History

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 被发布。

修改文件:

验证: 启动流程实例 → 挂起 → 恢复 → 完成,检查 domain_events 表应有 4 条对应事件。

A3. 消除硬编码默认租户 ID

问题: state.rs:49 使用 nil UUID 作为 default_tenant_idauth_handler.rs:30 在登录时直接使用。实际租户是 UUID v7nil UUID 不对应任何真实租户。

修改文件:

验证: 启动服务后检查日志,确认 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-servererp-authCargo.toml 声明了依赖但从未使用。

操作:

  1. 从根 Cargo.toml 移除 workspace member
  2. erp-server/Cargo.tomlerp-auth/Cargo.toml 移除依赖
  3. 删除 crates/erp-common/ 目录
  4. cargo build 验证

B2. 修复前端 API 层绕行问题

问题: 5 个设置子页面 (DictionaryManager, MenuConfig, NumberingRules, SystemSettings, ThemeSettings) 和 NotificationPreferences 直接调用 client.get/put,绕过了已存在的类型化 API 模块。导致 api/ 目录下多个导出成为死代码。

死代码清单:

  • api/errors.tsextractErrorMessage() 从未被导入
  • api/dictionaries.tslistItemsByCode() 从未被导入
  • api/menus.tsgetMenus(), batchSaveMenus() 从未被导入
  • api/settings.tsgetSetting(), updateSetting() 从未被导入
  • api/numberingRules.ts — 所有导出函数从未被导入

操作: 重构所有页面使用对应的 api/ 模块函数,删除未使用的直接调用。

B3. 添加流程实例恢复/挂起按钮

问题: 后端有 POST /instances/{id}/suspendPOST /instances/{id}/resume,但前端 InstanceMonitor 只有"终止"按钮。workflowInstances.ts 导出了 suspendInstance 但没有 resumeInstance

修改文件:

B4. 消除 EventHandler 死 trait

问题: EventHandler trait 定义在 events.rs 但全代码库零实现 (impl EventHandler 无匹配)。所有模块的 register_event_handlers() 方法体为空。消息模块通过独立的 start_event_listener() 静态方法处理事件。

操作: 两种方案二选一:

  • 方案 A (推荐): 删除 EventHandler traitregister_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.ts
  • apps/web/src/pages/settings/AuditLogViewer.tsx
  • 在 Settings.tsx 添加"审计日志"标签页

C2. 实现语言管理前端页面

问题: 后端有 GET /config/languagesPUT /config/languages/{code},无前端。

新增文件:

  • apps/web/src/api/languages.ts
  • apps/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 确认无回归。