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