From b410fa9f78ecfbcd700d2f64004cf3740affe87a Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 27 Apr 2026 08:00:50 +0800 Subject: [PATCH] =?UTF-8?q?docs:=205=20=E4=BB=BD=E5=AE=9E=E6=96=BD?= =?UTF-8?q?=E8=AE=A1=E5=88=92=20=E2=80=94=20=E6=80=A7=E8=83=BD/=E5=AE=89?= =?UTF-8?q?=E5=85=A8/=E4=BA=8B=E4=BB=B6/=E5=89=8D=E7=AB=AF/=E5=8F=AF?= =?UTF-8?q?=E8=A7=82=E6=B5=8B=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对应 5 份设计规格,共 75 个 Task: 1. 性能优化 (12 Task) — 批量INSERT/N+1内联name/合并COUNT/按需重绘/chunk拆分 2. 安全纵深防御 (8 Task) — RLS/行级数据范围/Redis session_key/审计哈希链 3. 事件驱动架构 (10 Task) — 11个缺失事件补发/LISTEN+NOTIFY/schema版本化 4. 前端工程化 (10 Task) — hook统一/组件拆分/Bundle优化 5. 可观测性运维 (10 Task) — 深度健康检查/Prometheus/OTel/生产Docker/告警 --- .../2026-04-26-event-driven-architecture.md | 106 +++++++ .../plans/2026-04-26-frontend-engineering.md | 290 ++++++++++++++++++ .../plans/2026-04-26-observability-and-ops.md | 200 ++++++++++++ .../2026-04-26-performance-optimization.md | 124 ++++++++ .../2026-04-26-security-defense-in-depth.md | 89 ++++++ 5 files changed, 809 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-26-event-driven-architecture.md create mode 100644 docs/superpowers/plans/2026-04-26-frontend-engineering.md create mode 100644 docs/superpowers/plans/2026-04-26-observability-and-ops.md create mode 100644 docs/superpowers/plans/2026-04-26-performance-optimization.md create mode 100644 docs/superpowers/plans/2026-04-26-security-defense-in-depth.md diff --git a/docs/superpowers/plans/2026-04-26-event-driven-architecture.md b/docs/superpowers/plans/2026-04-26-event-driven-architecture.md new file mode 100644 index 0000000..0d65814 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-event-driven-architecture.md @@ -0,0 +1,106 @@ +# 事件驱动架构增强实施计划 + +> 设计规格: `docs/superpowers/specs/2026-04-26-event-driven-architecture-design.md` +> 日期: 2026-04-26 | 状态: draft | 总周期: 2 周 + +--- + +## Phase 1: 高优先级事件补发(Week 1) + +### Task 1: dialysis_service 添加 dialysis_record.created/reviewed 事件 + +**涉及文件**: `crates/erp-health/src/service/dialysis_service.rs` + +**步骤**: `create_dialysis_record()` 成功后发布 `dialysis_record.created`(data: patient_id, dialysis_type, status, dialysis_date, duration, ultrafiltration_volume)。审核状态变更时发布 `dialysis_record.reviewed`(data: patient_id, reviewer_id, complication_notes)。payload 遵循统一信封(schema_version: "v1")。发布失败仅 warn 不阻断业务。 + +**验收**: 创建/审核后 domain_events 表出现对应事件;`cargo test` 通过。 + +### Task 2: diagnosis_service 添加 diagnosis.created/updated 事件 + +**涉及文件**: `crates/erp-health/src/service/diagnosis_service.rs` + +**步骤**: `create_diagnosis()` 后发布 `diagnosis.created`(data: patient_id, icd_code, diagnosis_name, severity)。`update_diagnosis()` 后发布 `diagnosis.updated`,计算变更 diff(changed_fields[], old_values{}, new_values{})。 + +**验收**: diagnosis.updated 事件 data 含 changed_fields 差异;`cargo test` 通过。 + +### Task 3: consent_service 添加 consent.granted/revoked 事件 + +**涉及文件**: `crates/erp-health/src/service/consent_service.rs` + +**步骤**: 签署时发布 `consent.granted`(data: patient_id, consent_type, consent_scope, granted_by, expires_at)。撤销时发布 `consent.revoked`(data: patient_id, consent_type, revoked_by, reason)。 + +**验收**: 签署/撤销后 domain_events 表出现事件;`cargo test` 通过。 + +--- + +## Phase 2: 中低优先级事件 + Outbox 优化(Week 2) + +### Task 4: points_service 添加 points.earned/exchanged 事件 + +**涉及文件**: `crates/erp-health/src/service/points_service.rs` + +**步骤**: earn 成功后发布 `points.earned`(data: patient_id, points, source_type, balance_after)。exchange 成功后发布 `points.exchanged`(data: patient_id, points, product_name, order_id, balance_after)。确保在事务提交后发布。 + +**验收**: 积分变动后 domain_events 出现事件;balance_after 正确反映余额。 + +### Task 5: article_service 添加 article.published/rejected 事件 + +**涉及文件**: `crates/erp-health/src/service/article_service.rs` + +**步骤**: 审核通过发布 `article.published`(data: title, author_id, category_id, tags[])。审核驳回发布 `article.rejected`(data: title, reviewer_id, reason)。 + +**验收**: 审核操作后 domain_events 出现事件;`cargo test` 通过。 + +### Task 6: daily_monitoring_service 添加 daily_monitoring.created 事件 + +**涉及文件**: `crates/erp-health/src/service/daily_monitoring_service.rs` + +**步骤**: 记录创建后发布 `daily_monitoring.created`(data: patient_id, monitoring_date, monitoring_type, values{})。 + +**验收**: 创建记录后 domain_events 出现事件;`cargo test` 通过。 + +### Task 7: Outbox relay 从轮询改为 LISTEN/NOTIFY + +**涉及文件**: `crates/erp-server/src/outbox.rs`, `crates/erp-core/src/events.rs` + +**步骤**: `EventBus::publish()` 持久化后执行 `NOTIFY outbox_channel, ''`。outbox relay 用 `sqlx::PgListener` 监听 + `tokio::select!`(LISTEN 触发 + 30s 兜底轮询)。保留 `process_pending_events()` 不变,仅改变触发方式。PgListener 添加断线自动重连。 + +**验收**: 事件延迟 < 100ms;DB 轮询频率从 5s 降为 30s 兜底;`cargo test --workspace` 通过。 + +--- + +## Phase 3: 事件 schema 版本化 + 清理(Week 2) + +### Task 8: 事件 payload 添加 schema_version 字段 + +**涉及文件**: `crates/erp-core/src/events.rs`, `crates/erp-health/src/service/` 下所有发布事件的 service + +**步骤**: 在 erp-core 创建 `build_event_payload()` 辅助函数,自动填充 schema_version/timestamp/metadata。逐个 service(14 个模块)替换手动构建为调用辅助函数,统一信封格式。 + +**验收**: 所有事件 payload 含 schema_version 字段;`cargo test --workspace` 通过。 + +### Task 9: Outbox 表分区或定期清理策略 + +**涉及文件**: `migration/src/m000075_domain_events_cleanup.rs`(新增), `erp-server/src/tasks/events_cleanup.rs`(新增) + +**步骤**: 迁移创建 `domain_events_archive` 表,添加 `cleanup_old_published_events()` SQL 函数(>90 天 published 事件迁移到归档表)。后台任务每日执行清理。归档表只读防篡改。 + +**验收**: 清理任务正确迁移 >90 天事件;`cargo test` 通过。 + +### Task 10: 消费者幂等性(dedup key 检查) + +**涉及文件**: `migration/src/m000076_processed_events.rs`(新增), `crates/erp-core/src/events.rs` + +**步骤**: 迁移创建 `processed_events` 表(event_id + consumer_id 联合主键 + processed_at)。erp-core 添加 `is_processed()` / `mark_processed()` 辅助函数。消费者模式:收到事件 -> 查已处理 -> 跳过或执行 -> 插入记录。添加 7 天 TTL 清理任务。 + +**验收**: 重复消费同一事件时第二次被跳过;`cargo test --workspace` 通过。 + +--- + +## 执行原则 + +1. **每 Task 完成后立即提交** — 不积压 +2. **Phase 1 优先** — P0 事件(透析/诊断)是核心医疗流程 +3. **事件发布不阻断业务** — publish 失败仅 warn,Outbox relay 兜底 +4. **统一信封格式** — 使用 `build_event_payload` 保证一致性 +5. **LISTEN/NOTIFY 保留兜底轮询** — 30s 轮询防 NOTIFY 丢失 diff --git a/docs/superpowers/plans/2026-04-26-frontend-engineering.md b/docs/superpowers/plans/2026-04-26-frontend-engineering.md new file mode 100644 index 0000000..1c1778a --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-frontend-engineering.md @@ -0,0 +1,290 @@ +# 前端工程化改进实施计划 + +> 设计规格: `docs/superpowers/specs/2026-04-26-frontend-engineering-design.md` +> 日期: 2026-04-26 | 状态: draft | 总周期: 7 天 + +--- + +## Phase 1: 重复模式统一(Day 1-2) + +### Task 1: 增强 useApiRequest hook,统一错误处理 + +**目标**: 补齐 loading 状态,消除组件内联 `catch (err) { message.error(...) }` 模式。 + +**涉及文件**: +- 修改: `apps/web/src/hooks/useApiRequest.ts` + +**详细步骤**: + +1. 在 `useApiRequest` 返回值中新增 `loading: boolean` 状态: +```typescript +interface UseApiRequestReturn { + execute: (fn: () => Promise, successMsg?: string) => Promise; + loading: boolean; +} +``` +2. `execute` 内部在调用前 `setLoading(true)`,finally 中 `setLoading(false)` +3. 保持现有调用点无需修改 — 返回值是对象解构,新增字段不影响旧代码 +4. 选取 3 个健康模块页面(PatientList、AppointmentList、FollowUpTaskList)迁移为使用 `execute` + `loading` + +**验收标准**: +- `pnpm build` 通过 +- 3 个迁移页面的 catch 块不再有内联 `message.error`,统一走 `handleApiError` +- loading 状态正确绑定到页面按钮/Spin 组件 + +--- + +### Task 2: 增强 usePaginatedData hook,健康模块页面迁移 + +**目标**: 支持泛型筛选参数,迁移 6 个健康列表页使用统一 hook。 + +**涉及文件**: +- 修改: `apps/web/src/hooks/usePaginatedData.ts` +- 修改: `apps/web/src/pages/health/PatientList.tsx` +- 修改: `apps/web/src/pages/health/OfflineEventList.tsx` +- 修改: `apps/web/src/pages/health/PointsProductList.tsx` + +**详细步骤**: + +1. 增强 hook 签名为泛型筛选: +```typescript +function usePaginatedData( + fetchFn: (page: number, pageSize: number, filters: F) => Promise<{ data: T[]; total: number }>, + options?: { pageSize?: number; defaultFilters: F; autoFetch?: boolean } +): { data, total, page, loading, filters, setFilters, refresh } +``` +2. 函数重载保持旧 `(fetchFn, pageSize?)` 签名兼容 +3. 新增 `filters` / `setFilters` 状态,`fetchFn` 调用时传入当前 filters +4. 迁移 PatientList(按 status/name/gender 筛选)和 OfflineEventList(按 status/dateRange 筛选) + +**验收标准**: +- 旧调用点(不传 filters)行为不变 +- PatientList 和 OfflineEventList 筛选功能正常,代码行数各减少 15-25 行 +- `pnpm build` 通过 + +--- + +### Task 3: 移除 nameCache,统一用 useHealthStore + +**目标**: 消除 AppointmentList 和 PointsOrderList 自建的 `useState>` nameCache。 + +**涉及文件**: +- 修改: `apps/web/src/stores/health.ts` +- 修改: `apps/web/src/pages/health/AppointmentList.tsx` +- 修改: `apps/web/src/pages/health/PointsOrderList.tsx` + +**详细步骤**: + +1. 在 `useHealthStore` 新增批量解析方法: + - `batchResolvePatientNames(ids: string[]): Promise>` + - `batchResolveDoctorNames(ids: string[]): Promise>` +2. 内部实现:去重 → 过滤已缓存 → 并发加载(限制 5 并发)→ 写入缓存并返回 +3. 在 AppointmentList 中移除 nameCache state,改用 store 方法 +4. 在 PointsOrderList 中同样迁移 + +**验收标准**: +- 两个页面无 `useState>` nameCache 代码 +- 患者姓名/医生姓名在列表中正确显示 +- `pnpm build` 通过 + +--- + +## Phase 2: 大组件拆分(Day 3-5) + +### Task 4: PluginCRUDPage 拆分为 CRUDTable/CRUDForm/DetailDrawer/ImportExport + +**目标**: 将 872 行的 PluginCRUDPage.tsx 拆为容器 + 展示组件。 + +**涉及文件**: +- 新增: `apps/web/src/pages/plugins/components/CRUDTable.tsx` (~150 行) +- 新增: `apps/web/src/pages/plugins/components/CRUDForm.tsx` (~180 行) +- 新增: `apps/web/src/pages/plugins/components/DetailDrawer.tsx` (~80 行) +- 新增: `apps/web/src/pages/plugins/components/ImportExport.tsx` (~100 行) +- 新增: `apps/web/src/pages/plugins/hooks/usePluginData.ts` (~120 行) +- 修改: `apps/web/src/pages/plugins/PluginCRUDPage.tsx` (缩减至 ~80 行) + +**详细步骤**: + +1. 创建 `hooks/usePluginData.ts`:提取 CRUD 操作、导入导出逻辑、Drawer 可见性状态 +2. 创建 `CRUDTable.tsx`:表格列定义 + 行操作按钮,props 接收 data/onDelete/onEdit/onDetail +3. 创建 `CRUDForm.tsx`:新增/编辑表单 + Drawer,包含校验规则 +4. 创建 `DetailDrawer.tsx`:详情展示 + 操作历史 Timeline +5. 创建 `ImportExport.tsx`:导入面板 + 导出按钮 +6. 改写 `PluginCRUDPage.tsx` 为容器组件:调用 usePluginData hook,组装子组件 + +**验收标准**: +- `pnpm build` 通过 +- 插件 CRUD 所有功能正常(新增、编辑、删除、详情、导入、导出) +- PluginCRUDPage.tsx <= 100 行,无子组件超过 200 行 + +--- + +### Task 5: PluginGraphPage 抽取 useGraphCanvas hook + +**目标**: 将 759 行的 PluginGraphPage.tsx 拆为 hook + 展示组件。 + +**涉及文件**: +- 新增: `apps/web/src/pages/plugins/hooks/useGraphLayout.ts` (~100 行) +- 新增: `apps/web/src/pages/plugins/hooks/useGraphData.ts` (~80 行) +- 新增: `apps/web/src/pages/plugins/components/GraphCanvas.tsx` (~200 行) +- 新增: `apps/web/src/pages/plugins/components/GraphToolbar.tsx` (~60 行) +- 修改: `apps/web/src/pages/plugins/PluginGraphPage.tsx` (缩减至 ~60 行) + +**详细步骤**: + +1. `useGraphData.ts`:数据加载、边/节点格式转换、字段映射 +2. `useGraphLayout.ts`:Dagre/elkjs 布局算法、节点位置计算、自动布局触发 +3. `GraphCanvas.tsx`:ReactFlow 渲染、自定义节点样式、拖拽交互 +4. `GraphToolbar.tsx`:缩放控制、自动布局、布局方向切换 +5. 容器组件组装以上模块 + +**验收标准**: +- 插件关系图页面正常渲染和交互 +- 拖拽节点、自动布局、缩放功能正常 +- `pnpm build` 通过 + +--- + +### Task 6: Organizations.tsx 抽象 TreeEntityManager + +**目标**: 将 622 行的 Organizations.tsx 按三层模式拆分。 + +**涉及文件**: +- 新增: `apps/web/src/pages/system/hooks/useOrgTree.ts` (~80 行) +- 新增: `apps/web/src/pages/system/components/OrgTree.tsx` (~120 行) +- 新增: `apps/web/src/pages/system/components/OrgDetail.tsx` (~150 行) +- 新增: `apps/web/src/pages/system/components/DeptMemberList.tsx` (~100 行) +- 修改: `apps/web/src/pages/system/Organizations.tsx` (缩减至 ~60 行) + +**详细步骤**: + +1. `useOrgTree.ts`:树数据加载、CRUD 操作、选中节点状态 +2. `OrgTree.tsx`:左侧树形选择(DirectoryTree + 搜索 + 右键菜单) +3. `OrgDetail.tsx`:右侧组织详情/编辑表单 +4. `DeptMemberList.tsx`:部门成员列表 + 人员分配 Modal +5. 容器组件三栏布局组装 + +**验收标准**: +- 组织管理 CRUD 功能正常(新增/编辑/删除组织、部门、人员分配) +- 树形选择、搜索过滤正常 +- `pnpm build` 通过 + +--- + +### Task 7: StatisticsDashboard 拆分为独立卡片组件 + +**目标**: 将 580 行的 StatisticsDashboard.tsx 拆为 hook + 独立图表卡片。 + +**涉及文件**: +- 新增: `apps/web/src/pages/health/hooks/useStatsData.ts` (~100 行) +- 新增: `apps/web/src/pages/health/components/PatientTrendChart.tsx` (~80 行) +- 新增: `apps/web/src/pages/health/components/AppointmentStats.tsx` (~80 行) +- 新增: `apps/web/src/pages/health/components/OverviewCards.tsx` (~60 行) +- 新增: `apps/web/src/pages/health/components/TimeRangeSelector.tsx` (~40 行) +- 修改: `apps/web/src/pages/health/StatisticsDashboard.tsx` (缩减至 ~50 行) + +**详细步骤**: + +1. `useStatsData.ts`:五个统计 API 并行加载、loading/error 状态、时间范围变更触发刷新 +2. `PatientTrendChart.tsx`:患者趋势折线图(@ant-design/charts Line) +3. `AppointmentStats.tsx`:预约统计饼图/柱状图 +4. `OverviewCards.tsx`:概览数字卡片组(Statistic + Card) +5. `TimeRangeSelector.tsx`:日期范围选择 + 快捷选项(近7天/近30天/近90天) +6. 容器组件组装,布局使用 Row + Col + +**验收标准**: +- 统计仪表板页面渲染正常,图表数据正确 +- 时间范围切换触发数据刷新 +- `pnpm build` 通过 + +--- + +## Phase 3: Bundle 优化(Day 6-7) + +### Task 8: vite.config.ts manualChunks 拆分重型依赖 + +**目标**: 将 @ant-design/charts、@xyflow/react、@wangeditor/editor 拆为独立 chunk,降低主 chunk 体积。 + +**涉及文件**: +- 修改: `apps/web/vite.config.ts` + +**详细步骤**: + +1. 在 `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'; +``` +2. 对应页面添加路由级 `React.lazy()`: + - `StatisticsDashboard` → `lazy(() => import('./health/StatisticsDashboard'))` + - `PluginGraphPage` → `lazy(() => import('./plugins/PluginGraphPage'))` + - `ArticleEditor` → `lazy(() => import('./health/ArticleEditor'))` +3. 将 `chunkSizeWarningLimit` 从 600 降至 500 +4. 运行 `pnpm build` 对比拆分前后各 chunk 大小 + +**验收标准**: +- 主 chunk 体积 < 400KB(gzip 前约 600KB 以内) +- `vendor-charts`、`vendor-flow`、`vendor-editor` 独立生成 +- `pnpm build` 无警告 +- 统计仪表板、插件关系图、文章编辑器页面功能正常(懒加载无闪烁) + +--- + +### Task 9: columns 配置 useMemo 化 + +**目标**: 消除 PluginCRUDPage 和健康模块列表页的 columns 重复创建,减少不必要的 re-render。 + +**涉及文件**: +- 修改: `apps/web/src/pages/plugins/components/CRUDTable.tsx`(Phase 2 Task 4 产物) +- 修改: `apps/web/src/pages/health/PatientList.tsx` +- 修改: `apps/web/src/pages/health/AppointmentList.tsx` +- 修改: `apps/web/src/pages/health/FollowUpTaskList.tsx` + +**详细步骤**: + +1. 在每个列表页中,将 `columns` 数组定义包裹在 `useMemo` 中 +2. 依赖项包含 columns 中引用的回调函数(如 onDelete、onEdit) +3. 确保回调函数通过 `useCallback` 缓存,避免 useMemo 失效 +4. 使用 React DevTools Profiler 验证翻页/筛选时减少不必要渲染 + +**验收标准**: +- 列表翻页时 Table 组件不因 columns 引用变化触发全量渲染 +- 所有列表页功能正常(排序、筛选、操作按钮) +- `pnpm build` 通过 + +--- + +### Task 10: API 层新代码统一为对象风格 + +**目标**: 确认新增 API 文件采用对象风格(`xxxApi.list()` 而非 `listXxx()`),修改已有文件时顺手迁移。 + +**涉及文件**: +- 修改: `apps/web/src/api/health/` 下近期新增的 API 文件(如 `alerts.ts`、`deviceReadings.ts`) + +**详细步骤**: + +1. 审计 `apps/web/src/api/` 下所有文件,标记函数风格的文件清单 +2. 近期新增的文件(alerts、deviceReadings 等)统一改为对象风格: +```typescript +export const alertApi = { + list: (params) => client.get('/alerts', { params }), + acknowledge: (id) => client.post(`/alerts/${id}/acknowledge`), +}; +``` +3. 更新引用处的 import(页面组件中的调用方式) +4. 旧文件不强制迁移,仅记录待迁移清单 + +**验收标准**: +- `alerts.ts` 和 `deviceReadings.ts` 为对象风格导出 +- 对应页面功能正常 +- `pnpm build` 通过 + +--- + +## 执行原则 + +1. **每 Task 完成后立即提交** — 不积压,保持可追溯 +2. **先基础设施后拆分** — Phase 1 的 hook 增强完成后再做 Phase 2 组件拆分 +3. **每步验证** — 每个 Task 完成后 `pnpm build` 验证,拆分任务额外验证页面功能 +4. **渐进迁移** — 重复模式统一采用渐进策略,不一次性全量迁移 diff --git a/docs/superpowers/plans/2026-04-26-observability-and-ops.md b/docs/superpowers/plans/2026-04-26-observability-and-ops.md new file mode 100644 index 0000000..d940d12 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-observability-and-ops.md @@ -0,0 +1,200 @@ +# 可观测性与运维基础设施实施计划 + +> 设计规格: `docs/superpowers/specs/2026-04-26-observability-and-ops-design.md` +> 日期: 2026-04-26 | 总周期: 7-9 天 + +--- + +## Phase 1: 健康检查 + Prometheus 指标(Day 1-2) + +### Task 1: 深度健康检查端点 + +**涉及文件**: +- 修改: `crates/erp-server/src/handlers/health.rs` + +**步骤**: +1. 拆分为两个端点: + - `GET /health/live` — 存活探针(仅返回 `{ status: "ok" }`,不依赖任何外部服务) + - `GET /health/ready` — 就绪探针(验证 DB ping + Redis ping + 模块状态) +2. `/health/ready` 实现: +```rust +async fn health_ready(State(state): State) -> Json { + let db_ok = sql_query("SELECT 1").execute(&state.db).await.is_ok(); + let redis_ok = state.redis.ping().await.is_ok(); + Json(HealthResponse { status: if db_ok && redis_ok { "ok" } else { "degraded" }, db: db_ok, redis: redis_ok, ... }) +} +``` +3. 保持旧 `GET /health` 兼容(重定向到 `/health/ready`) + +**验收**: `/health/ready` 在 DB/Redis 正常时返回 200,任一不可达时返回 503 + 降级详情 + +### Task 2: Prometheus 指标基础 + +**涉及文件**: +- 修改: `crates/erp-server/Cargo.toml`(添加 `metrics` + `metrics-exporter-prometheus` 依赖) +- 新增: `crates/erp-server/src/middleware/metrics.rs` +- 修改: `crates/erp-server/src/main.rs`(注册 metrics middleware + 路由) + +**步骤**: +1. 在 `Cargo.toml` 添加: +```toml +metrics = "0.24" +metrics-exporter-prometheus = "0.16" +``` +2. 创建 `metrics.rs` Axum middleware: + - 记录每个请求的 `http_request_duration_seconds`(直方图,按 method/path/status 标签) + - 记录 `http_requests_total`(计数器) +3. 在 `main.rs` 启动 Prometheus exporter(`/metrics` 端点,端口 9090) +4. 在 AppState 中注册 metrics recorder + +**验收**: `curl localhost:9090/metrics` 返回 Prometheus 格式指标,包含请求延迟直方图 + +### Task 3: DB 连接池 + EventBus 积压指标 + +**涉及文件**: +- 修改: `crates/erp-server/src/main.rs` +- 修改: `crates/erp-core/src/events.rs` + +**步骤**: +1. DB 连接池指标:每 30 秒采样 `db_pool_connections_active` / `db_pool_connections_idle` +2. EventBus 积压指标:在 `publish()` 中递增 `eventbus_pending_total`,在 relay 处理后递减 +3. 在 `/metrics` 端点暴露 + +**验收**: `/metrics` 包含 DB 连接池使用率和事件积压计数 + +--- + +## Phase 2: OpenTelemetry + 生产 Docker(Day 3-5) + +### Task 4: OpenTelemetry 条件集成 + +**涉及文件**: +- 修改: `crates/erp-server/Cargo.toml`(添加 `opentelemetry` + `tracing-opentelemetry` + `opentelemetry-otlp`,optional feature) +- 新增: `crates/erp-server/src/telemetry.rs` +- 修改: `crates/erp-server/src/main.rs` + +**步骤**: +1. 添加 optional 依赖: +```toml +[features] +tracing = ["opentelemetry", "tracing-opentelemetry", "opentelemetry-otlp"] +``` +2. 创建 `telemetry.rs`:条件初始化 OpenTelemetry tracer(环境变量 `ERP__TELEMETRY__ENABLED=true` 时启用) +3. 配置 OTLP exporter(默认 `http://localhost:4317`,可通过环境变量覆盖) +4. 在 `main.rs` 的 tracing subscriber 中条件注册 OpenTelemetry layer +5. 在 SeaORM 的 `DatabaseConnection` 包装中添加 span(记录查询耗时) + +**验收**: 启用后 Jaeger/Tempo 可看到请求 → SQL 查询 → 事件发布的完整链路;不启用时零开销 + +### Task 5: 生产 Docker 多阶段构建 + +**涉及文件**: +- 新增: `Dockerfile`(项目根目录) +- 新增: `docker/docker-compose.production.yml` + +**步骤**: +1. 多阶段 Dockerfile: +```dockerfile +# Stage 1: Build +FROM rust:1.82-bookworm AS builder +COPY . /app +RUN cargo build --release -p erp-server + +# Stage 2: Runtime +FROM debian:bookworm-slim +COPY --from=builder /app/target/release/erp-server /usr/local/bin/ +COPY --from=builder /app/crates/erp-server/config/default.toml /etc/erp/config.toml +EXPOSE 3000 9090 +CMD ["erp-server"] +``` +2. `docker-compose.production.yml`: + - erp-server 服务(限制 1 CPU / 512MB) + - PostgreSQL 16 + Redis 7 作为独立服务 + - 健康检查配置(使用 /health/ready) + - 环境变量注入(JWT secret / DB URL / Redis URL 通过 secrets) + +**验收**: `docker build -t hms-server .` 成功,运行时镜像 < 80MB + +### Task 6: 前端生产构建 + Nginx + +**涉及文件**: +- 新增: `apps/web/Dockerfile` +- 新增: `apps/web/nginx.conf` + +**步骤**: +1. 多阶段构建:node 构建 → nginx 运行 +2. Nginx 配置:SPA fallback + `/api` 代理到后端 3000 端口 + +**验收**: Docker 内前端可正常访问,API 代理工作 + +--- + +## Phase 3: 日志聚合 + 告警(Day 5-7) + +### Task 7: Grafana Loki 日志集成 + +**涉及文件**: +- 新增: `docker/loki-config.yaml` +- 修改: `docker/docker-compose.production.yml` + +**步骤**: +1. 在 production compose 中添加 Loki + Promtail 服务 +2. Promtail 配置:读取 erp-server 的 JSON 日志输出 +3. Grafana 数据源配置:Loki + Prometheus + +**验收**: Grafana 可查询和过滤后端日志 + +### Task 8: Prometheus 告警规则 + +**涉及文件**: +- 新增: `docker/alert-rules.yml` + +**步骤**: +1. 定义 5 条告警规则: +```yaml +- alert: HighRequestLatency # P95 > 2s 持续 5 分钟 +- alert: HighErrorRate # 5xx 比率 > 5% 持续 3 分钟 +- alert: EventBusBacklog # 积压事件 > 100 持续 5 分钟 +- alert: DatabasePoolExhausted # 活跃连接 > 90% 持续 2 分钟 +- alert: HealthCheckDegraded # /health/ready 非 ok 持续 1 分钟 +``` +2. 配置 Alertmanager 通知渠道(Webhook/邮件) + +**验收**: 触发告警条件时 Alertmanager 发送通知 + +### Task 9: Grafana Dashboard 模板 + +**涉及文件**: +- 新增: `docker/grafana/dashboards/hms-overview.json` + +**步骤**: +1. 创建 HMS Overview Dashboard,包含面板: + - 请求速率 + 延迟分布(P50/P95/P99) + - 错误率趋势(按 status code 分组) + - DB 连接池使用率 + - EventBus 发布/消费速率 + - 健康检查状态 + +**验收**: Dashboard 展示实时指标 + +### Task 10: 运维文档 + +**涉及文件**: +- 新增: `wiki/observability.md` + +**步骤**: +1. 记录监控端点(/health/live, /health/ready, /metrics) +2. 记录告警规则和响应流程 +3. 记录日志查询方法(Grafana Loki) +4. 记录 Docker 部署命令 + +**验收**: 新团队成员可通过文档独立部署和排查问题 + +--- + +## 执行原则 + +1. **条件编译** — OpenTelemetry 使用 feature gate,不启用时零开销 +2. **渐进式** — Phase 1 可独立上线(无外部依赖),Phase 2/3 需要 Docker 环境 +3. **性能优先** — 指标收集使用 `metrics` crate 的无锁实现,不影响请求延迟 +4. **端口分离** — 业务 API (3000) + Metrics (9090) 分离,避免暴露内部指标 diff --git a/docs/superpowers/plans/2026-04-26-performance-optimization.md b/docs/superpowers/plans/2026-04-26-performance-optimization.md new file mode 100644 index 0000000..37665ef --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-performance-optimization.md @@ -0,0 +1,124 @@ +# 性能优化实施计划 + +> 设计规格: `docs/superpowers/specs/2026-04-26-performance-optimization-design.md` +> 日期: 2026-04-26 | 状态: draft | 总周期: 3 周 + +--- + +## Phase 1: 后端批量插入优化(Week 1, P0) + +### Task 1: device_reading_service batch_insert_readings 改为 SeaORM insert_many + +**涉及文件**: `crates/erp-health/src/service/device_reading_service.rs` + +**步骤**: 将 `batch_insert_readings()` 中 for 循环逐条 `model.insert(db).await` 替换为构建 `Vec` 后调用 `insert_many()` + `on_conflict(columns([...]).do_nothing())`。补充 50 条模拟数据的批量插入测试。 + +**验收**: `cargo test -p erp-health device_reading` 通过;500 条插入延迟 < 100ms。 + +### Task 2: device_reading_service upsert_hourly_aggregates 批量化 + +**涉及文件**: `crates/erp-health/src/service/device_reading_service.rs` + +**步骤**: 批量查出已存在的聚合记录(按 patient_id + device_type + hour),分为"新增"和"更新"两组,分别 `insert_many` 和批量 `update`,事务包装。补充同一小时多次 upsert 的聚合精度测试。 + +**验收**: 聚合值(avg/min/max)精度与优化前一致;`cargo test` 通过。 + +--- + +## Phase 2: 前端 N+1 根治(Week 2, P0) + +### Task 3: 后端 appointment_service list 返回 patient_name/doctor_name + +**涉及文件**: `crates/erp-health/src/service/appointment_service.rs`, `dto/mod.rs`, `handler/mod.rs` + +**步骤**: 列表 DTO 增加 `patient_name/doctor_name` 字段,service 查询中 `find_also_related` 或子查询关联 users 表获取 display_name,handler 层映射到响应 DTO。保持 Option 类型向后兼容。 + +**验收**: `GET /api/v1/health/appointments` 每条记录含 name 字段。 + +### Task 4: 后端 consultation_service / follow_up_service 同样内联 name + +**涉及文件**: `consultation_service.rs`, `follow_up_service.rs`, `dto/mod.rs` + +**步骤**: consultation list 添加 patient_name/doctor_name,follow_up list 添加 patient_name,复用 Task 3 的 JOIN 模式。 + +**验收**: 两个列表 API 响应均含 name 字段;`cargo test` 通过。 + +### Task 5: 前端 AppointmentList 移除 nameCache,改用内联字段 + +**涉及文件**: `apps/web/src/pages/health/AppointmentList.tsx` + +**步骤**: 移除 `nameCache` useState 及逐条请求 name 的 useEffect,表格列直接使用后端返回的 `patient_name/doctor_name`,消除 fetchData 对 nameCache 的依赖。 + +**验收**: Network 面板仅 1 个列表 API 请求;首屏 < 500ms(20 条记录)。 + +### Task 6: 前端 ConsultationList / FollowUpTaskList 同样改造 + +**涉及文件**: `ConsultationList.tsx`, `FollowUpTaskList.tsx` + +**步骤**: 两个页面移除 nameCache,使用内联 name 字段。验证无 N+1 请求。 + +**验收**: 两个页面 Network 面板无 N+1 请求;`pnpm build` 通过。 + +--- + +## Phase 3: 后端查询优化(Week 3, P1) + +### Task 7: stats_service 合并多次 COUNT 为 GROUP BY + +**涉及文件**: `crates/erp-health/src/service/stats_service.rs` + +**步骤**: 6 个统计函数从多次 COUNT 合并为 `SELECT status, COUNT(*) GROUP BY status` + 应用层 HashMap 聚合。`compute_avg_field` 用宏生成静态 SQL 常量替代 `format!` 拼接。编写对比测试确认 GROUP BY 与多次 COUNT 结果一致。 + +**验收**: `get_follow_up_statistics` 查询次数从 4 降为 1;`compute_avg_field` 不再 format! 拼接。 + +### Task 8: patient_service get_health_summary 用 tokio::join! 并行化 + +**涉及文件**: `crates/erp-health/src/service/patient_service.rs` + +**步骤**: `get_health_summary()` 中 4 次 `.await` 改为 `tokio::join!` 并行执行,各查询错误独立处理(未找到返回 None)。 + +**验收**: 并行化后返回数据与串行一致;`cargo test` 通过。 + +### Task 9: alert_engine 预加载规则批量评估 + +**涉及文件**: `crates/erp-health/src/service/alert_engine.rs` + +**步骤**: 批量查询患者最近 cooldown 期间所有 alerts 构建 `HashSet`,按 device_type 批量查最新 hourly 记录后在内存匹配规则条件。重构为:批量加载 -> 内存过滤 -> 批量生成告警。 + +**验收**: 10 条规则评估查询次数从 ~20 降为 2-3;`cargo test` 通过。 + +--- + +## Phase 4: 前端渲染优化(Week 3, P2) + +### Task 10: PluginCRUDPage columns useMemo + 拆分子组件 + +**涉及文件**: `apps/web/src/pages/PluginCRUDPage.tsx` + +**步骤**: `columns` 包裹 `useMemo(() => [...], [schema])`,搜索栏/分页/表格拆为独立子组件。 + +**验收**: 输入搜索时 columns 不重建;`pnpm build` 通过。 + +### Task 11: PluginGraphPage 按需重绘 + +**涉及文件**: `apps/web/src/pages/PluginGraphPage.tsx` + +**步骤**: 移除持续 requestAnimationFrame 循环,改为数据变更 useEffect 触发单次重绘 + ResizeObserver 监听容器变化。 + +**验收**: 静态页面时 CPU 占用 < 1%;`pnpm build` 通过。 + +### Task 12: vite.config.ts manualChunks 拆分 heavy deps + +**涉及文件**: `apps/web/vite.config.ts` + +**步骤**: `manualChunks` 配置 `vendor-charts`(@ant-design/charts) / `vendor-flow`(@xyflow/react) / `vendor-editor`(@wangeditor/editor),对应路由改用 `React.lazy` 动态加载。 + +**验收**: 主 bundle gzip 体积降低 200KB+;图表/流程图/编辑器按需加载。 + +--- + +## 执行原则 + +1. **每 Task 完成后立即提交** — 不积压,保持可追溯 +2. **Phase 1-2 为 P0** — 批量插入和 N+1 根治直接影响生产性能 +3. **cargo test + pnpm build 必须通过** — 每个 Task 完成后验证 diff --git a/docs/superpowers/plans/2026-04-26-security-defense-in-depth.md b/docs/superpowers/plans/2026-04-26-security-defense-in-depth.md new file mode 100644 index 0000000..03172ca --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-security-defense-in-depth.md @@ -0,0 +1,89 @@ +# 安全纵深防御实施计划 + +> 设计规格: `docs/superpowers/specs/2026-04-26-security-defense-in-depth-design.md` +> 日期: 2026-04-26 | 状态: draft | 总周期: 2-3 周 + +--- + +## Phase 1: PostgreSQL RLS 安全网(Week 1) + +### Task 1: 创建 RLS 策略迁移 + +**涉及文件**: `crates/erp-server/migration/src/m000073_enable_rls_all_tables.rs`(新增), `lib.rs` + +**步骤**: 对所有含 tenant_id 的表(30 基础 + 34 健康)执行 `ALTER TABLE ENABLE ROW LEVEL SECURITY` + `CREATE POLICY tenant_isolation USING (tenant_id = current_setting('app.current_tenant_id')::uuid)` + `CREATE POLICY tenant_bypass USING (current_user IN ('erp_admin', 'erp_migration'))`。创建 `erp_app` 数据库角色。down 方法完整回退。 + +**验收**: 迁移执行成功;以 erp_app 角色未设置 tenant_id 时查询返回空;现有应用行为不变。 + +### Task 2: Axum middleware 设置当前连接的 tenant_id + +**涉及文件**: `crates/erp-server/src/middleware/tenant.rs` + +**步骤**: tenant 中间件从 JWT 解析 tenant_id 后执行 `SET LOCAL app.current_tenant_id = ''`。确认在事务内执行(SeaORM 显式事务包装或 session 级 SET + RESET)。添加 tracing 日志记录注入状态。SET LOCAL 失败时 warn 但不阻断请求。 + +**验收**: 注入后以 erp_app 角色查询自动按 tenant_id 过滤;`cargo test --workspace` 通过。 + +### Task 3: 验证现有测试不受影响 + +**涉及文件**: 可能修改测试辅助代码 + +**步骤**: 运行 `cargo test --workspace` 检查 RLS 导致的测试失败。分析失败原因(测试未设置 tenant_id → 查询返回空),在 TestDb/TestApp 初始化时注入 tenant_id。不修改任何业务逻辑代码。 + +**验收**: `cargo test --workspace` 全部通过。 + +--- + +## Phase 2: 行级数据范围 + session_key Redis(Week 2) + +### Task 4: require_permission 增加 data_scope 过滤逻辑 + +**涉及文件**: `erp-core/src/types.rs`(TenantContext 增加 permission_data_scopes), `erp-core/src/rbac.rs`(apply_data_scope 函数), JWT 中间件, `erp-health/src/handler/mod.rs` + +**步骤**: TenantContext 新增 `permission_data_scopes: HashMap`(枚举 All/Self/Department/DepartmentTree)。JWT 中间件查询 `role_permissions.data_scope` 填充。实现 `apply_data_scope(query, ctx, permission, owner_column, dept_column)` 按变体追加 filter。各 health handler 列表查询调用此函数。 + +**验收**: data_scope = Department 时查询自动追加部门过滤;未配置时默认 All 向后兼容。 + +### Task 5: 微信 session_key 从 HashMap 迁移到 Redis + +**涉及文件**: `crates/erp-auth/src/service/wechat_service.rs`, `erp-server/src/app_state.rs` + +**步骤**: 替换 `LazyLock>` 为 Redis `SET wechat:session:{openid} {key} EX 300` / `GET + DEL`。Redis 连接池通过 AuthState 传入。添加 fallback:Redis 不可用时降级内存 HashMap 并 warn 日志。 + +**验收**: 小程序登录端到端通过;`cargo test -p erp-auth` 通过。 + +### Task 6: 小程序 openid 加密存储 + +**涉及文件**: `apps/miniprogram/src/utils/secure-storage.ts`(新增), `stores/auth.ts` + +**步骤**: 创建 AES 加密存储工具(setSecure/getSecure)。后端登录接口响应新增 `storage_key` 字段。auth.ts 中 openid 存储改用 `setSecure('openid', openid)`。 + +**验收**: `Taro.getStorageSync('openid')` 返回密文;小程序登录端到端通过。 + +--- + +## Phase 3: 健康检查增强 + 审计日志(Week 2-3) + +### Task 7: /health/ready 增加 DB ping + Redis ping + +**涉及文件**: `crates/erp-server/src/handlers/health.rs` + +**步骤**: 添加 `sqlx::query("SELECT 1")` DB 检查 + `redis PING` 检查,使用 `tokio::join!` 并行。响应扩展 status(ok/degraded) + database + redis 字段。 + +**验收**: DB 不可用时返回 `status: "degraded"` + `database: "unreachable"`。 + +### Task 8: audit_logs 表增加 prev_hash 字段实现哈希链 + +**涉及文件**: `migration/src/m000074_audit_logs_hash_chain.rs`(新增), `erp-core/src/audit.rs` + +**步骤**: 迁移添加 `prev_hash TEXT` + `record_hash TEXT` 列。写入时查询最新 record_hash 作为 prev_hash,计算 `SHA256(id + action + resource_type + resource_id + created_at + prev_hash)` 作为 record_hash。添加完整性验证函数检测篡改。内存缓存最近 1000 条 record_hash 优化性能。 + +**验收**: 新审计日志含哈希链字段;修改记录后验证函数检测到链断裂;`cargo test` 通过。 + +--- + +## 执行原则 + +1. **每 Task 完成后立即提交** — 不积压 +2. **Phase 1 最高优先** — RLS 是医疗数据合规红线 +3. **RLS 迁移必须可回退** — down 方法完整恢复 +4. **渐进式启用 data_scope** — 未配置默认 All,不破坏现有行为