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

10 KiB
Raw Blame History

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),但均未检查关联的岗位/用户
  • 新增 P0stats_service.rs:423-444compute_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 后添加部门存在性检查
// 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 全量回归测试