fix: 全系统审计问题修复 — 安全/数据完整性/功能缺陷/UX (Phase 1-5)
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

Phase 1 安全热修复:
- P0-1: /uploads 文件服务添加 JWT 认证中间件(支持 header + query param)
- P0-2: analytics/batch 路由从 public 移到 protected_routes
- P0-3: plugin engine SQL 注入修复(format! → 参数化查询)
- P0-new: stats_service compute_avg_field 字段白名单 + FLOAT8 类型转换

Phase 2 数据完整性:
- P0-4: 组织删除级联检查(添加部门存在性校验)
- P0-5: 部门删除级联检查(添加岗位 + 用户存在性校验)
- P0-8: workflow on_tenant_deleted 实现 5 实体批量删除
- P0-7: 并行网关 race condition 修复(consumed → completed 原子转换)

Phase 3 P1 后端 Bug:
- P1-12: plugin host 表名消毒(使用 sanitize_identifier)
- P1-10: workflow deprecated 状态转换(published → deprecated)
- P1-11: workflow 更新验证条件(nodes/edges 任一变化即验证)
- P0-9: 小程序 .gitignore 添加 .env/.env.*/日志
- P1-19: 小程序加密密钥替换为 64 字符强密钥

Phase 4 消息模块:
- P1-5: 通知偏好 GET 路由 + handler
- P1-4: 消息模板 update/delete CRUD + version
- P2-8: mark_all_read SQL 添加 version + 1
- P2-7: markAsRead 改为乐观更新 + 失败回滚

Phase 5 前端修复:
- P2-9: 通知面板点击导航到 /messages
- P2-1: 随访任务患者名批量 ID 解析(替代 UUID 显示)
- P2-5: AppointmentList 分离 patient_id/doctor_id 分别调用 API
- P2-17: PluginMarket installed 字段修正(name → id)
- P3-3: 路由标题 fallback 改为模式匹配(支持 :id 动态路径)
- P2-15: workflow updateDefinition 添加 version 字段
- P3-9: Kanban 版本使用记录实际 version
- P2-21: secure-storage 生产环境无密钥时阻止存储
- P3-11: destroyOnHidden → destroyOnClose
- P3-13: PendingTasks 深色模式 Tag 颜色适配

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-26 19:16:23 +08:00
parent a19b097409
commit 83fe89cbcd
33 changed files with 1238 additions and 70 deletions

View File

@@ -0,0 +1,261 @@
# HMS 审计问题全量修复计划
> 日期: 2026-04-26 | 基于 audit-report-2026-04-26.md 的 72 个问题
> 分 7 个 Phase 按优先级执行,每个 Phase 完成后提交验证
## Context
全系统审计发现 72 个问题9 P0 / 18 P1 / 22 P2 / 15 P3 / 6 P4 + 2 额外发现)。本计划将所有问题按修复复杂度和依赖关系分 7 个阶段执行。
## 修正后的审计计数
经代码验证,部分 P0 发现需要修正:
- **P0-4/5 部分存在**:组织删除已检查子组织(`org_service.rs:241-252`),部门删除已检查子部门(`dept_service.rs:264-275`),但均未检查关联的岗位/用户
- **新增 P0**`stats_service.rs:423-444``compute_avg_field` 函数也有 SQL 注入(`format!` 拼接 `field` 参数)
- **P3-5 误报**:登录默认 tenant_id 是开发环境行为,非 bug
---
## Phase 1: 安全热修复P0 安全4 个问题)
### 1.1 P0-1: 上传文件认证
- **文件**: `crates/erp-server/src/main.rs:543-546`
- **现状**: `ServeDir` 在 auth middleware 之外
- **修复**: 将 `/uploads` 移到 protected_routes或添加 `axum_middleware::from_fn` JWT 检查
- **方案**: 创建 `serve_file_with_auth` 中间件,从 query param 或 header 取 token 验证
### 1.2 P0-2: analytics/batch 认证
- **文件**: `crates/erp-server/src/main.rs:497-500`
- **现状**: 在 `public_routes`
- **修复**: 移到 `protected_routes`(加 `.merge(...)` 到 line 514 的 protected_routes
### 1.3 P0-3: plugin engine SQL 注入
- **文件**: `crates/erp-plugin/src/engine.rs:630-637`
- **现状**: `format!()` 拼接 `pid`
- **修复**: 改用 `Statement::from_sql_and_values` + `$1`, `$2` 参数化
### 1.4 P0-new: stats_service compute_avg_field SQL 注入
- **文件**: `crates/erp-health/src/service/stats_service.rs:423-444`
- **现状**: `format!("SELECT AVG({field}) AS avg_val...")` 直接拼接字段名
- **修复**: 添加字段名白名单验证 + 使用 `CAST(AVG(...) AS FLOAT8)` 同时解决类型问题
---
## Phase 2: 数据完整性修复P0 数据4 个问题)
### 2.1 P0-4: 组织删除级联(补充检查)
- **文件**: `crates/erp-auth/src/service/org_service.rs:240-252`
- **现状**: 已检查子组织,未检查部门
- **修复**: 在 line 252 后添加部门存在性检查
```rust
// Check for departments under this org
let depts = department::Entity::find()
.filter(department::Column::OrganizationId.eq(id))
.filter(department::Column::DeletedAt.is_null())
.one(db).await?;
if depts.is_some() {
return Err(AuthError::Validation("该组织下存在部门,无法删除".into()));
}
```
### 2.2 P0-5: 部门删除级联(补充检查)
- **文件**: `crates/erp-auth/src/service/dept_service.rs:264-275`
- **现状**: 已检查子部门,未检查岗位
- **修复**: 添加岗位存在性检查
### 2.3 P0-6: 岗位删除级联
- **文件**: `crates/erp-auth/src/service/position_service.rs:214-249`
- **现状**: 无关联检查
- **修复**: 检查 user_position 关联表中是否有用户分配到此岗位
### 2.4 P0-8: workflow on_tenant_deleted
- **文件**: `crates/erp-workflow/src/module.rs:148-154`
- **现状**: 空操作 `Ok(())`
- **修复**: 实现 5 个实体的批量软删除process_definition → instance → task/token/variable
- **实体**: `process_definitions`, `process_instances`, `tasks`, `tokens`, `process_variables`
---
## Phase 3: 并行网关 + P1 后端 Bug7 个问题)
### 3.1 P0-7: 并行网关 token 关联
- **文件**: `crates/erp-workflow/src/engine/executor.rs:369-425`
- **修复**: 在 token 表添加 `fork_id` 字段(或使用 token 创建时间窗口),区分不同 fork 产生的 token
- **轻量方案**: 使用 `SELECT ... FOR UPDATE` 加行锁 + 检查 token 的 `consumed_at` 时间窗口
### 3.2 P1-1: 统计报表 SQL 类型修复
- **文件**: `crates/erp-health/src/service/stats_service.rs`
- **修复**: SQL 中使用 `CAST(AVG(...) AS FLOAT8)``AVG(...)::FLOAT8`
- **同时修复**: `compute_avg_field` 的字段名白名单
### 3.3 P1-12: plugin host 表名消毒
- **文件**: `crates/erp-plugin/src/host.rs:339`
- **修复**: 使用已有的 `sanitize_identifier()` 函数
### 3.4 P1-10: workflow deprecated 状态
- **文件**: `crates/erp-workflow/src/service/definition_service.rs`
- **修复**: 添加 `deprecate` 方法,实现 `published → deprecated` 转换
### 3.5 P1-11: workflow 更新验证
- **文件**: `crates/erp-workflow/src/service/definition_service.rs:174-181`
- **修复**: nodes 或 edges 任一存在即执行验证
### 3.6 P0-9: 小程序 .gitignore
- **文件**: `apps/miniprogram/.gitignore`
- **修复**: 添加 `.env`, `.env.*`, `*.log`
### 3.7 P1-19: 小程序加密密钥
- **文件**: `apps/miniprogram/.env`
- **修复**: 生成 64 字符 hex 强密钥替换
---
## Phase 4: 消息模块修复P15 个问题)
### 4.1 P1-5: 通知偏好 GET + version
- **后端**: `crates/erp-message/src/module.rs` 添加 `GET /message-subscriptions` 路由
- **后端**: `crates/erp-message/src/handler/subscription_handler.rs` 添加 `get_subscription` handler
- **前端**: `apps/web/src/pages/messages/NotificationPreferences.tsx`
- useEffect 中调用 GET API 加载已有配置
- 保存时发送 version 字段
### 4.2 P1-4: 消息模板 CRUD
- **后端**: `template_service.rs` 添加 `update`, `delete` 方法
- **后端**: `module.rs` 添加 `PUT /message-templates/{id}`, `DELETE /message-templates/{id}` 路由
- **前端**: `MessageTemplates.tsx` 添加编辑/删除按钮
### 4.3 P2-6/7/8: 消息 store + mark_all_read 修复
- `stores/message.ts`: markAsRead 改为乐观更新 + 失败回滚
- `stores/message.ts`: 添加 markAllRead action重置 unreadCount 为 0
- `message_service.rs:298-326`: mark_all_read SQL 中添加 `version = version + 1`
### 4.4 P2-9: 通知面板点击导航
- `NotificationPanel.tsx:81-85`: 添加 `navigate('/messages')` 跳转
### 4.5 P1-20: urlCheck 配置
- **文件**: `apps/miniprogram/project.config.json:6`
- **修复**: 添加注释说明仅开发环境使用 false或改用条件配置
---
## Phase 5: 前端 P2-P3 Bug 修复15 个问题)
### 5.1 P2-1: 随访任务患者名 UUID
- **文件**: `apps/web/src/pages/health/FollowUpTaskList.tsx`
- **修复**: 在 `fetchTasks` 后添加 `AppointmentList` 风格的批量 ID 解析循环
### 5.2 P2-5: AppointmentList 冗余请求
- **文件**: `apps/web/src/pages/health/AppointmentList.tsx:103-121`
- **修复**: 分离 patient_id 和 doctor_id分别调用对应 API
### 5.3 P3-3: PatientDetail 标题"页面"
- **文件**: `apps/web/src/layouts/MainLayout.tsx:84-95, 387`
- **修复**: 将 `routeTitleFallback` 查找改为路径模式匹配(用 `startsWith` + 动态段替换)
### 5.4 P3-1: 已完成任务显示操作按钮
- **文件**: 健康模块各列表页
- **修复**: 根据状态条件渲染按钮
### 5.5 P2-17: PluginMarket installed 字段
- **文件**: `apps/web/src/pages/PluginMarket.tsx:68`
- **修复**: `Set` 改为 `result.data.map(p => p.id)` 而非 `p.name`
### 5.6 P3-6: 审计日志 UUID
- **文件**: `apps/web/src/pages/settings/AuditLogViewer.tsx:123-135`
- **修复**: 添加用户 ID → 用户名解析(批量查询或缓存)
### 5.7 P3-7: 审计日志资源类型过滤
- **文件**: `AuditLogViewer.tsx:7-18`
- **修复**: 添加 plugin 相关类型到 RESOURCE_TYPE_OPTIONS
### 5.8 P3-9: Kanban version 硬编码
- **文件**: `apps/web/src/pages/PluginKanbanPage.tsx:113`
- **修复**: 使用 record 的实际 version 字段
### 5.9 P3-11: destroyOnHidden → destroyOnClose
- **文件**: `ProcessDefinitions.tsx:192`
- **修复**: 替换 prop 名
### 5.10 P3-13: 深色模式 Tag
- **文件**: `PendingTasks.tsx:95-101`
- **修复**: 使用 `isDark` 条件判断颜色
### 5.11 P2-21: secure-storage 明文回退
- **文件**: `apps/miniprogram/src/utils/secure-storage.ts:12`
- **修复**: 生产环境下密钥为空时阻止存储,抛出错误
### 5.12 P3-12: InstanceMonitor node_id
- **文件**: `InstanceMonitor.tsx:149`
- **修复**: 从 definition 的 nodes 中查找 node_name
### 5.13 P2-14: 委派 UUID 输入
- **文件**: `PendingTasks.tsx:207-213`
- **修复**: 替换为用户搜索选择组件
### 5.14 P2-15: UpdateDefinition version
- **文件**: `apps/web/src/api/workflowDefinitions.ts:45-51`
- **修复**: 添加 version 字段
### 5.15 P3-4: any 类型替换
- 搜索 `apps/web/src/` 中 4 处 any替换为具体类型
---
## Phase 6: 功能补全P1 功能缺失8 个问题)
### 6.1 P1-3: 消息 SSE 推送(最小可行方案)
- **后端**: 添加 `GET /api/v1/messages/stream` SSE 端点
- **后端**: EventBus 订阅 `message.sent` 事件推送给对应用户
- **前端**: NotificationPanel 中连接 SSE收到事件立即更新
- **注意**: 先实现 SSE比 WebSocket 简单),后续可升级
### 6.2 P1-6/7/8/9: 工作流引擎功能补全
- **P1-6 ServiceTask**: 添加 HTTP 调用能力(基础版:支持 GET/POST URL
- **P1-7 事件注册**: 实现 `register_event_handlers`,监听 `user.deleted` 等事件
- **P1-8 任务认领**: 添加 `claim` 方法,支持 candidate_groups 过滤列表
- **P1-9 超时升级**: timeout checker 中添加自动通知发布
### 6.3 P1-15: 名称唯一性
- **文件**: `org_service.rs`, `dept_service.rs`
- **修复**: create/update 时检查同 tenant 下名称唯一性
### 6.4 P1-18: 消息群发 fan-out
- **文件**: `message_service.rs`
- **修复**: 当 recipient_type 为 role/dept/all 时,查询对应用户列表,批量创建消息
---
## Phase 7: P3-P4 收尾 + 优化6 个问题)
- P4-1: PluginAdmin purge 按钮状态
- P4-3: recover_plugins tenant 过滤
- P4-4: LanguageManager 编辑弹窗
- P4-5: ChangePassword 后端验证
- P4-6: Settings API URL 编码
- P3-2: ArticleEditor 图片上传(标记为未来任务)
- P3-14: 偏好设置 DND 时间范围验证
- P3-15: 小程序空状态处理
---
## 验证计划
每个 Phase 完成后执行:
1. `cargo check` — 编译通过
2. `cargo test --workspace` — 所有测试通过
3. 浏览器验证 — 启动服务,操作对应页面确认修复生效
4. `git commit` — 提交修复
5. `git push` — 推送到远程
### 关键验证点
| Phase | 验证方式 |
|-------|---------|
| Phase 1 | curl 无 token 访问 /uploads 应 401 |
| Phase 2 | 尝试删除含部门的组织应返回错误 |
| Phase 3 | 统计报表页面应正常加载数据 |
| Phase 4 | 偏好设置保存后重载应显示已有配置 |
| Phase 5 | FollowUpTaskList 患者名应显示而非 UUID |
| Phase 6 | 发送角色消息,该角色用户应收到 |
| Phase 7 | 全量回归测试 |