# 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 | 全量回归测试 |