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
This commit is contained in:
iven
2026-04-12 15:22:28 +08:00
parent 685df5e458
commit 14f431efff
34 changed files with 785 additions and 304 deletions

View File

@@ -0,0 +1,277 @@
# 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](crates/erp-server/src/main.rs) — 将 `rate_limit_by_ip` 中间件应用到 `public_routes`
**验证**: 使用 `curl` 快速发送 20 次登录请求,第 11 次起应返回 429。
### A2. 补全工作流实例状态变更事件
**问题**: 实例完成 (`completed`)、挂起 (`suspended`)、恢复 (`resumed`)、终止 (`terminated`) 时未发布领域事件。只有 `process_instance.started` 被发布。
**修改文件**:
- [instance_service.rs](crates/erp-workflow/src/service/instance_service.rs) — 在 `change_status()` 方法中补全事件发布
- [flow_executor.rs](crates/erp-workflow/src/engine/flow_executor.rs) — 在 `check_instance_completion()` 中发布 `process_instance.completed`
**验证**: 启动流程实例 → 挂起 → 恢复 → 完成,检查 `domain_events` 表应有 4 条对应事件。
### A3. 消除硬编码默认租户 ID
**问题**: [state.rs:49](crates/erp-server/src/state.rs#L49) 使用 nil UUID 作为 `default_tenant_id`[auth_handler.rs:30](crates/erp-auth/src/handler/auth_handler.rs#L30) 在登录时直接使用。实际租户是 UUID v7nil UUID 不对应任何真实租户。
**修改文件**:
- [state.rs](crates/erp-server/src/state.rs) — 从数据库或配置读取真实的默认租户 ID
- [auth_handler.rs](crates/erp-auth/src/handler/auth_handler.rs) — 支持动态租户解析
**验证**: 启动服务后检查日志,确认 `default_tenant_id` 为种子数据中的实际租户 ID。
### A4. 实现审计日志查询 API
**问题**: 43 处审计日志写入覆盖所有 CRUD 操作,但无任何读取接口。`audit_logs` 表数据不可访问。
**新增文件**:
- `crates/erp-core/src/handler/audit_handler.rs` — 分页查询处理器
- 在 [main.rs](crates/erp-server/src/main.rs) 注册 `GET /api/v1/audit-logs` 路由
**查询参数**: `resource_type`, `user_id`, `from`, `to`, `page`, `page_size`
**验证**: 通过 API 查询审计日志,返回分页结果。
### A5. 修复 CORS 生产环境配置
**问题**: 默认配置允许 `"*"` 来源,生产环境不安全。
**修改文件**:
- [main.rs](crates/erp-server/src/main.rs) — 在 CORS 为 `"*"` 且非开发模式时发出警告或拒绝启动
---
## 阶段 B: 功能完整性修复 (P2)
### B1. 清理 erp-common 死代码 crate
**问题**: `erp-common` crate 导出了 4 个工具函数,但全代码库中零引用 (`use erp_common` 无匹配)。`erp-server``erp-auth``Cargo.toml` 声明了依赖但从未使用。
**操作**:
1. 从根 `Cargo.toml` 移除 workspace member
2.`erp-server/Cargo.toml``erp-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.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](apps/web/src/api/workflowInstances.ts) — 添加 `resumeInstance()`
- [InstanceMonitor.tsx](apps/web/src/pages/workflow/InstanceMonitor.tsx) — 根据状态显示挂起/恢复按钮
### B4. 消除 EventHandler 死 trait
**问题**: `EventHandler` trait 定义在 [events.rs](crates/erp-core/src/events.rs) 但全代码库零实现 (`impl EventHandler` 无匹配)。所有模块的 `register_event_handlers()` 方法体为空。消息模块通过独立的 `start_event_listener()` 静态方法处理事件。
**操作**: 两种方案二选一:
- **方案 A (推荐)**: 删除 `EventHandler` trait`register_event_handlers()` 接收 `&EventBus` 引用,各模块自行订阅
- **方案 B**: 实际在消息模块实现该 trait作为示范
### B5. 补全任务完成通知
**问题**: 消息模块收到 `task.completed` 事件后跳过处理(仅输出 debug 日志)。工作流任务完成后无通知。
**修改文件**:
- [module.rs](crates/erp-message/src/module.rs) — 在 `handle_workflow_event()` 中处理 `task.completed`
### B6. 接线 TimeoutChecker 后台任务
**问题**: [timeout.rs](crates/erp-workflow/src/engine/timeout.rs) 实现了 `TimeoutChecker::find_overdue_tasks()` 但从未被调用。无后台定时任务检查超时。
**修改文件**:
- [main.rs](crates/erp-server/src/main.rs) — 添加定时调用 TimeoutChecker 的后台任务(参考 outbox relay 模式)
### B7. 处理 ServiceTask 节点
**问题**: [flow_executor.rs](crates/erp-workflow/src/engine/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](crates/erp-core/src/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/languages``PUT /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](crates/erp-server/src/main.rs) | 模块注册、路由组装、事件总线、后台任务 |
| [state.rs](crates/erp-server/src/state.rs) | 硬编码 tenant_id、AppState 定义 |
| [module.rs](crates/erp-core/src/module.rs) | ErpModule trait、ModuleRegistry |
| [events.rs](crates/erp-core/src/events.rs) | EventBus、EventHandler trait |
| [instance_service.rs](crates/erp-workflow/src/service/instance_service.rs) | 缺失事件发布 |
| [module.rs (message)](crates/erp-message/src/module.rs) | 唯一的事件订阅者 |
| [timeout.rs](crates/erp-workflow/src/engine/timeout.rs) | 未接线的超时检查 |
| [flow_executor.rs](crates/erp-workflow/src/engine/flow_executor.rs) | ServiceTask 未实现 |
| [InstanceMonitor.tsx](apps/web/src/pages/workflow/InstanceMonitor.tsx) | 缺 resume/suspend 按钮 |
---
## 执行优先级
**Phase A (生产阻塞)****Phase B (功能完整)****Phase C (规格合规)****Phase D (E2E 验证)**
每个 Phase 完成后运行 `cargo check && cargo test --workspace && pnpm build` 确认无回归。