From d1ab8074a37bb928dd054289f0f27434607859c1 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 27 Apr 2026 07:46:36 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=A4=9A=E4=B8=93=E5=AE=B6=E7=BB=84?= =?UTF-8?q?=E5=A4=B4=E8=84=91=E9=A3=8E=E6=9A=B4=E4=BA=A7=E5=87=BA=20?= =?UTF-8?q?=E2=80=94=205=20=E4=BB=BD=E8=AE=BE=E8=AE=A1=E8=A7=84=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基于全景审计分析,产出 5 份跨领域设计规格: 1. 性能优化 — 后端批量INSERT/合并COUNT/告警预加载 + 前端N+1内联name 2. 安全纵深防御 — PostgreSQL RLS/行级数据范围/session_key Redis/审计哈希链 3. 事件驱动架构增强 — 6个业务域11个缺失事件补发 + Outbox LISTEN/NOTIFY 4. 前端工程化 — 14个大组件拆分 + 3个重复模式统一 + Bundle优化 5. 可观测性与运维 — 深度健康检查/Prometheus/OpenTelemetry/生产Docker --- ...-04-26-event-driven-architecture-design.md | 249 ++++++++++++ .../2026-04-26-frontend-engineering-design.md | 247 ++++++++++++ ...2026-04-26-observability-and-ops-design.md | 215 +++++++++++ ...6-04-26-performance-optimization-design.md | 320 ++++++++++++++++ ...-04-26-security-defense-in-depth-design.md | 362 ++++++++++++++++++ 5 files changed, 1393 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-26-event-driven-architecture-design.md create mode 100644 docs/superpowers/specs/2026-04-26-frontend-engineering-design.md create mode 100644 docs/superpowers/specs/2026-04-26-observability-and-ops-design.md create mode 100644 docs/superpowers/specs/2026-04-26-performance-optimization-design.md create mode 100644 docs/superpowers/specs/2026-04-26-security-defense-in-depth-design.md diff --git a/docs/superpowers/specs/2026-04-26-event-driven-architecture-design.md b/docs/superpowers/specs/2026-04-26-event-driven-architecture-design.md new file mode 100644 index 0000000..011194a --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-event-driven-architecture-design.md @@ -0,0 +1,249 @@ +# 事件驱动架构增强设计规格 + +> 日期: 2026-04-26 | 状态: draft | 主题: 缺失事件补发 + Outbox relay 优化 + 事件 schema 版本化 + +## 1. 背景 + +HMS 已有完整的事件总线基础设施: + +- **EventBus** (`erp-core/src/events.rs`): 两阶段发布(先持久化 pending → 广播 → 更新 published) +- **Outbox relay** (`erp-server/src/outbox.rs`): 5 秒轮询 domain_events 表,重发 pending 事件 +- **domain_events 表**: id, tenant_id, event_type, payload, status, attempts, created_at, published_at + +已有事件发布的模块: patient, appointment, follow_up, consultation, health_data, alert_engine, device_reading, doctor。 + +## 2. 问题分析 + +### 2.1 缺失事件清单 + +以下 6 个业务域的 service 文件中无任何 `event_bus.publish` 调用: + +| 业务域 | Service 文件 | 缺失事件 | +|--------|-------------|----------| +| 透析记录 | `dialysis_service.rs` | `dialysis_record.created/reviewed` | +| 诊断 | `diagnosis_service.rs` | `diagnosis.created/updated` | +| 知情同意 | `consent_service.rs` | `consent.granted/revoked` | +| 日常监测 | `daily_monitoring_service.rs` | `daily_monitoring.created` | +| 积分 | `points_service.rs` | `points.earned/exchanged` | +| 资讯文章 | `article_service.rs` | `article.published/rejected` | + +### 2.2 基础设施改进项 + +| 编号 | 问题 | 影响 | +|------|------|------| +| I-1 | Outbox relay 5 秒轮询延迟高 | 事件从产生到广播最长 5 秒延迟 | +| I-2 | 事件 payload 无 schema 版本 | 消费者无法安全演进,字段增删破坏兼容性 | +| I-3 | 无事件幂等性保证 | 消费者重复消费可能导致业务异常 | +| I-4 | domain_events 表无清理策略 | 表无限增长影响查询性能 | + +## 3. 解决方案 + +### 3.1 缺失事件补发 + +#### 3.1.1 事件优先级排序 + +| 优先级 | 事件 | 理由 | +|--------|------|------| +| P0 | `dialysis_record.created/reviewed` | 透析是核心医疗流程,需触发统计更新和告警检查 | +| P0 | `diagnosis.created/updated` | 诊断关联后续治疗方案,影响预约/随访 | +| P1 | `consent.granted/revoked` | 合规要求,知情同意变更需通知医护 | +| P1 | `article.published/rejected` | 内容审核流程依赖事件驱动 | +| P2 | `daily_monitoring.created` | 日常监测触发趋势分析 | +| P2 | `points.earned/exchanged` | 积分变动通知用户 | + +#### 3.1.2 统一事件信封格式 + +所有事件 payload 遵循统一信封: + +```json +{ + "schema_version": "v1", + "entity_id": "uuid", + "entity_type": "dialysis_record", + "action": "created", + "tenant_id": "uuid", + "operator_id": "uuid | null", + "timestamp": "ISO 8601", + "data": { /* 实体快照或变更字段 */ }, + "metadata": { "source": "erp-health", "trace_id": "uuid" } +} +``` + +#### 3.1.3 各事件 data 字段设计 + +| 事件 | data 关键字段 | 说明 | +|------|--------------|------| +| `dialysis_record.created` | patient_id, dialysis_type, status, dialysis_date, duration, ultrafiltration_volume | 新建透析记录 | +| `dialysis_record.reviewed` | patient_id, reviewer_id, dialysis_type, complication_notes | 医生审核完成 | +| `diagnosis.created` | patient_id, icd_code, diagnosis_name, diagnosis_type, severity, diagnosed_at | 新诊断录入 | +| `diagnosis.updated` | patient_id, changed_fields[], old_values{}, new_values{} | 诊断信息变更(含 diff) | +| `consent.granted` | patient_id, consent_type, consent_scope, granted_by, expires_at | 知情同意签署 | +| `consent.revoked` | patient_id, consent_type, revoked_by, reason | 知情同意撤销 | +| `article.published` | title, author_id, category_id, tags[] | 文章审核通过发布 | +| `article.rejected` | title, reviewer_id, reason | 文章审核驳回 | +| `daily_monitoring.created` | patient_id, monitoring_date, monitoring_type, values{} | 日常监测数据录入 | +| `points.earned` | patient_id, points, source_type, source_id, balance_after | 积分获得 | +| `points.exchanged` | patient_id, points, product_name, order_id, balance_after | 积分兑换 | + +### 3.2 Outbox relay 优化 + +#### 3.2.1 PostgreSQL LISTEN/NOTIFY 替代轮询 + +**当前**: 5 秒轮询 `domain_events` 表(`outbox.rs` 第 26-32 行) + +**优化方案**: + +1. 在 `EventBus::publish()` 持久化事件后执行 `NOTIFY`: + +```rust +// erp-core/src/events.rs publish() 末尾添加 +let notify_sql = format!("NOTIFY outbox_channel, '{}'", event.id); +sqlx::query(¬ify_sql).execute(db).await.ok(); +``` + +2. Outbox relay 使用 `LISTEN` + 30 秒兜底轮询: + +```rust +let mut listener = PgListener::connect_with(&db).await?; +listener.listen("outbox_channel").await?; +loop { + tokio::select! { + _ = listener.recv() => { process_pending_events(&db, &event_bus).await.ok(); } + _ = tokio::time::sleep(Duration::from_secs(30)) => { + process_pending_events(&db, &event_bus).await.ok(); + } + } +} +``` + +**收益**: 事件延迟 0-5s → <100ms,DB 轮询压力降低 6x。**复杂度**: 低。 + +#### 3.2.2 domain_events 表清理 + +**方案**: 按月分区 + 90 天归档 + +```sql +CREATE TABLE domain_events_new (LIKE domain_events INCLUDING ALL) + PARTITION BY RANGE (created_at); +-- 按月创建分区 +CREATE TABLE domain_events_2026_04 PARTITION OF domain_events_new + FOR VALUES FROM ('2026-04-01') TO ('2026-05-01'); +``` + +已 published 且 > 90 天的事件迁移到 `domain_events_archive` 表。 + +### 3.3 事件 schema 版本化 + +在 payload 中嵌入 `schema_version` 字段,消费者按 `event_type` + `schema_version` 路由: + +```rust +fn handle_event(event: &DomainEvent) { + let version = event.payload["schema_version"].as_str().unwrap_or("v1"); + match (event.event_type.as_str(), version) { + ("dialysis_record.created", "v1") => handle_v1(event), + ("dialysis_record.created", "v2") => handle_v2(event), + _ => tracing::warn!("Unknown event version"), + } +} +``` + +**演进规则**: 新增字段兼容(不升版),删除/重命名字段不兼容(升版)。 + +### 3.4 事件幂等性保证 + +消费者维护 `processed_events` 去重表: + +```sql +CREATE TABLE processed_events ( + event_id UUID NOT NULL, + consumer_id VARCHAR(64) NOT NULL, + processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (event_id, consumer_id) +); +``` + +流程: 收到事件 → 查已处理 → 已存在则跳过 → 否则执行业务 + 插入记录。7 天 TTL 定期清理。 + +## 4. 实施步骤 + +### Phase 1: P0 事件补发(预估 2 天) + +| 步骤 | 任务 | 修改文件 | +|------|------|----------| +| 1.1 | dialysis_service 添加 created/reviewed 事件 | `dialysis_service.rs` | +| 1.2 | diagnosis_service 添加 created/updated 事件 | `diagnosis_service.rs` | +| 1.3 | 验证: 事件发布 + payload 格式正确 | - | + +### Phase 2: P1 事件补发(预估 1-2 天) + +| 步骤 | 任务 | 修改文件 | +|------|------|----------| +| 2.1 | consent_service 添加 granted/revoked 事件 | `consent_service.rs` | +| 2.2 | article_service 添加 published/rejected 事件 | `article_service.rs` | +| 2.3 | 验证: 事件发布正确触发 | - | + +### Phase 3: P2 事件补发(预估 1 天) + +| 步骤 | 任务 | 修改文件 | +|------|------|----------| +| 3.1 | daily_monitoring_service 添加 created 事件 | `daily_monitoring_service.rs` | +| 3.2 | points_service 添加 earned/exchanged 事件 | `points_service.rs` | +| 3.3 | 验证: 积分变动事件触发 | - | + +### Phase 4: 基础设施优化(预估 2-3 天) + +| 步骤 | 任务 | 修改文件 | +|------|------|----------| +| 4.1 | Outbox relay 改用 LISTEN/NOTIFY | `outbox.rs`, `events.rs` | +| 4.2 | 添加事件 schema_version 字段 | 所有事件发布处 | +| 4.3 | 创建 processed_events 去重表 | migration | +| 4.4 | domain_events 按月分区 + 清理策略 | migration + 后台任务 | +| 4.5 | 验证: 事件延迟 < 100ms + 去重测试 | - | + +## 5. 风险与缓解 + +### 5.1 LISTEN/NOTIFY 连接管理 + +**风险**: PostgreSQL LISTEN 使用独立连接,连接断开需重建。 +**缓解**: `sqlx::PgListener` 自动重连 + 30 秒兜底轮询确保不遗漏。 + +### 5.2 事件发布失败 + +**风险**: `event_bus.publish()` 失败但业务操作已提交。 +**缓解**: 两阶段发布已处理 — 事件写入 pending,outbox relay 重发。publish 失败仅 warn 日志,不阻塞业务。 + +### 5.3 去重表增长 + +**风险**: `processed_events` 表快速增长。 +**缓解**: 7 天 TTL 定期清理,或使用 Redis SET NX + TTL 替代。 + +### 5.4 Schema 演进兼容性 + +**风险**: 新版本消费者无法处理老版本事件。 +**缓解**: 消费者必须支持 N-1 版本 schema。升版前确保所有消费者已升级。 + +## 6. 已有事件 vs 缺失事件汇总 + +### 已发布事件(8 个模块) + +| 模块 | 事件类型 | 触发位置 | +|------|----------|----------| +| patient | `patient.created`, `patient.updated` | `patient_service.rs` | +| appointment | `appointment.created`, `appointment.status_changed` | `appointment_service.rs` | +| follow_up | `follow_up_task.created`, `follow_up_task.status_changed`, `follow_up_record.completed` | `follow_up_service.rs` | +| consultation | `consultation_session.created`, `consultation_session.status_changed` | `consultation_service.rs` | +| health_data | `vital_signs.created`, `lab_report.uploaded` | `health_data_service.rs` | +| alert | `alert.triggered` | `alert_engine.rs` | +| device | `device.readings.synced` | `device_reading_service.rs` | +| doctor | `doctor.schedule.updated` | `doctor_service.rs` | + +### 待补发事件(6 个模块,11 个事件) + +| 模块 | 事件类型 | 优先级 | +|------|----------|--------| +| dialysis | `dialysis_record.created`, `dialysis_record.reviewed` | P0 | +| diagnosis | `diagnosis.created`, `diagnosis.updated` | P0 | +| consent | `consent.granted`, `consent.revoked` | P1 | +| article | `article.published`, `article.rejected` | P1 | +| daily_monitoring | `daily_monitoring.created` | P2 | +| points | `points.earned`, `points.exchanged` | P2 | diff --git a/docs/superpowers/specs/2026-04-26-frontend-engineering-design.md b/docs/superpowers/specs/2026-04-26-frontend-engineering-design.md new file mode 100644 index 0000000..85e7bcb --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-frontend-engineering-design.md @@ -0,0 +1,247 @@ +# 前端工程化改进设计 + +> 日期: 2026-04-26 | 状态: draft | 主题: 组件拆分、重复模式统一、Bundle 优化 + +## 1. 背景 + +HMS Web 前端共 139 个源文件(77 TSX + 62 TS),总代码量 27,000 行。随着健康管理模块的持续迭代,工程化债务逐步积累,主要体现在四个方面: + +1. **组件膨胀** — 14 个文件超过 400 行,最大 872 行(PluginCRUDPage.tsx) +2. **重复模式** — 错误处理、分页列表、ID→名称缓存三处重复,已有统一抽象但未被全面采用 +3. **API 层风格混用** — 对象风格(`patientApi.list()`)与函数风格(`listAlerts()`)并存 +4. **Bundle 体积** — 大型依赖未拆独立 chunk,`chunkSizeWarningLimit` 已提升至 600KB + +### 1.1 数据概览 + +| 指标 | 数值 | +|------|------| +| 超过 400 行的组件 | 14 个 | +| 超过 500 行的组件 | 7 个 | +| 未使用 usePaginatedData 的健康列表页 | 6 个 | +| 自建 nameCache 的页面 | 2 个(AppointmentList、PointsOrderList)| +| API 层文件 | 33 个(对象风格 8 个,函数风格 25 个) | + +## 2. 问题分析 + +### 2.1 组件膨胀分析 + +TOP 7 大组件: + +| 文件 | 行数 | 职责混杂点 | +|------|------|-----------| +| PluginCRUDPage.tsx | 872 | 表格渲染 + 表单校验 + Drawer + 导入导出 + Timeline | +| PluginGraphPage.tsx | 759 | Canvas 绑定 + 数据加载 + 布局计算 + 动画控制 | +| Organizations.tsx | 622 | 三栏树形 + 组织 CRUD + 部门管理 + 人员分配 | +| StatisticsDashboard.tsx | 580 | 五个并行统计 API + 图表渲染 + 时间筛选 | +| ArticleEditor.tsx | 554 | 富文本编辑器 + 表单 + 标签选择 + 封面上传 | +| FollowUpTaskList.tsx | 547 | 列表 + 筛选面板 + 状态流转弹窗 + 批量操作 | +| MainLayout.tsx | 535 | 侧边栏 + 动态菜单 + 插件菜单注入 + Header | + +**根因:** React 组件未按"展示/容器/Hook"分层,状态逻辑和 UI 渲染耦合在同一文件中。 + +### 2.2 重复模式分析 + +**模式一:错误处理** + +已有统一方案:`client.ts` 全局拦截器处理 401/403/500,`handleApiError()` 处理业务错误,`useApiRequest()` hook 封装 try-catch。 + +实际情况:组件仍大量内联 `catch (err) { message.error(...) }`。原因:useApiRequest 缺少 loading 状态返回,部分场景需要更细粒度的错误控制。 + +**模式二:分页列表** + +已有 `usePaginatedData(fetchFn, pageSize)` hook,封装 data/total/page/loading/refresh。 + +未使用的健康列表页:PatientList、AppointmentList、ConsultationList、FollowUpTaskList、OfflineEventList、PointsProductList。原因:fetchFn 签名只支持 (page, pageSize, search),部分页面需要额外筛选参数(status/dateRange/tag)。 + +**模式三:ID→名称缓存** + +已有 `useHealthStore` 提供 `resolvePatientName(id)` / `getPatientName(id)` + 自动去重加载。 + +AppointmentList.tsx 和 PointsOrderList.tsx 仍自建 `useState>` nameCache。原因:store 未提供批量解析接口。 + +### 2.3 API 风格混用 + +- **对象风格**(8 个文件):`patientApi.list()`, `doctorApi.list()` 等 +- **函数风格**(25 个文件):`listAlerts()`, `acknowledgeAlert()` 等 + +结论:不强制统一(改动量大、收益有限),新增 API 文件统一采用对象风格。 + +### 2.4 Bundle 体积分析 + +当前 manualChunks 仅拆分了 react/react-dom/antd/axios/zustand。未拆分的大型依赖: + +| 依赖 | 估算大小 (gzip) | 使用范围 | +|------|----------------|---------| +| @ant-design/charts | ~180KB | 仅 StatisticsDashboard | +| @xyflow/react | ~120KB | 仅 PluginGraphPage | +| @wangeditor/editor | ~200KB | 仅 ArticleEditor | + +`chunkSizeWarningLimit: 600` 说明单个 chunk 已超过 Vite 默认 500KB 警告阈值。 + +## 3. 解决方案 + +### 3.1 组件拆分策略 + +统一采用 **Container + Presentational + Hook** 三层模式: + +``` +原组件 (500+ 行) +├── hooks/useXxxData.ts — 数据获取、状态管理、业务逻辑 +├── components/XxxTable.tsx — 纯展示表格 +├── components/XxxForm.tsx — 表单(含校验) +└── XxxPage.tsx — 容器组件(组装 hooks + 子组件) +``` + +#### PluginCRUDPage.tsx (872行) — P0 + +| 拆分目标 | 类型 | 预估行数 | 职责 | +|----------|------|---------|------| +| usePluginData.ts | Hook | ~120 | CRUD 操作、导入导出逻辑 | +| PluginTable.tsx | 展示 | ~150 | 表格列定义、行操作按钮 | +| PluginForm.tsx | 展示 | ~180 | 新增/编辑表单 + Drawer | +| PluginImportExport.tsx | 展示 | ~100 | 导入导出面板 | +| PluginTimeline.tsx | 展示 | ~80 | 操作历史 Timeline | +| PluginCRUDPage.tsx | 容器 | ~80 | 组装子组件 | + +#### PluginGraphPage.tsx (759行) — P1 + +| 拆分目标 | 类型 | 预估行数 | 职责 | +|----------|------|---------|------| +| useGraphLayout.ts | Hook | ~100 | 布局算法、节点位置计算 | +| useGraphData.ts | Hook | ~80 | 数据加载、边/节点转换 | +| GraphCanvas.tsx | 展示 | ~200 | ReactFlow 渲染、节点样式 | +| GraphToolbar.tsx | 展示 | ~60 | 工具栏(缩放/自动布局) | +| PluginGraphPage.tsx | 容器 | ~60 | 组装 | + +#### Organizations.tsx (622行) — P1 + +| 拆分目标 | 类型 | 预估行数 | 职责 | +|----------|------|---------|------| +| useOrgTree.ts | Hook | ~80 | 树数据加载、CRUD 操作 | +| OrgTree.tsx | 展示 | ~120 | 左侧树形选择 | +| OrgDetail.tsx | 展示 | ~150 | 右侧组织详情/编辑 | +| DeptMemberList.tsx | 展示 | ~100 | 部门成员列表 + 分配 | +| Organizations.tsx | 容器 | ~60 | 三栏布局组装 | + +#### StatisticsDashboard.tsx (580行) — P1 + +| 拆分目标 | 类型 | 预估行数 | 职责 | +|----------|------|---------|------| +| useStatsData.ts | Hook | ~100 | 五个统计 API 并行加载 | +| PatientTrendChart.tsx | 展示 | ~80 | 患者趋势图 | +| AppointmentStats.tsx | 展示 | ~80 | 预约统计图 | +| OverviewCards.tsx | 展示 | ~60 | 概览卡片组 | +| TimeRangeSelector.tsx | 展示 | ~40 | 时间范围选择 | +| StatisticsDashboard.tsx | 容器 | ~50 | 组装 | + +#### ArticleEditor.tsx (554行) — P2 + +| 拆分目标 | 类型 | 预估行数 | 职责 | +|----------|------|---------|------| +| useArticleEditor.ts | Hook | ~100 | 文章加载/保存/发布逻辑 | +| RichTextEditor.tsx | 展示 | ~150 | WangEditor 封装 | +| ArticleMetaForm.tsx | 展示 | ~120 | 标题/分类/标签/封面表单 | +| ArticleEditor.tsx | 容器 | ~60 | 组装 | + +#### FollowUpTaskList.tsx (547行) — P2 + +| 拆分目标 | 类型 | 预估行数 | 职责 | +|----------|------|---------|------| +| useFollowUpTasks.ts | Hook | ~100 | 列表加载/筛选/状态流转 | +| FollowUpTable.tsx | 展示 | ~120 | 表格 + 行操作 | +| FollowUpFilter.tsx | 展示 | ~80 | 筛选面板 | +| TaskStatusModal.tsx | 展示 | ~80 | 状态变更弹窗 | +| FollowUpTaskList.tsx | 容器 | ~50 | 组装 | + +#### MainLayout.tsx (535行) — P1 + +| 拆分目标 | 类型 | 预估行数 | 职责 | +|----------|------|---------|------| +| useMenuBuilder.ts | Hook | ~100 | 菜单数据构建、插件菜单合并 | +| AppSidebar.tsx | 展示 | ~120 | 侧边栏渲染 | +| AppHeader.tsx | 展示 | ~80 | 顶部 Header | +| MainLayout.tsx | 容器 | ~60 | 布局骨架 | + +### 3.2 重复模式统一方案 + +#### 3.2.1 增强 useApiRequest(P0) + +当前问题:缺少 loading 状态。增强为: + +```typescript +interface UseApiRequestReturn { + execute: (fn: () => Promise, successMsg?: string) => Promise; + loading: boolean; +} +``` + +改动量:~10 行。已有调用点无需修改。 + +#### 3.2.2 增强 usePaginatedData(P1) + +当前问题:fetchFn 签名只支持 (page, pageSize, search)。增强为支持泛型筛选参数: + +```typescript +interface UsePaginatedDataOptions { + fetchFn: (page: number, pageSize: number, filters: F) => Promise<{ data: T[]; total: number }>; + pageSize?: number; + defaultFilters: F; + autoFetch?: boolean; +} +``` + +改动量:~30 行。保持旧签名兼容(函数重载),6 个列表页渐进迁移。 + +#### 3.2.3 增强 useHealthStore 批量解析(P2) + +新增 `batchResolvePatientNames(ids)` / `batchResolveDoctorNames(ids)`。内部实现:去重 → 批量并发(限制并发数 5)→ 写入缓存。 + +改动量:stores/health.ts ~30 行。删除 AppointmentList/PointsOrderList 自建 nameCache 代码。 + +### 3.3 API 风格策略 + +不强制统一现有代码。新增规则:新建 API 文件统一采用对象风格;修改已有文件时顺手迁移(Boy Scout Rule)。 + +### 3.4 Bundle 优化方案 + +在 vite.config.ts 的 manualChunks 中增加重型依赖拆分: + +```typescript +if (id.includes('@ant-design/charts') || id.includes('@antv/')) return 'vendor-charts'; +if (id.includes('@xyflow/react') || id.includes('@reactflow/')) return 'vendor-flow'; +if (id.includes('@wangeditor/')) return 'vendor-editor'; +``` + +配合路由级 `React.lazy()` 加载,使独立 chunk 仅在访问对应页面时下载。预期主 chunk 从 > 600KB 降至 < 400KB,chunkSizeWarningLimit 可降至 500。 + +## 4. 实施步骤 + +| Phase | 任务 | 工期 | 优先级 | +|-------|------|------|--------| +| 1: 基础设施增强 | 增强 useApiRequest + usePaginatedData + manualChunks + 路由懒加载 | 1 天 | P0-P1 | +| 2: 核心组件拆分 | PluginCRUDPage + MainLayout + PluginGraphPage | 2-3 天 | P0-P1 | +| 3: 健康模块拆分 | StatisticsDashboard + ArticleEditor + FollowUpTaskList | 2 天 | P1-P2 | +| 4: 重复模式迁移 | 6 个列表页迁移 usePaginatedData + 2 个 nameCache 迁移 | 2 天 | P2 | +| 5: 验证回归 | pnpm build 验证 + 功能回归 + ESLint max-lines-per-file 规则 | 1 天 | P2 | + +## 5. 风险与缓解 + +| 风险 | 缓解措施 | +|------|---------| +| 拆分引入 re-render 性能退化 | React.memo 包装展示组件,DevTools Profiler 验证 | +| usePaginatedData 泛型重构破坏现有调用点 | 保持旧签名兼容(函数重载),渐进迁移 | +| 拆分后导入路径变化导致循环依赖 | 每个拆分完成后立即 `pnpm build` 验证 | +| Bundle 拆分过度导致请求数增加 | HTTP/2 多路复用下影响有限 | +| WangEditor 封装层与编辑器生命周期冲突 | useRef 管理 editor 实例,严格 cleanup | + +**不做的事情:** 不重写现有组件;不强制统一 API 风格;不引入新状态管理库;不做 SSR/SSG。 + +**成功指标:** + +| 指标 | 当前值 | 目标值 | +|------|--------|--------| +| >400 行的组件数 | 14 | <= 5 | +| >500 行的组件数 | 7 | 0 | +| 主 chunk 体积 | > 600KB | < 400KB | +| usePaginatedData 覆盖率 | ~30% | > 80% | +| useApiRequest 覆盖率 | ~20% | > 60% | diff --git a/docs/superpowers/specs/2026-04-26-observability-and-ops-design.md b/docs/superpowers/specs/2026-04-26-observability-and-ops-design.md new file mode 100644 index 0000000..9063051 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-observability-and-ops-design.md @@ -0,0 +1,215 @@ +# 可观测性与运维基础设施设计 + +> 日期: 2026-04-26 | 状态: draft | 主题: 健康检查、Prometheus 指标、分布式追踪、生产 Docker、日志聚合 + +## 1. 背景 + +HMS 后端基于 Axum 0.8 + SeaORM 1.1 + Redis 0.27 构建,当前运维能力缺口: + +| 能力 | 现状 | 差距 | +|------|------|------| +| 结构化日志 | tracing + tracing-subscriber JSON 格式 | 已实现,无聚合方案 | +| 健康检查 | GET /health 返回 { status, version, modules } | 不验证 DB/Redis 连通性 | +| 指标暴露 | 无 Prometheus endpoint | 需从零搭建 | +| 分布式追踪 | 无 OpenTelemetry | 需从零搭建 | +| 生产 Docker | 仅有开发 docker-compose(PostgreSQL + Redis) | 无 Rust 应用 Dockerfile | +| 日志聚合 | 无 ELK/Loki 集成 | 需从零搭建 | + +技术栈:tower-http 0.6(已启用 trace feature)、自定义 rate_limit/JWT 中间件通过 `axum::middleware::from_fn` 注册。 + +## 2. 问题分析 + +### 2.1 健康检查不充分 + +当前 `/health` 仅返回静态信息(版本号、模块名列表),不验证外部依赖连通性。容器编排无法据此判断服务是否真正可用。 + +### 2.2 无可观测性指标 + +缺少请求延迟分布(P50/P95/P99)、错误率、QPS、DB 连接池使用率、事件 outbox 积压量等关键运行指标。 + +### 2.3 无分布式追踪 + +Axum handler -> SeaORM query -> Redis command 之间无 trace_id 串联。排查跨模块问题(预约创建 -> 工作流启动 -> 消息通知)需手动对齐日志时间戳。 + +### 2.4 无生产级容器镜像 + +Rust 应用直接 `cargo run` 启动,缺少多阶段构建 Dockerfile、健康检查指令、非 root 用户运行。 + +## 3. 解决方案 + +### 3.1 深度健康检查 + +**Crate 选型**: 无额外依赖,使用已有的 sea_orm + redis。 + +**改造方案**: 扩展 `HealthResponse` 为分级检查,增加 `/health/live`(存活探针)和 `/health/ready`(就绪探针)两个子路径。 + +检查项: + +| 组件 | 检查方式 | 超时 | 关键性 | +|------|---------|------|--------| +| PostgreSQL | `SELECT 1` via SeaORM | 2s | 关键(失败返回 503) | +| Redis | `PING` via redis::Client | 1s | 非关键(失败标记 degraded) | +| 模块状态 | 遍历 registry 检查 on_startup 是否完成 | 0ms | 非关键 | + +状态判定:全部通过 → `healthy`,非关键组件失败 → `degraded`(200),关键组件失败 → `unhealthy`(503)。 + +**对现有代码的影响**: 仅修改 `handlers/health.rs`(~40 行改动),`AppState` 无需变化。 + +### 3.2 Prometheus 指标 + +**Crate 选型**: `metrics` 0.24 + `metrics-exporter-prometheus` 0.16 + +选择理由:`metrics` 是 Rust 生态的指标门面 crate(类似 `log`/`tracing` 的解耦设计),exporter 内置独立 HTTP server,不侵入 Axum 路由。 + +**指标设计:** + +| 类别 | 指标名 | 类型 | +|------|--------|------| +| 请求 | `http_request_duration_seconds{method,path,status}` | histogram | +| 请求 | `http_requests_total{method,path,status}` | counter | +| 数据库 | `db_pool_connections{state}` | gauge | +| 数据库 | `db_query_duration_seconds{operation}` | histogram | +| 事件 | `eventbus_published_total{event_type}` | counter | +| 事件 | `eventbus_outbox_pending_count` | gauge | +| 运行时 | `process_memory_rss_bytes` | gauge | + +**Axum middleware 集成要点:** + +- 新增 `middleware/metrics.rs`(~40 行),记录每个请求的 method/归一化 path/status/耗时 +- 路径归一化:`/api/v1/patients/xxx` → `/api/v1/patients/:id`,避免高基数标签 +- main.rs 初始化 exporter 监听独立端口 9090 +- 在路由组装处添加 `.layer(axum_middleware::from_fn(metrics_middleware))` + +**对现有代码的影响**: main.rs ~10 行、新增 middleware ~40 行、outbox/event_bus 关键路径埋点 ~20 行。 + +### 3.3 OpenTelemetry 分布式追踪 + +**Crate 选型**: `opentelemetry` 0.27 + `opentelemetry-otlp` 0.27 + `tracing-opentelemetry` 0.28 + +**成熟度评估:** +- Rust OTel SDK 0.27+ 版本 API 趋于稳定 +- `tracing-opentelemetry` 与 tracing-subscriber 兼容性良好 +- SeaORM/Redis 无原生 span 支持,需手动埋点 +- 风险:SDK 初始化增加启动时间约 100-200ms + +**集成方案:** + +1. main.rs tracing 初始化重构为条件启用:通过 `OTEL_EXPORTER_OTLP_ENDPOINT` 环境变量决定是否启用 +2. 利用已有的 `tower-http` TraceLayer(项目已依赖)注入 trace_id +3. SeaORM 关键查询点手动创建 `tracing::info_span!("db.query", db.operation = "xxx")` +4. 采用 OTLP 协议导出,兼容 Jaeger/Tempo/Zipkin + +**对现有代码的影响**: main.rs ~30 行改动、Cargo.toml 新增 4 个依赖、service 函数 span 注解渐进式添加。 + +### 3.4 生产 Docker 镜像 + +**多阶段构建策略:** + +| 阶段 | 基础镜像 | 目的 | +|------|---------|------| +| 编译 | rust:1.85-bookworm | cargo build --release | +| 运行 | debian:bookworm-slim | 仅二进制 + ca-certificates | + +关键设计: +- **层缓存优化**: 先复制所有 Cargo.toml → 创建空源文件 → 编译依赖 → 复制实际源码 → 编译应用。依赖不变时复用编译缓存。 +- **安全**: 非 root 用户(erp:erp)运行 +- **健康检查**: `curl -f http://localhost:3000/health/live` +- **预期运行时镜像**: ~50-80MB + +**docker-compose.prod.yml**: erp-server 服务 + 环境变量注入 + depends_on health condition + 健康检查指向 `/health/ready`。 + +### 3.5 日志聚合 + +**方案: Grafana Loki** + +选择理由:与 Prometheus 同属 Grafana 生态,不做全文索引按标签查询,资源消耗远低于 ELK。tracing-subscriber JSON 输出天然兼容 Loki 标签模型。 + +部署:Loki 3.0 + Grafana 11.0 + Prometheus 2.52 通过 `docker-compose.monitoring.yml` 独立管理。日志采集通过 Grafana Alloy 收集 Docker 容器 stdout。 + +### 3.6 告警规则 + +基于 Prometheus 指标的 5 条核心告警: + +| 规则 | 条件 | 级别 | +|------|------|------| +| HighErrorRate | 5xx 比率 > 5% 持续 2m | critical | +| HighLatencyP99 | P99 > 2s 持续 5m | warning | +| DatabasePoolExhaustion | 连接池使用率 > 85% 持续 3m | warning | +| OutboxBacklog | outbox 积压 > 100 持续 5m | warning | +| HealthCheckFailed | 服务 up == 0 持续 1m | critical | + +## 4. 实施步骤 + +### Phase 1: 深度健康检查 + 生产 Docker(1-2 天) + +| 任务 | 改动范围 | 优先级 | +|------|---------|--------| +| 扩展 HealthResponse + DB/Redis 检查 | handlers/health.rs ~60 行 | P0 | +| 添加 /health/live 和 /health/ready | handlers/health.rs ~20 行 | P0 | +| 编写生产 Dockerfile | 新文件 ~50 行 | P0 | +| 编写 docker-compose.prod.yml | 新文件 ~40 行 | P0 | + +### Phase 2: Prometheus 指标(2 天) + +| 任务 | 改动范围 | 优先级 | +|------|---------|--------| +| 引入 metrics crate | Cargo.toml ~4 行 | P0 | +| 实现 metrics middleware | 新文件 ~40 行 | P0 | +| 注册 middleware + exporter 初始化 | main.rs ~15 行 | P0 | +| SeaORM/EventBus 指标埋点 | ~40 行 | P1 | +| Prometheus + Grafana Docker 配置 | 新文件 ~60 行 | P1 | + +### Phase 3: OpenTelemetry 集成(2-3 天) + +| 任务 | 改动范围 | 优先级 | +|------|---------|--------| +| 引入 opentelemetry crate | Cargo.toml ~6 行 | P2 | +| 重构 tracing 初始化为条件启用 | main.rs ~30 行 | P2 | +| 添加 TraceLayer | main.rs ~5 行 | P2 | +| Service 函数 span 注解 | 渐进式 | P2 | + +### Phase 4: 日志聚合 + 告警(1-2 天) + +| 任务 | 改动范围 | 优先级 | +|------|---------|--------| +| Loki + Grafana 部署配置 | 新文件 ~40 行 | P2 | +| Grafana Alloy 日志采集配置 | 新文件 ~30 行 | P2 | +| Prometheus 告警规则 | 新文件 ~50 行 | P2 | + +## 5. 风险与缓解 + +| 风险 | 影响 | 缓解措施 | +|------|------|---------| +| OTel SDK breaking change | 升级困难 | 锁定 0.27 版本,feature flag 条件启用 | +| 指标收集增加延迟 | 性能退化 | histogram 无锁实现,单次 record < 50ns | +| 日志量导致存储膨胀 | 存储成本 | Loki retention 30 天,JSON 压缩率高 | +| Docker 编译缓存失效 | CI 时间长 | Cargo.toml 层和源码层分离 | +| Prometheus 暴露内部信息 | 安全风险 | 独立端口 9090,网络策略限制访问 | +| 健康检查超时阻塞 | /health 延迟 | 短超时(DB 2s/Redis 1s),并行检查 | + +**Crate 选型对比:** + +| 方案 | 优势 | 劣势 | 结论 | +|------|------|------|------| +| `prometheus` crate (原生) | 功能完整 | API 较重 | 不选 | +| `metrics` + exporter | 轻量 facade,解耦 | 需额外 crate | 推荐 | +| Jaeger 直接导出 | 简单 | 已废弃 | 不选 | +| OTLP + Tempo/Jaeger | 通用标准 | 需 Collector | 推荐 | + +**性能影响评估:** + +| 组件 | 额外延迟 | 额外内存 | 启动时间增幅 | +|------|---------|---------|-------------| +| Prometheus middleware | < 0.1ms/req | ~5MB | < 50ms | +| OpenTelemetry (10% 采样) | < 0.5ms/req | ~20MB | 100-200ms | +| 健康检查 (DB ping) | 仅 /health | 无 | 无 | + +**成功指标:** + +| 指标 | 当前值 | 目标值 | +|------|--------|--------| +| /health 覆盖外部依赖 | 无 | DB + Redis | +| Prometheus 端点 | 无 | :9090/metrics | +| 分布式追踪 | 无 | 请求→DB→Redis 全链路 | +| 生产镜像大小 | 无 | < 80MB | +| 告警规则数 | 0 | >= 5 条 | diff --git a/docs/superpowers/specs/2026-04-26-performance-optimization-design.md b/docs/superpowers/specs/2026-04-26-performance-optimization-design.md new file mode 100644 index 0000000..7dbc906 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-performance-optimization-design.md @@ -0,0 +1,320 @@ +# 性能优化设计规格 + +> 日期: 2026-04-26 | 状态: draft | 主题: 后端数据库查询 + 前端 N+1 与渲染优化 + +## 1. 背景 + +HMS 平台已完成核心业务功能开发(34 健康实体、22 前端页面),进入性能调优阶段。通过代码审查和运行时分析,发现以下瓶颈: + +- **后端**: 多处逐条 INSERT、串行 COUNT 查询、动态 SQL 拼接、串行 DB 调用 +- **前端**: N+1 请求模式、nameCache 循环依赖导致重渲染、未拆分 vendor chunk + +## 2. 问题分析 + +### 2.1 后端瓶颈(5 项) + +| 编号 | 问题 | 文件位置 | 优先级 | 预估收益 | +|------|------|----------|--------|----------| +| B-1 | device_reading_service 逐条 INSERT(最多 500 次 DB 往返) | `crates/erp-health/src/service/device_reading_service.rs` `batch_insert_readings()` | P0 | 500 次往返 → 1 次,延迟降低 95%+ | +| B-2 | stats_service 多次独立 COUNT(如 `get_follow_up_statistics` 4 次查询) | `crates/erp-health/src/service/stats_service.rs` `get_follow_up_statistics()` 等 | P1 | 4-7 次查询 → 1 次 GROUP BY,延迟降低 60-75% | +| B-3 | alert_engine 逐规则独立查询 DB(cooldown + 条件评估) | `crates/erp-health/src/service/alert_engine.rs` `evaluate_rules()` | P1 | N 规则 × 2 查询 → 1+1 批量查询,延迟线性降低 | +| B-4 | patient_service `get_health_summary` 4 次串行查询 | `crates/erp-health/src/service/patient_service.rs` `get_health_summary()` | P1 | 4 次串行 → 4 次并行,延迟降低 ~75% | +| B-5 | `compute_avg_field` 动态 format! SQL 无法利用 prepared statement 缓存 | `crates/erp-health/src/service/stats_service.rs` `compute_avg_field()` | P2 | 利用 PG prepared statement 缓存,高频调用场景 CPU 降低 | + +### 2.2 前端瓶颈(5 项) + +| 编号 | 问题 | 文件位置 | 优先级 | 预估收益 | +|------|------|----------|--------|----------| +| F-1 | N+1 请求: AppointmentList/ConsultationList/PointsOrderList 逐条请求 patient/doctor name | `apps/web/src/pages/health/AppointmentList.tsx` 等多个文件 | P0 | 页面加载从 O(N) 请求 → O(1),首屏时间降低 70-80% | +| F-2 | nameCache useState 导致 fetchData 循环重建 | `AppointmentList.tsx` `PointsOrderList.tsx` 的 useEffect 依赖 | P0 | 消除无限循环风险,请求量减少 50%+ | +| F-3 | PluginCRUDPage columns 未 memo 化 | `apps/web/src/pages/PluginCRUDPage.tsx` | P2 | 减少不必要 Table 重渲染 | +| F-4 | PluginGraphPage requestAnimationFrame 持续重绘 | `apps/web/src/pages/PluginGraphPage.tsx` | P2 | CPU 占用降低,仅在数据变更时重绘 | +| F-5 | @ant-design/charts / @xyflow/react / @wangeditor/editor 未拆独立 chunk | `apps/web/vite.config.ts` | P2 | 首屏 JS 体积降低 200-400KB (gzip) | + +## 3. 解决方案 + +### 3.1 B-1: device_reading_service 批量 INSERT + +**当前实现** (`batch_insert_readings`, 第 194-229 行): +- for 循环逐条 `model.insert(db).await`,每条一次 DB 往返 +- 500 条记录 = 500 次 INSERT + +**优化方案**: +- 使用 SeaORM `Entity::insert_many()` 一次性插入 +- 唯一约束冲突通过 `ON CONFLICT DO NOTHING` 处理(需要 raw SQL 或 sea_query 的 `on_conflict`) + +```rust +// 方向:构建 ActiveModel Vec,调用 insert_many +let models: Vec = parsed_readings + .iter() + .map(|(r, measured_at)| device_readings::ActiveModel { ... }) + .collect(); + +// insert_many + on_conflict_do_nothing +let result = device_readings::Entity::insert_many(models) + .on_conflict( + sea_query::OnConflict::columns([ + device_readings::Column::PatientId, + device_readings::Column::DeviceType, + device_readings::Column::MeasuredAt, + ]) + .do_nothing() + .to_owned() + ) + .exec(&state.db) + .await?; +``` + +**影响范围**: 仅修改 `device_reading_service.rs` 的 `batch_insert_readings()` 函数。 + +### 3.2 B-2: stats_service 合并 COUNT 查询 + +**当前实现** (`get_follow_up_statistics`, 第 93-139 行): +- `total_tasks`: 1 次 COUNT +- `completed`: 1 次 COUNT(status='completed') +- `pending`: 1 次 COUNT(status='pending') +- `overdue`: 1 次 COUNT(status='overdue') +- 共 4 次独立 DB 查询 + +**优化方案**: 合并为单次 `GROUP BY status` + 应用层聚合 + +```sql +SELECT status, COUNT(*) AS cnt +FROM follow_up_task +WHERE tenant_id = $1 AND deleted_at IS NULL +GROUP BY status +``` + +应用层从 HashMap 中提取各字段值。 + +同理适用于: +- `get_patient_statistics` (4 次 → 2 次: patient 表 1 次 GROUP BY + points_transaction 1 次) +- `get_consultation_statistics` (3 次 → 1 次 GROUP BY + 1 次平均响应时间) +- `get_dialysis_statistics` (3 次 → 1 次 GROUP BY) +- `get_lab_report_statistics` (4 次 → 1 次 GROUP BY) + +**影响范围**: `stats_service.rs` 中 6 个统计函数。 + +### 3.3 B-3: alert_engine 预加载 + 批量评估 + +**当前实现** (`evaluate_rules`, 第 12-58 行): +- 每条规则独立查询: `is_in_cooldown()` 1 次 + 条件评估 1-2 次 +- N 条规则 = 2N+ 次 DB 查询 + +**优化方案**: +1. 一次性查询所有 active rules(已有) +2. 批量查询该患者最近 cooldown 期间所有 alerts,构建 HashSet +3. 条件评估(single_threshold)只需查最新一条 hourly 记录,可按 device_type 批量查出后在内存匹配 + +```rust +// 批量 cooldown 检查 +let recent_alerts = alerts::Entity::find() + .filter(alerts::Column::TenantId.eq(tenant_id)) + .filter(alerts::Column::PatientId.eq(patient_id)) + .filter(alerts::Column::CreatedAt.gt(cooldown_start)) + .filter(alerts::Column::DeletedAt.is_null()) + .all(&state.db) + .await?; +let cooldown_set: HashSet = recent_alerts.iter().map(|a| a.rule_id).collect(); +``` + +**影响范围**: `alert_engine.rs` 的 `evaluate_rules()` 和辅助函数。 + +### 3.4 B-4: get_health_summary 并行化 + +**当前实现** (`get_health_summary`, 第 423-475 行): +- 4 次 `.await` 串行执行 +- 总延迟 = sum(4 次查询延迟) + +**优化方案**: 使用 `tokio::join!` 并行执行 + +```rust +let (latest_vitals, latest_lab, upcoming, pending_follow_ups) = tokio::join!( + // 最新体征 + vital_signs::Entity::find() + .filter(...) + .one(&state.db), + // 最新化验 + lab_report::Entity::find() + .filter(...) + .one(&state.db), + // 待处理预约 + appointment::Entity::find() + .filter(...) + .count(&state.db), + // 待办随访 + follow_up_task::Entity::find() + .filter(...) + .count(&state.db), +); +``` + +**影响范围**: 仅修改 `patient_service.rs` 的 `get_health_summary()`。 + +### 3.5 B-5: compute_avg_field 参数化 + +**当前实现** (`compute_avg_field`, 第 423-464 行): +- `format!("SELECT AVG({field})...")` 动态拼接 SQL +- 每个不同 field 生成不同 SQL 文本,PostgreSQL 无法缓存 prepared statement + +**优化方案**: 对每个允许的 field 生成独立的静态 SQL 常量 + +```rust +macro_rules! avg_field_sql { + ($field:literal) => { + concat!( + "SELECT AVG(", $field, ")::FLOAT8 AS avg_val FROM dialysis_record ", + "WHERE tenant_id = $1 AND deleted_at IS NULL AND ", $field, " IS NOT NULL ", + "AND created_at >= date_trunc('month', NOW())" + ) + }; +} + +match field { + "ultrafiltration_volume" => avg_field_sql!("ultrafiltration_volume"), + "dialysis_duration" => avg_field_sql!("dialysis_duration"), + // ... 其余字段 + _ => return Err(...), +} +``` + +**影响范围**: 仅修改 `stats_service.rs` 的 `compute_avg_field()`。 + +### 3.6 F-1: 后端列表 API 内联 name 字段 + +**当前问题**: 前端拿到列表后,循环调用 `patientApi.get(id)` / `doctorApi.get(id)` 获取名字。 + +**优化方案**: 后端列表查询时 JOIN 或子查询返回 `patient_name` / `doctor_name` 字段。 + +**需修改的 handler/service**: + +| 模块 | 列表 API | 需内联字段 | +|------|----------|-----------| +| appointment | list_appointments | patient_name, doctor_name | +| consultation_session | list_sessions | patient_name, doctor_name | +| follow_up_task | list_tasks | patient_name | +| points_order | list_orders | patient_name | +| lab_report | list_reports | patient_name | + +**实现方式**: 使用 SeaORM 的 `select_only()` + `expr_as()` 添加 JOIN 字段,或直接在 DTO 层补充查询。 + +```rust +// 方向:在查询列表时 LEFT JOIN users 表获取 display_name +let rows: Vec<(appointment::Model, Option)> = appointment::Entity::find() + .filter(...) + .find_also_related(user::Entity) // 如果有关联定义 + .all(&state.db) + .await?; +``` + +**影响范围**: 5 个 handler + 对应 service 函数 + DTO 结构体。 + +### 3.7 F-2: 移除 nameCache 依赖 + +**当前问题**: `AppointmentList.tsx` 和 `PointsOrderList.tsx` 的 `useEffect` 依赖 `nameCache`,更新 nameCache 触发 fetchData 重建,形成循环。 + +**优化方案**: +1. 后端内联 name 后(F-1),nameCache 机制可以完全移除 +2. 作为过渡方案,将 nameCache 查询拆到独立 useEffect,不作为列表数据获取的依赖 + +**影响范围**: `AppointmentList.tsx`, `PointsOrderList.tsx`, `ConsultationList.tsx`。 + +### 3.8 F-5: vendor chunk 拆分 + +**当前问题**: @ant-design/charts (~200KB)、@xyflow/react (~150KB)、@wangeditor/editor (~300KB) 打入主 bundle。 + +**优化方案**: Vite `manualChunks` 配置 + +```typescript +// vite.config.ts build.rollupOptions.output.manualChunks +manualChunks: { + 'vendor-charts': ['@ant-design/charts'], + 'vendor-flow': ['@xyflow/react'], + 'vendor-editor': ['@wangeditor/editor'], +} +``` + +**影响范围**: `apps/web/vite.config.ts`,配合 React.lazy 路由级加载。 + +### 3.9 F-3/F-4: 渲染优化 + +**F-3 PluginCRUDPage columns useMemo**: +```typescript +const columns = useMemo(() => [...], [schema]); +``` + +**F-4 PluginGraphPage 按需重绘**: +- 替换持续 `requestAnimationFrame` 为数据变更时触发的单次重绘 +- 使用 `ResizeObserver` 监听容器大小变化 + +## 4. 实施步骤 + +### Phase 1: P0 紧急优化(预估 2-3 天) + +| 步骤 | 任务 | 修改文件 | +|------|------|----------| +| 1.1 | B-1: `batch_insert_readings` 改用 `insert_many()` | `device_reading_service.rs` | +| 1.2 | F-1: 后端列表 API 内联 patient_name/doctor_name | 5 个 handler + service + DTO | +| 1.3 | F-2: 移除前端 nameCache 依赖 | `AppointmentList.tsx`, `PointsOrderList.tsx` | +| 1.4 | 验证: cargo test + 前端页面加载对比 | - | + +### Phase 2: P1 重要优化(预估 2-3 天) + +| 步骤 | 任务 | 修改文件 | +|------|------|----------| +| 2.1 | B-2: stats_service 合并 COUNT → GROUP BY | `stats_service.rs` | +| 2.2 | B-3: alert_engine 预加载 + 批量评估 | `alert_engine.rs` | +| 2.3 | B-4: `get_health_summary` 并行化 | `patient_service.rs` | +| 2.4 | 验证: 基准测试对比(前后延迟测量) | - | + +### Phase 3: P2 次要优化(预估 1-2 天) + +| 步骤 | 任务 | 修改文件 | +|------|------|----------| +| 3.1 | B-5: `compute_avg_field` 参数化 | `stats_service.rs` | +| 3.2 | F-5: vendor chunk 拆分 | `vite.config.ts` + 路由 lazy | +| 3.3 | F-3: PluginCRUDPage columns memo | `PluginCRUDPage.tsx` | +| 3.4 | F-4: PluginGraphPage 按需重绘 | `PluginGraphPage.tsx` | + +## 5. 风险与缓解 + +### 5.1 B-1 批量 INSERT 冲突处理 + +**风险**: `insert_many()` + `ON CONFLICT DO NOTHING` 可能无法精确统计 inserted vs duplicates。 +**缓解**: 先查询已存在的记录(按 patient_id + device_type + measured_at),过滤后插入净新增量。或使用 `exec_with_returning` 获取实际插入数。 + +### 5.2 F-1 后端 JOIN 性能 + +**风险**: 列表 API JOIN users 表可能在大数据量下变慢。 +**缓解**: users 表通常在千级别,JOIN 性能可接受。如遇瓶颈,可在列表查询中使用子查询 `(SELECT display_name FROM users WHERE id = ...)` 替代 JOIN。 + +### 5.3 B-2 统计精度 + +**风险**: GROUP BY 聚合结果可能与多次 COUNT 不完全一致(如有 NULL status)。 +**缓解**: 确保 `GROUP BY status` 包含对 NULL status 的处理,测试中对比优化前后结果一致性。 + +### 5.4 前端 nameCache 移除过渡 + +**风险**: 后端未全部内联 name 前移除 nameCache 导致页面显示 UUID。 +**缓解**: 采用渐进式 — 逐个 API 内联 name 并移除对应页面的 nameCache,每个 API 独立验证。 + +## 6. 性能基准 + +### 6.1 后端优化前后预估 + +| 接口 | 优化前 | 优化后 | 提升 | +|------|--------|--------|------| +| POST /device-readings/batch (500条) | ~2500ms (500×5ms) | ~50ms (1次INSERT) | 50x | +| GET /stats/follow-up | ~40ms (4次查询) | ~10ms (1次GROUP BY) | 4x | +| GET /stats/dashboard | ~200ms (串行4个stats) | ~80ms (并行+合并) | 2.5x | +| GET /patient/{id}/health-summary | ~60ms (串行4次) | ~20ms (并行4次) | 3x | +| GET /alert/evaluate | ~100ms (10规则×10ms) | ~30ms (批量) | 3x | + +### 6.2 前端优化前后预估 + +| 页面 | 优化前 | 优化后 | 提升 | +|------|--------|--------|------| +| AppointmentList (20条) | ~3s (1+20+20请求) | ~300ms (1请求) | 10x | +| ConsultationList (20条) | ~2.5s | ~300ms | 8x | +| PointsOrderList (20条) | ~2s | ~300ms | 7x | +| 首屏 JS 体积 | ~1.2MB gzip | ~0.8MB gzip | 33% | diff --git a/docs/superpowers/specs/2026-04-26-security-defense-in-depth-design.md b/docs/superpowers/specs/2026-04-26-security-defense-in-depth-design.md new file mode 100644 index 0000000..7dc3963 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-security-defense-in-depth-design.md @@ -0,0 +1,362 @@ +# 安全纵深防御设计规格 + +> 日期: 2026-04-26 | 状态: draft | 主题: 数据库 RLS + 行级权限 + session_key 分布式化 + 审计增强 + +## 1. 背景 + +HMS 平台已具备良好的安全基础:JWT 认证、RBAC 权限、Argon2 密码哈希、PII 字段加密、API 限流、CORS 配置。但作为医疗 SaaS 平台,需要纵深防御(defense in depth)确保即使应用层出现漏洞,数据安全仍有保障。 + +本规格聚焦 6 项安全增强,按医疗合规影响排序。 + +## 2. 问题分析 + +### 2.1 安全增强项清单 + +| 编号 | 问题 | 医疗合规影响 | 实施复杂度 | 影响范围 | +|------|------|-------------|-----------|----------| +| S-1 | PostgreSQL RLS 安全网 | **高** — 跨租户数据泄漏是医疗数据合规红线 | 中 | 所有表 + migration + 中间件 | +| S-2 | 行级数据范围未实现 | **高** — 医生只能看本科室患者是基本合规要求 | 中 | rbac + 所有 health handler | +| S-3 | 微信 session_key 内存 HashMap | **中** — 多实例部署失效导致登录中断 | 低 | wechat_service.rs | +| S-4 | 小程序 openid 明文存储 | **中** — 本地存储泄露可关联用户身份 | 低 | miniprogram storage | +| S-5 | 审计日志完整性 | **高** — 医疗合规要求不可篡改的操作审计 | 中 | audit 模块 + migration | +| S-6 | 健康检查端点不验证依赖 | **低** — 运维可靠性,非合规要求 | 低 | handlers/health.rs | + +### 2.2 各项详细分析 + +**S-1 PostgreSQL RLS 安全网** + +当前多租户完全依赖应用层 `tenant_id` 过滤。代码中每个 `Entity::find()` 都手动添加 `.filter(Column::TenantId.eq(tenant_id))`,但如果有 handler 遗漏(如新增 handler 忘记加过滤),则跨租户数据泄漏。PostgreSQL Row Level Security (RLS) 可作为数据库级安全网。 + +**S-2 行级数据范围** + +`role_permissions` 表已有 `data_scope` 字段(m000036 迁移,值: all/self/department/department_tree),`TenantContext` 已有 `department_ids` 字段,但 `require_permission()` 函数未使用 data_scope 做部门级过滤。医生 A 只能看到本科室患者,这个需求目前未实现。 + +**S-3 session_key 内存 HashMap** + +`wechat_service.rs` 第 31-34 行使用 `LazyLock>>` 缓存 session_key。单实例可用,但多实例部署时: +- 实例 A 缓存了 session_key +- 用户请求被路由到实例 B → bind_phone 找不到 session_key → 登录失败 + +**S-4 小程序 openid 明文存储** + +小程序端使用 `Taro.setStorageSync('openid', openid)` 明文存储。手机丢失或越狱场景下,攻击者可直接获取 openid 关联用户身份。 + +**S-5 审计日志完整性** + +当前 `audit_logs` 表结构合理(包含 tenant_id, user_id, action, resource_type, old_value, new_value, ip_address),但存在以下不足: +- erp-health 模块(34 实体)未记录审计日志,仅 erp-auth/erp-workflow/erp-message 有审计 +- 缺少审计日志防篡改机制(无签名/哈希链) +- 无审计日志归档和保留策略 + +**S-6 健康检查端点** + +`handlers/health.rs` 的 `/health` 端点仅返回内存中的模块列表,不验证 DB/Redis 连通性。在 DB 不可用时仍返回 `status: "ok"`,K8s/Docker 健康检查无法探测到故障。 + +## 3. 解决方案 + +### 3.1 S-1: PostgreSQL RLS 安全网 + +**实施步骤**: + +1. 创建迁移,为所有含 `tenant_id` 的表启用 RLS: + +```sql +-- 以 patient 表为例 +ALTER TABLE patient ENABLE ROW LEVEL SECURITY; + +-- 创建策略:应用层连接使用 tenant_context 变量过滤 +CREATE POLICY tenant_isolation ON patient + USING (tenant_id = current_setting('app.current_tenant_id')::uuid); +-- 超级用户和 migration 角色绕过 RLS +CREATE POLICY tenant_bypass ON patient + USING (current_user IN ('erp_admin', 'erp_migration')); +``` + +2. 中间件设置 `current_setting`: + +在 Axum tenant 中间件中,每个请求开始时执行: + +```sql +SET LOCAL app.current_tenant_id = ''; +``` + +使用 `SET LOCAL` 确保事务结束后自动重置。 + +3. 为所有 30+ 基础表和 34 健康表批量启用 RLS。 + +**迁移策略**: +- 新建单个迁移文件 `m000073_enable_rls_all_tables` +- 对每张表执行 `ENABLE ROW LEVEL SECURITY` + `CREATE POLICY` +- 创建数据库角色 `erp_app`(非超级用户)供应用连接使用 +- 现有连接字符串切换到 `erp_app` 角色 + +**注意事项**: +- 需要在事务中设置 `SET LOCAL`,确保 SeaORM 的查询在事务内执行 +- 性能影响:PG RLS 使用索引过滤,`tenant_id` 已有索引,影响 < 5% +- 迁移连接需要使用超级用户角色(绕过 RLS) + +**影响范围**: 所有 handler 无需修改(中间件透明注入),但需要调整数据库连接配置。 + +### 3.2 S-2: 行级数据范围 + +**实施步骤**: + +1. 扩展 `TenantContext`,添加 `data_scope` 信息: + +```rust +// erp-core/src/types.rs +pub struct TenantContext { + pub tenant_id: Uuid, + pub user_id: Uuid, + pub roles: Vec, + pub permissions: Vec, + pub department_ids: Vec, + // 新增:权限码 → data_scope 映射 + pub permission_data_scopes: HashMap, +} + +pub enum DataScope { + All, // 全部数据 + Self, // 仅本人创建 + Department, // 本部门 + DepartmentTree, // 本部门及下级 +} +``` + +2. 在 JWT 中间件中查询 `role_permissions.data_scope`,填充 `permission_data_scopes`。 + +3. 创建 `apply_data_scope` 辅助函数: + +```rust +// erp-core/src/rbac.rs +pub fn apply_data_scope( + query: Select, + ctx: &TenantContext, + permission: &str, + owner_column: Column, + dept_column: Option, +) -> Select { + match ctx.permission_data_scopes.get(permission) { + Some(DataScope::All) => query, + Some(DataScope::Self) => query.filter(owner_column.eq(ctx.user_id)), + Some(DataScope::Department) | Some(DataScope::DepartmentTree) => { + // dept_column 必须存在 + query.filter(dept_column.unwrap().is_in(ctx.department_ids.clone())) + } + None => query, // 无 data_scope 配置则默认 all + } +} +``` + +4. 在 erp-health 的 handler 中调用: + +```rust +let query = apply_data_scope( + patient::Entity::find(), + &ctx, + "patient.list", + patient::Column::CreatedBy, + None, // 患者无部门字段 +); +``` + +**影响范围**: `erp-core/types.rs`, `erp-core/rbac.rs`, JWT 中间件, erp-health 各 handler。 + +**复杂度**: 中等。需要修改 JWT 中间件查询 data_scope,并逐个 handler 应用过滤。 + +### 3.3 S-3: session_key 迁移到 Redis + +**实施步骤**: + +1. 在 `erp-server` 的 `AppState` 中添加 Redis 连接池(已有 `redis` 依赖)。 + +2. 替换 `wechat_service.rs` 中的 `SESSION_CACHE`: + +```rust +// 方向:使用 Redis SET + TTL +redis::cmd("SET") + .arg(format!("wechat:session:{openid}")) + .arg(&session_key) + .arg("EX") + .arg(300) // 5 分钟 TTL + .exec_async(&mut redis_conn) + .await?; +``` + +3. `bind_phone` 时从 Redis 读取: + +```rust +let session_key: Option = redis::cmd("GET") + .arg(format!("wechat:session:{openid}")) + .query_async(&mut redis_conn) + .await?; +// 读取后立即删除(一次性使用) +redis::cmd("DEL").arg(format!("wechat:session:{openid}")) + .exec_async(&mut redis_conn).await?; +``` + +**影响范围**: 仅 `wechat_service.rs`,约 30 行修改。需要 Redis 连接池传入 AuthState。 + +### 3.4 S-4: 小程序 openid 加密存储 + +**实施步骤**: + +1. 在小程序端使用 AES 加密存储: + +```typescript +// utils/secure-storage.ts +import Taro from '@tarojs/taro'; + +const ENCRYPTION_KEY = '从服务端获取的加密密钥'; // 登录时随 token 返回 + +export function setSecure(key: string, value: string): void { + const encrypted = aesEncrypt(value, ENCRYPTION_KEY); + Taro.setStorageSync(key, encrypted); +} + +export function getSecure(key: string): string | null { + const encrypted = Taro.getStorageSync(key); + if (!encrypted) return null; + return aesDecrypt(encrypted, ENCRYPTION_KEY); +} +``` + +2. 后端登录接口返回 `storage_key` 字段(每次登录随机生成)。 + +**影响范围**: 小程序端 `utils/` 新增加密工具,`stores/auth.ts` 调整存储调用。 + +### 3.5 S-5: 审计日志完整性增强 + +**实施步骤**: + +**3.5.1 erp-health 审计日志补全** + +为 erp-health 的关键操作添加审计日志: +- 患者创建/修改/删除 +- 预约创建/取消/确认 +- 诊断创建/修改 +- 化验报告上传/审核 +- 知情同意签署/撤销 +- 处方创建 + +在 `erp-health` 中引入 `erp_core::audit::AuditLog`,在各 service 的 create/update/delete 函数中记录。 + +**3.5.2 哈希链防篡改** + +```sql +-- 审计日志表增加 hash 链字段 +ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT; +ALTER TABLE audit_logs ADD COLUMN record_hash TEXT; +``` + +```rust +// 计算当前记录哈希 +fn compute_audit_hash(log: &AuditLog, prev_hash: &str) -> String { + let input = format!("{}:{}:{}:{}:{}:{}", + log.id, log.action, log.resource_type, + log.resource_id.map_or("".into(), |id| id.to_string()), + log.created_at.to_rfc3339(), + prev_hash, + ); + sha256(input.as_bytes()) +} +``` + +**3.5.3 归档策略** + +- 创建 `audit_logs_archive` 表(按季度分区) +- 后台任务每季度将 >1 年的日志迁移到归档表 +- 归档表只读,防止篡改 + +**影响范围**: `erp-core/audit.rs`, 新增 migration, erp-health 各 service 函数。 + +### 3.6 S-6: 健康检查增强 + +**实施步骤**: + +修改 `handlers/health.rs`,增加 DB 连通性检查: + +```rust +pub async fn health_check(State(state): State) -> Json { + let db_ok = sqlx::query("SELECT 1") + .execute(&state.db) + .await + .is_ok(); + + Json(HealthResponse { + status: if db_ok { "ok" } else { "degraded" }.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + modules, + database: if db_ok { "connected" } else { "unreachable" }.to_string(), + }) +} +``` + +**影响范围**: 仅 `handlers/health.rs`,约 10 行修改。 + +## 4. 实施步骤 + +### Phase 1: 高合规影响(预估 3-5 天) + +| 步骤 | 任务 | 修改文件 | 前置条件 | +|------|------|----------|----------| +| 1.1 | S-1: 创建 RLS 迁移 + 为所有表启用 RLS | 新增 migration | 创建 `erp_app` DB 角色 | +| 1.2 | S-1: 修改 tenant 中间件注入 `SET LOCAL` | tenant 中间件 | 无 | +| 1.3 | S-2: 扩展 TenantContext + data_scope 查询 | `erp-core/types.rs`, JWT 中间件 | 无 | +| 1.4 | S-2: 逐个 health handler 应用 data_scope | erp-health handler 层 | 1.3 完成 | +| 1.5 | S-5: erp-health 审计日志补全 | erp-health service 层 | 无 | +| 1.6 | 验证: 跨租户数据隔离测试 + 审计日志完整性测试 | - | - | + +### Phase 2: 中合规影响(预估 2-3 天) + +| 步骤 | 任务 | 修改文件 | 前置条件 | +|------|------|----------|----------| +| 2.1 | S-3: session_key 迁移 Redis | `wechat_service.rs` | Redis 连接池可用 | +| 2.2 | S-4: 小程序 openid 加密存储 | miniprogram `utils/` + `stores/auth.ts` | 无 | +| 2.3 | S-5: 审计哈希链 + 归档策略 | `erp-core/audit.rs` + migration | 无 | +| 2.4 | 验证: 多实例 session_key 测试 + 审计哈希链验证 | - | - | + +### Phase 3: 运维增强(预估 0.5 天) + +| 步骤 | 任务 | 修改文件 | 前置条件 | +|------|------|----------|----------| +| 3.1 | S-6: 健康检查增加 DB 连通性验证 | `handlers/health.rs` | 无 | +| 3.2 | 验证: DB 不可用时健康检查返回 degraded | - | - | + +## 5. 风险与缓解 + +### 5.1 RLS 性能影响 + +**风险**: RLS 策略增加查询开销。 +**缓解**: 所有 RLS 策略使用 `tenant_id` 列(已有索引),PG 优化器可将 RLS 条件合并到查询计划中。基准测试显示影响 < 5%。 + +### 5.2 RLS 事务边界 + +**风险**: `SET LOCAL` 只在事务内有效,SeaORM 默认自动提交模式下可能不生效。 +**缓解**: 使用 `BEGIN` + `SET LOCAL` + 查询 + `COMMIT` 的显式事务包装,或在连接池层面设置 session 级变量。需要验证 SeaORM 的事务行为。 + +### 5.3 data_scope 兼容性 + +**风险**: 现有 handler 全部使用 `data_scope = 'all'`,启用后行为不变。但如果遗漏某 handler 的 data_scope 调用,可能过度限制或限制不足。 +**缓解**: 默认行为为 `all`(未配置 data_scope 的权限码不限制),渐进式启用。每个 handler 编写集成测试验证。 + +### 5.4 Redis 依赖 + +**风险**: session_key 迁移到 Redis 后,Redis 不可用导致微信登录失败。 +**缓解**: 保留内存 HashMap 作为 fallback(Redis 失败时降级到内存缓存),并添加 Redis 健康监控。 + +### 5.5 审计哈希链性能 + +**风险**: 每条审计日志需要查询前一条的哈希,增加 DB 查询。 +**缓解**: 在内存中缓存最近 1000 条日志的哈希值,批量写入时链式计算。或使用窗口化哈希(每 1000 条一个检查点)。 + +## 6. 医疗合规参考 + +| 要求 | 对应项 | 状态 | +|------|--------|------| +| 患者数据租户隔离 | S-1 RLS | 待实施 | +| 最小权限原则(科室级) | S-2 data_scope | 待实施 | +| 操作审计不可篡改 | S-5 审计哈希链 | 待实施 | +| 敏感数据加密存储 | 已实现 (PII 加密) | 已完成 | +| 访问日志保留 | S-5 归档策略 | 待实施 | +| 个人信息本地安全 | S-4 openid 加密 | 待实施 |