diff --git a/docs/superpowers/specs/2026-05-01-tri-platform-audit-fix-design.md b/docs/superpowers/specs/2026-05-01-tri-platform-audit-fix-design.md new file mode 100644 index 0000000..765ab99 --- /dev/null +++ b/docs/superpowers/specs/2026-05-01-tri-platform-audit-fix-design.md @@ -0,0 +1,729 @@ +--- +title: 三端联调审计问题修复设计规格 +created: 2026-05-01 +status: draft +scope: 全量修复(15 项) +estimated_effort: 24h +phases: 3 +--- + +# 三端联调审计问题修复设计规格 + +> 基于 `docs/discussions/2026-05-01-tri-platform-integration-audit.md` 审计报告, +> 经 4 个专家组(统一性治理/功能孤岛/小程序/数据质量)代码级分析后整合的修复方案。 + +## 问题总览 + +| 级别 | # | 问题 | 专家组 | 工作量 | +|------|---|------|--------|--------| +| P0 | 1 | erp-plugin 测试编译失败 | 基础设施 | 5min | +| P0 | 2 | 品牌命名与主题设置联动 | 统一性 | 2h | +| P1 | 3 | 告警页面侧边栏无入口 | 统一性 | 1h | +| P1 | 4 | 行动收件箱侧边栏无入口 | 孤岛 | 0.5h | +| P1 | 5 | 危急值告警端点 500 | 基础设施 | 0.5h | +| P1 | 6 | domain_events 堆积 1166 条 | 基础设施 | 1h | +| P2 | 7 | 患者详情关联导航缺失 | 统一性 | 2h | +| P2 | 8 | AI 分析列表无患者 Link | 统一性 | 1h | +| P2 | 9 | 小程序 AI 建议跳转错误 | 小程序 | 0.5h | +| P2 | 10 | 小程序通知 Tab 空壳 | 小程序 | 1h | +| P2 | 11 | 小程序咨询功能孤立 | 小程序 | 0.5h | +| P3 | 12 | AI 分析 SSE 无触发入口 | 孤岛 | 4h | +| P3 | 13 | 家属管理无 UI | 孤岛 | 3h | +| P3 | 14 | E2E 测试数据污染 | 基础设施 | 1h | +| P4 | 15 | 统计仪表盘消费不足 | 孤岛 | 2h | + +## 实施阶段 + +- **Phase 1(快速修复,~5h)**: #1-#6 — 主题设置联动、菜单入口、测试修复、危急值 500、事件堆积 +- **Phase 2(体验补全,~5.5h)**: #7-#11 — 导航关联、小程序修复 +- **Phase 3(功能闭环,~10h)**: #12-#15 — AI SSE 入口、家属管理、E2E 清理、统计消费 + +--- + +## #1: erp-plugin 测试编译失败 [P0] + +### 根因 + +`crates/erp-plugin/src/plugin_validator.rs` 的 `#[cfg(test)] mod tests` 中调用了 `parse_manifest()`,但该函数在同 crate 的 `manifest.rs` 中。测试模块的 `use super::*` 只引入 `plugin_validator` 模块的公开项。 + +### 修复 + +**文件**: `crates/erp-plugin/src/plugin_validator.rs` + +在测试模块中添加 1 行导入: + +```rust +#[cfg(test)] +mod tests { + use super::*; + use crate::manifest::parse_manifest; // 新增 +``` + +### 验证 + +```bash +cargo test -p erp-plugin --no-run +``` + +--- + +## #2: 品牌命名与主题设置联动 [P0] + +### 根因 + +1. 登录页 `Login.tsx` 硬编码 "HMR Platform"、副标题、版权信息 +2. 后端 `ThemeResp` 只有 3 个字段(`primary_color`/`logo_url`/`sidebar_style`),缺少品牌名称、系统描述、版权等配置项 +3. 前端主题设置页面 `ThemeSettings.tsx` 只展示 3 个字段,主题设置实际未起到管理品牌信息的作用 +4. 侧边栏/页脚的品牌文字也是硬编码 + +### 设计思路 + +扩展现有主题设置体系,将品牌信息纳入主题配置。后端 ThemeResp 增加品牌字段,前端 ThemeSettings 页面增加品牌信息编辑区域,Login/MainLayout 从主题配置读取品牌信息。 + +### 后端修改 + +**文件**: `crates/erp-config/src/dto.rs` — ThemeResp 增加字段 + +```rust +pub struct ThemeResp { + // 已有字段 + pub primary_color: Option, + pub logo_url: Option, + pub sidebar_style: Option, + + // 新增品牌字段 + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_name: Option, // 品牌名称,如 "HMS 健康管理平台" + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_slogan: Option, // 品牌标语,如 "新一代健康管理平台" + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_features: Option, // 功能亮点,如 "患者管理 · 健康监测 · 随访管理 · AI 智能分析" + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_copyright: Option, // 版权信息,如 "HMS 健康管理平台 · ©汕头市智界科技有限公司" +} +``` + +**文件**: `crates/erp-config/src/handler/theme_handler.rs` — 默认值更新 + +```rust +fn default_theme() -> ThemeResp { + ThemeResp { + primary_color: None, + logo_url: None, + sidebar_style: None, + brand_name: Some("HMS 健康管理平台".into()), + brand_slogan: Some("新一代健康管理平台".into()), + brand_features: Some("患者管理 · 健康监测 · 随访管理 · AI 智能分析".into()), + brand_copyright: Some("HMS 健康管理平台 · ©汕头市智界科技有限公司".into()), + } +} +``` + +### 前端修改 + +**文件**: `apps/web/src/api/themes.ts` — 扩展 ThemeConfig + +```typescript +export interface ThemeConfig { + primary_color?: string; + logo_url?: string; + sidebar_style?: 'light' | 'dark'; + // 新增 + brand_name?: string; + brand_slogan?: string; + brand_features?: string; + brand_copyright?: string; +} +``` + +**文件**: `apps/web/src/stores/app.ts` — 缓存主题配置 + +在 app store 中增加 `themeConfig: ThemeConfig | null` 字段,login 后自动加载并缓存到 localStorage。提供 `loadThemeConfig()` action。 + +**文件**: `apps/web/src/pages/settings/ThemeSettings.tsx` — 增加品牌信息区域 + +在现有表单下方增加"品牌信息"分组(Divider 分隔): + +```tsx +品牌信息 + + + + + + + + + + + + +``` + +**文件**: `apps/web/src/pages/Login.tsx` — 从主题配置读取 + +```tsx +const { themeConfig } = useAppStore(); + +

{themeConfig?.brand_name || 'HMS 健康管理平台'}

+

{themeConfig?.brand_slogan || '新一代健康管理平台'}

+

{themeConfig?.brand_features || '患者管理 · 健康监测 · 随访管理 · AI 智能分析'}

+``` + +底部版权同理。 + +**文件**: `apps/web/src/layouts/MainLayout.tsx` — Footer 和侧边栏 + +侧边栏 Logo 文字和 Footer 版权信息也从 `themeConfig` 读取,带默认值 fallback。 + +### 验证 + +1. 在系统设置 → 主题设置中修改品牌名称为 "XX 医院健康管理" +2. 保存后刷新页面,侧边栏/页脚品牌文字更新 +3. 登出后登录页显示新品牌名称 +4. 未配置时回退到默认值 "HMS 健康管理平台" + +--- + +## #3: 告警页面侧边栏无入口 [P1] + +### 根因 + +数据库种子迁移 `m20260429_000095_seed_alert_device_menus.rs` 已插入告警菜单数据,但前端 `MainLayout.tsx` 的 `iconMap` 中缺少 `AlertOutlined`、`BellOutlined`、`ControlOutlined` 等图标映射。菜单数据返回后图标无法渲染。 + +### 修复 + +**文件**: `apps/web/src/layouts/MainLayout.tsx` + +1. 在 import 区域补充图标: + +```tsx +import { + // ... 已有导入 ... + AlertOutlined, + BellOutlined, + ControlOutlined, + InboxOutlined, + ApiOutlined, + ReadOutlined, + ExperimentOutlined, +} from '@ant-design/icons'; +``` + +2. 在 `iconMap` 对象中补充映射: + +```tsx +AlertOutlined: , +BellOutlined: , +ControlOutlined: , +InboxOutlined: , +ApiOutlined: , +ReadOutlined: , +ExperimentOutlined: , +``` + +### 验证 + +登录后侧边栏"健康管理"分组下出现:告警仪表盘、告警列表、告警规则、设备管理、透析管理、资讯管理菜单项,图标正确渲染。 + +--- + +## #4: 行动收件箱侧边栏无入口 [P1] + +### 根因 + +前端代码已完整:`ActionInbox.tsx`(页面)、`ActionThreadDrawer.tsx`(组件)、`actionInbox.ts`(API)、`App.tsx:260`(路由注册)。仅缺数据库菜单种子数据。 + +### 修复 + +**新建迁移**: `crates/erp-server/migration/src/m20260501_000098_seed_action_inbox_menu.rs` + +```sql +INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order, + created_at, updated_at, is_active) +SELECT + 'b0000003-0000-7000-8000-000000000020'::uuid, + t.id, + (SELECT id FROM menus WHERE path = '/health' AND tenant_id = t.id LIMIT 1), + '行动收件箱', + '/health/action-inbox', + 'InboxOutlined', + 36, + NOW(), NOW(), true +FROM tenants t +WHERE NOT EXISTS ( + SELECT 1 FROM menus + WHERE path = '/health/action-inbox' AND tenant_id = t.id +); +``` + +同时需要种子关联 `health.action-inbox.list` 权限码(参照 `m20260501_000097_seed_menu_permissions.rs` 模式)。 + +### 验证 + +重启后端 → 刷新前端 → 侧边栏出现"行动收件箱"菜单项。 + +--- + +## #5: 危急值告警端点 500 [P1] + +### 根因 + +代码逻辑链路完整(handler→service→entity 无 bug)。最可能根因是 `critical_alerts` 表在 RLS 批量启用迁移(m000086)之后才由 m000090 创建,导致该表缺少 RLS 策略。如果手工启用了 RLS 却无策略,查询会被阻断。 + +### 修复 + +**步骤 1**: 确认迁移状态和表存在 + +```sql +SELECT * FROM seaql_migrations WHERE name LIKE '%090%'; +SELECT table_name FROM information_schema.tables +WHERE table_name IN ('critical_alerts', 'critical_alert_responses'); +``` + +**步骤 2**: 补齐 RLS 策略 + +**新建迁移**: `crates/erp-server/migration/src/m20260501_000099_rls_for_post_migration_tables.rs` + +使用动态 SQL 扫描所有含 `tenant_id` 列但缺少 `tenant_isolation` 策略的表,自动补齐 RLS。 + +**步骤 3**: 在 `critical_alert_service.rs` 添加 tracing 日志 + +```rust +.map_err(|e| { + tracing::error!(error = %e, "查询危急值告警列表失败"); + HealthError::DbError(e.to_string()) +})?; +``` + +### 验证 + +```bash +curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/v1/health/critical-alerts +# 期望: 200 + 空列表 +``` + +--- + +## #6: domain_events 堆积 1166 条 [P1] + +### 根因 + +清理函数 `cleanup_old_published_events()` 保留 90 天,项目运行不到 2 周,所有事件未满 90 天。 + +### 修复 + +**步骤 1**: 立即手动清理 + +```sql +INSERT INTO domain_events_archive +SELECT * FROM domain_events WHERE status = 'published'; +DELETE FROM domain_events WHERE status = 'published' + AND id IN (SELECT id FROM domain_events_archive); +``` + +**步骤 2**: 调整保留期 + +**文件**: `crates/erp-server/src/tasks.rs` 第 26 行 + +```rust +// 修改前 +"SELECT cleanup_old_published_events(90, 1000)" +// 修改后 +"SELECT cleanup_old_published_events(7, 1000)" +``` + +**步骤 3**: 添加索引(可选) + +```sql +CREATE INDEX IF NOT EXISTS idx_domain_events_status_created +ON domain_events (status, created_at ASC) WHERE status = 'pending'; +``` + +### 验证 + +```sql +SELECT status, COUNT(*) FROM domain_events GROUP BY status; +-- 期望: published 数量大幅减少 +``` + +--- + +## #7: 患者详情关联导航缺失 [P2] + +### 根因 + +`PatientDetail.tsx` 有 5 个 Tab(基本信息/健康数据/随访/积分/AI建议),但无法跳转到预约、咨询、透析、全局随访等关联功能页面。 + +### 修复 + +**文件**: `apps/web/src/pages/health/PatientDetail.tsx` + +在基本信息卡片与 Tabs 卡片之间添加快捷导航卡片: + +```tsx + + + 快捷跳转: + + + + + + + +``` + +**前提条件**: 目标页面需支持 URL 参数 `?patient_id=xxx` 过滤。需在以下页面添加 URL 参数读取: + +- `AppointmentList.tsx` +- `ConsultationList.tsx` +- `DialysisManageList.tsx` +- `FollowUpTaskList.tsx` +- `AiAnalysisList.tsx` + +各页面在初始化查询参数时从 `useSearchParams()` 读取 `patient_id` 并设为默认筛选条件。 + +### 验证 + +在患者详情页点击各快捷跳转按钮,确认目标页面自动按患者 ID 过滤。 + +--- + +## #8: AI 分析列表无患者 Link [P2] + +### 根因 + +`AiAnalysisList.tsx` 的 `patient_id` 列只显示截断 ID(`v.slice(0, 8)`),无跳转。 + +### 修复 + +**文件**: `apps/web/src/pages/health/AiAnalysisList.tsx` + +1. 添加 `import { Link } from 'react-router-dom';` +2. 修改列渲染: + +```tsx +{ + title: '患者', + dataIndex: 'patient_id', + key: 'patient_id', + width: 140, + render: (v: string) => ( + + {v.slice(0, 8)} + + ), +}, +``` + +### 验证 + +AI 分析列表中患者 ID 列变为可点击链接,跳转到对应患者详情页。 + +--- + +## #9: 小程序 AI 建议跳转错误 [P2] + +### 根因 + +`apps/miniprogram/src/pages/health/index.tsx` 第 179 行,AI 建议卡片 onClick 统一跳转到设置页 `/pages/pkg-profile/settings/index`,而非根据 `suggestion_type`(appointment/followup)跳转到对应功能页。 + +### 修复 + +**文件**: `apps/miniprogram/src/pages/health/index.tsx` + +修改 onClick 处理逻辑: + +```typescript +// 修改前 +onClick={() => Taro.navigateTo({ url: '/pages/pkg-profile/settings/index' })} + +// 修改后 +onClick={() => { + if (item.suggestion_type === 'appointment') { + Taro.navigateTo({ url: `/pages/pkg-appointment/create/index?patientId=${item.patient_id}` }); + } else if (item.suggestion_type === 'followup') { + Taro.navigateTo({ url: `/pages/pkg-followup/detail/index?id=${item.related_id}` }); + } else { + Taro.navigateTo({ url: `/pages/health/index` }); // 通用回退 + } +}} +``` + +### 验证 + +小程序健康页点击不同类型的 AI 建议卡片,确认跳转到正确页面。 + +--- + +## #10: 小程序通知 Tab 空壳 [P2] + +### 根因 + +`apps/miniprogram/src/pages/messages/index.tsx` 第 34-37 行 `setNotifications([])` 硬编码空数组,注释"后续可对接独立通知 API"。 + +### 修复 + +**文件**: `apps/miniprogram/src/pages/messages/index.tsx` + +1. 新建通知 API 服务:`apps/miniprogram/src/services/notification.ts` + +```typescript +import { http } from '@/utils/http'; + +export const notificationService = { + list: (params?: { page?: number; page_size?: number }) => + http.get('/api/v1/messages/notifications', { params }), + markRead: (id: string) => + http.put(`/api/v1/messages/notifications/${id}/read`), +}; +``` + +2. 替换硬编码空数组为 API 调用: + +```typescript +// 修改前 +setNotifications([]); + +// 修改后 +const res = await notificationService.list({ page: 1, page_size: 20 }); +setNotifications(res.data?.data || []); +``` + +**前提条件**: 后端 `erp-message` 模块需确认通知 API 端点路径。如果尚无独立通知端点,可先对接消息列表端点 `/api/v1/messages` 并按类型筛选。 + +### 验证 + +小程序消息页"通知"Tab 展示从后端获取的数据列表。 + +--- + +## #11: 小程序咨询功能孤立 [P2] + +### 根因 + +`/pages/consultation/index` 页面存在且已注册路由,但首页/健康页/个人中心均无入口。唯一入口在消息 Tab 的"咨询"子 Tab。 + +### 修复 + +**文件**: `apps/miniprogram/src/pages/profile/index.tsx` + +在 `MENU_ITEMS` 数组中添加入口: + +```typescript +// 在"我的预约"项附近添加 +{ + title: '在线咨询', + icon: 'chat', + url: '/pages/consultation/index', +}, +``` + +**文件**: `apps/miniprogram/src/pages/index/index.tsx` + +在首页快捷操作区添加咨询入口图标。 + +### 验证 + +小程序个人中心/首页出现"在线咨询"入口,点击进入咨询页面。 + +--- + +## #12: AI 分析 SSE 无触发入口 [P3] + +### 根因 + +后端 4 个 SSE 端点就绪(lab-report/trends/checkup-plan/report-summary),前端无调用入口。 + +### 修复 + +#### 新建文件 + +**1. `apps/web/src/api/ai/analysisSse.ts`** — SSE 分析 API + +```typescript +export type AnalysisType = 'lab-report' | 'trends' | 'checkup-plan' | 'report-summary'; + +export interface SseAnalysisOptions { + type: AnalysisType; + reportId?: string; + patientId?: string; + metrics?: string[]; + onChunk: (content: string, index: number) => void; + onError: (message: string) => void; + onDone: (analysisId: string) => void; +} + +export async function startAnalysis(options: SseAnalysisOptions): Promise { + const controller = new AbortController(); + const endpoint = ENDPOINT_MAP[options.type]; + // fetch POST → 读取 ReadableStream → 解析 SSE → 回调 + return controller; +} +``` + +**2. `apps/web/src/components/AiAnalysisPanel.tsx`** — SSE 分析面板组件 + +展示:分析类型选择、实时流式内容(Markdown)、进度指示器、完成后跳转。 + +#### 修改文件 + +**3. `apps/web/src/pages/health/components/LabReportsTab.tsx`** — 每行添加"AI 解读"按钮 + +**4. `apps/web/src/pages/health/PatientDetail.tsx`** — AI 建议标签页添加"发起分析"按钮 + +**5. `apps/web/src/pages/health/AiAnalysisList.tsx`** — 顶部添加"新建分析"入口 + +### 验证 + +1. 在化验报告列表点击"AI 解读",确认 SSE 流实时显示分析内容 +2. 在患者详情点击"趋势分析",确认分析完成并跳转历史详情 + +--- + +## #13: 家属管理无 UI [P3] + +### 根因 + +后端 4 个 API + 前端 API 封装全部就绪,仅缺 PatientDetail 中的 Tab 组件。 + +### 修复 + +**新建文件**: `apps/web/src/pages/health/components/FamilyMembersTab.tsx` + +```typescript +export function FamilyMembersTab({ patientId }: { patientId: string }) { + // Table: 姓名 | 关系 | 电话 | 身份证号 | 备注 | 操作 + // DrawerForm: 添加/编辑家属(关系 Select: 父母/配偶/子女/兄弟姐妹/其他) + // AuthButton code="health.patient.manage" +} +``` + +**修改文件**: `apps/web/src/pages/health/PatientDetail.tsx` + +在 Tabs `items` 数组中注册: + +```typescript +{ + key: 'family', + label: '家属管理', + children: id ? : null, +}, +``` + +### 验证 + +患者详情页出现"家属管理"Tab,可添加/编辑/删除家属。 + +--- + +## #14: E2E 测试数据污染 [P3] + +### 根因 + +E2E 测试无 teardown,30 个 `E2E患者_*` 永久残留。 + +### 修复 + +**步骤 1**: 立即清理 + +```sql +UPDATE patients SET deleted_at = NOW(), updated_at = NOW() +WHERE name LIKE 'E2E患者_%' AND deleted_at IS NULL; +``` + +**步骤 2**: 新建 `apps/web/e2e/fixtures/cleanup.ts` + +```typescript +export async function cleanupE2EData(api: ApiClient): Promise { + const patients = await api.get('/api/v1/health/patients?keyword=E2E'); + for (const p of patients?.data?.data ?? []) { + if (p.name.startsWith('E2E')) await api.delete(`/api/v1/health/patients/${p.id}`); + } +} +``` + +**步骤 3**: 每个 E2E spec 的 `afterAll` 中调用 `cleanupE2EData`。 + +### 验证 + +数据库中 E2E 测试患者被清理,后续 E2E 测试运行后自动清理。 + +--- + +## #15: 统计仪表盘消费不足 [P4] + +### 根因 + +9 个统计端点中仅 `get_personal_stats`(个人维度统计)未被前端消费。其余端点已被聚合端点 `health-data` 覆盖。 + +### 修复 + +**文件**: `apps/web/src/pages/health/StatisticsDashboard/DoctorDashboard.tsx` + +消费 `pointsApi.getPersonalStats()`,展示: +- 今日预约数、逾期随访、今日待随访 +- 待审化验报告、异常体征患者、我的患者数 + +**文件**: `apps/web/src/pages/health/StatisticsDashboard/NurseDashboard.tsx` + +类似消费,侧重体征上报率、今日随访。 + +### 验证 + +医生/护士仪表盘新增个人统计指标卡片。 + +--- + +## 关键文件清单 + +| 文件 | 修改类型 | 问题# | +|------|---------|-------| +| `crates/erp-plugin/src/plugin_validator.rs` | 添加 1 行导入 | #1 | +| `apps/web/src/pages/Login.tsx` | 从主题配置读取品牌信息 | #2 | +| `apps/web/src/stores/app.ts` | 缓存 themeConfig | #2 | +| `apps/web/src/pages/settings/ThemeSettings.tsx` | 增加品牌信息表单 | #2 | +| `crates/erp-config/src/dto.rs` | ThemeResp 增加品牌字段 | #2 | +| `crates/erp-config/src/handler/theme_handler.rs` | 默认品牌值 | #2 | +| `apps/web/src/api/themes.ts` | 扩展 ThemeConfig 接口 | #2 | +| `apps/web/src/layouts/MainLayout.tsx` | Footer/Logo 从配置读取 | #2,#3 | +| `apps/web/src/layouts/MainLayout.tsx` | 图标导入+映射 | #3 | +| `migration/m20260501_000098_seed_action_inbox_menu.rs` | 新建迁移 | #4 | +| `migration/m20260501_000099_rls_for_post_migration_tables.rs` | 新建迁移 | #5 | +| `crates/erp-health/src/service/critical_alert_service.rs` | 添加 tracing | #5 | +| `crates/erp-server/src/tasks.rs` | 保留期 90→7 | #6 | +| `apps/web/src/pages/health/PatientDetail.tsx` | 快捷导航 | #7 | +| 5 个列表页面 | URL 参数支持 | #7 | +| `apps/web/src/pages/health/AiAnalysisList.tsx` | Link 添加 | #8 | +| `apps/miniprogram/src/pages/health/index.tsx` | 跳转逻辑 | #9 | +| `apps/miniprogram/src/pages/messages/index.tsx` | API 对接 | #10 | +| `apps/miniprogram/src/services/notification.ts` | 新建服务 | #10 | +| `apps/miniprogram/src/pages/profile/index.tsx` | 菜单项 | #11 | +| `apps/web/src/api/ai/analysisSse.ts` | 新建 SSE API | #12 | +| `apps/web/src/components/AiAnalysisPanel.tsx` | 新建组件 | #12 | +| `apps/web/src/pages/health/components/LabReportsTab.tsx` | AI 解读按钮 | #12 | +| `apps/web/src/pages/health/components/FamilyMembersTab.tsx` | 新建组件 | #13 | +| `apps/web/e2e/fixtures/cleanup.ts` | 新建清理 | #14 | +| `apps/web/src/pages/health/StatisticsDashboard/DoctorDashboard.tsx` | 个人统计 | #15 | + +## 变更记录 + +| 日期 | 变更 | +|------|------| +| 2026-05-01 | 初始版本 — 4 专家组整合,15 项修复,3 阶段实施 |