Files
hms/plans/brainstorm-encapsulated-gray.md
iven 83fe89cbcd
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
fix: 全系统审计问题修复 — 安全/数据完整性/功能缺陷/UX (Phase 1-5)
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>
2026-04-26 19:16:23 +08:00

262 lines
10 KiB
Markdown
Raw 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.
# 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 | 全量回归测试 |