docs: 5 份实施计划 — 性能/安全/事件/前端/可观测性
对应 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:
106
docs/superpowers/plans/2026-04-26-event-driven-architecture.md
Normal file
106
docs/superpowers/plans/2026-04-26-event-driven-architecture.md
Normal 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`,计算变更 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, '<event_id>'`。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 丢失
|
||||
290
docs/superpowers/plans/2026-04-26-frontend-engineering.md
Normal file
290
docs/superpowers/plans/2026-04-26-frontend-engineering.md
Normal 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 体积 < 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. **渐进迁移** — 重复模式统一采用渐进策略,不一次性全量迁移
|
||||
200
docs/superpowers/plans/2026-04-26-observability-and-ops.md
Normal file
200
docs/superpowers/plans/2026-04-26-observability-and-ops.md
Normal 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 + 生产 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) 分离,避免暴露内部指标
|
||||
124
docs/superpowers/plans/2026-04-26-performance-optimization.md
Normal file
124
docs/superpowers/plans/2026-04-26-performance-optimization.md
Normal 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_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<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 完成后验证
|
||||
@@ -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 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<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 传入。添加 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,不破坏现有行为
|
||||
Reference in New Issue
Block a user