docs: 5 份实施计划 — 性能/安全/事件/前端/可观测性
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

对应 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/告警
This commit is contained in:
iven
2026-04-27 08:00:50 +08:00
parent 215fb35e0e
commit b410fa9f78
5 changed files with 809 additions and 0 deletions

View File

@@ -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`,计算变更 diffchanged_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, '<event_id>'`。outbox relay 用 `sqlx::PgListener` 监听 + `tokio::select!`LISTEN 触发 + 30s 兜底轮询)。保留 `process_pending_events()` 不变仅改变触发方式。PgListener 添加断线自动重连。
**验收**: 事件延迟 < 100msDB 轮询频率从 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。逐个 service14 个模块)替换手动构建为调用辅助函数,统一信封格式。
**验收**: 所有事件 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 失败仅 warnOutbox relay 兜底
4. **统一信封格式** — 使用 `build_event_payload` 保证一致性
5. **LISTEN/NOTIFY 保留兜底轮询** — 30s 轮询防 NOTIFY 丢失

View File

@@ -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: <T>(fn: () => Promise<T>, successMsg?: string) => Promise<T | null>;
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<T, F = string>(
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<Record<string, string>>` 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<Record<string, string>>`
- `batchResolveDoctorNames(ids: string[]): Promise<Record<string, string>>`
2. 内部实现:去重 → 过滤已缓存 → 并发加载(限制 5 并发)→ 写入缓存并返回
3. 在 AppointmentList 中移除 nameCache state改用 store 方法
4. 在 PointsOrderList 中同样迁移
**验收标准**:
- 两个页面无 `useState<Record<string, string>>` 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 体积 < 400KBgzip 前约 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. **渐进迁移** — 重复模式统一采用渐进策略,不一次性全量迁移

View File

@@ -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<AppState>) -> Json<HealthResponse> {
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 + 生产 DockerDay 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) 分离,避免暴露内部指标

View File

@@ -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<ActiveModel>` 后调用 `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_namehandler 层映射到响应 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_namefollow_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 请求;首屏 < 500ms20 条记录)。
### 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<rule_id>`,按 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 完成后验证

View File

@@ -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 = '<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 RedisWeek 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<String, DataScope>`(枚举 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<Mutex<HashMap>>` 为 Redis `SET wechat:session:{openid} {key} EX 300` / `GET + DEL`。Redis 连接池通过 AuthState 传入。添加 fallbackRedis 不可用时降级内存 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不破坏现有行为