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

278 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` 确认无回归。