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>
262 lines
10 KiB
Markdown
262 lines
10 KiB
Markdown
# 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 后端 Bug(7 个问题)
|
||
|
||
### 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: 消息模块修复(P1,5 个问题)
|
||
|
||
### 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 | 全量回归测试 |
|