Files
hms/docs/superpowers/specs/2026-05-01-tri-platform-audit-fix-design.md
iven ff073c83a5 docs: 三端联调审计问题修复设计规格 — 15 项修复方案
基于 4 专家组代码级分析整合:
- P0: erp-plugin 测试修复 + 品牌主题设置联动
- P1: 菜单入口补全 + 危急值 500 修复 + 事件堆积清理
- P2: 导航关联 + 小程序 3 项修复
- P3: AI SSE 入口 + 家属管理 + E2E 清理
- P4: 统计仪表盘消费

品牌信息改为通过主题设置动态管理(非硬编码)。
2026-05-01 17:07:50 +08:00

22 KiB
Raw Blame History

title, created, status, scope, estimated_effort, phases
title created status scope estimated_effort phases
三端联调审计问题修复设计规格 2026-05-01 draft 全量修复15 项) 24h 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 行导入:

#[cfg(test)]
mod tests {
    use super::*;
    use crate::manifest::parse_manifest;  // 新增

验证

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 增加字段

pub struct ThemeResp {
    // 已有字段
    pub primary_color: Option<String>,
    pub logo_url: Option<String>,
    pub sidebar_style: Option<String>,

    // 新增品牌字段
    #[serde(skip_serializing_if = "Option::is_none")]
    pub brand_name: Option<String>,           // 品牌名称,如 "HMS 健康管理平台"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub brand_slogan: Option<String>,         // 品牌标语,如 "新一代健康管理平台"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub brand_features: Option<String>,       // 功能亮点,如 "患者管理 · 健康监测 · 随访管理 · AI 智能分析"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub brand_copyright: Option<String>,      // 版权信息,如 "HMS 健康管理平台 · ©汕头市智界科技有限公司"
}

文件: crates/erp-config/src/handler/theme_handler.rs — 默认值更新

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

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 分隔):

<Divider>品牌信息</Divider>
<Form.Item name="brand_name" label="品牌名称">
  <Input placeholder="HMS 健康管理平台" />
</Form.Item>
<Form.Item name="brand_slogan" label="品牌标语">
  <Input placeholder="新一代健康管理平台" />
</Form.Item>
<Form.Item name="brand_features" label="功能亮点">
  <Input placeholder="患者管理 · 健康监测 · 随访管理 · AI 智能分析" />
</Form.Item>
<Form.Item name="brand_copyright" label="版权信息">
  <Input placeholder="HMS 健康管理平台 · ©汕头市智界科技有限公司" />
</Form.Item>

文件: apps/web/src/pages/Login.tsx — 从主题配置读取

const { themeConfig } = useAppStore();

<h1 className="brand-title">{themeConfig?.brand_name || 'HMS 健康管理平台'}</h1>
<p className="brand-desc">{themeConfig?.brand_slogan || '新一代健康管理平台'}</p>
<p className="brand-sub-desc">{themeConfig?.brand_features || '患者管理 · 健康监测 · 随访管理 · AI 智能分析'}</p>

底部版权同理。

文件: 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.tsxiconMap 中缺少 AlertOutlinedBellOutlinedControlOutlined 等图标映射。菜单数据返回后图标无法渲染。

修复

文件: apps/web/src/layouts/MainLayout.tsx

  1. 在 import 区域补充图标:
import {
  // ... 已有导入 ...
  AlertOutlined,
  BellOutlined,
  ControlOutlined,
  InboxOutlined,
  ApiOutlined,
  ReadOutlined,
  ExperimentOutlined,
} from '@ant-design/icons';
  1. iconMap 对象中补充映射:
AlertOutlined: <AlertOutlined />,
BellOutlined: <BellOutlined />,
ControlOutlined: <ControlOutlined />,
InboxOutlined: <InboxOutlined />,
ApiOutlined: <ApiOutlined />,
ReadOutlined: <ReadOutlined />,
ExperimentOutlined: <ExperimentOutlined />,

验证

登录后侧边栏"健康管理"分组下出现:告警仪表盘、告警列表、告警规则、设备管理、透析管理、资讯管理菜单项,图标正确渲染。


#4: 行动收件箱侧边栏无入口 [P1]

根因

前端代码已完整:ActionInbox.tsx(页面)、ActionThreadDrawer.tsx(组件)、actionInbox.tsAPIApp.tsx:260(路由注册)。仅缺数据库菜单种子数据。

修复

新建迁移: crates/erp-server/migration/src/m20260501_000098_seed_action_inbox_menu.rs

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: 确认迁移状态和表存在

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 日志

.map_err(|e| {
    tracing::error!(error = %e, "查询危急值告警列表失败");
    HealthError::DbError(e.to_string())
})?;

验证

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: 立即手动清理

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 行

// 修改前
"SELECT cleanup_old_published_events(90, 1000)"
// 修改后
"SELECT cleanup_old_published_events(7, 1000)"

步骤 3: 添加索引(可选)

CREATE INDEX IF NOT EXISTS idx_domain_events_status_created
ON domain_events (status, created_at ASC) WHERE status = 'pending';

验证

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 卡片之间添加快捷导航卡片:

<Card style={{ marginBottom: 16, padding: '12px 16px' }}>
  <Space size={8} wrap>
    <Text type="secondary">快捷跳转:</Text>
    <Button type="link" size="small"
      onClick={() => navigate(`/health/appointments?patient_id=${id}`)}>
      预约记录
    </Button>
    <Button type="link" size="small"
      onClick={() => navigate(`/health/consultations?patient_id=${id}`)}>
      咨询记录
    </Button>
    <Button type="link" size="small"
      onClick={() => navigate(`/health/dialysis?patient_id=${id}`)}>
      透析记录
    </Button>
    <Button type="link" size="small"
      onClick={() => navigate(`/health/follow-up-tasks?patient_id=${id}`)}>
      随访任务
    </Button>
    <Button type="link" size="small"
      onClick={() => navigate(`/health/ai-analysis?patient_id=${id}`)}>
      AI 分析
    </Button>
  </Space>
</Card>

前提条件: 目标页面需支持 URL 参数 ?patient_id=xxx 过滤。需在以下页面添加 URL 参数读取:

  • AppointmentList.tsx
  • ConsultationList.tsx
  • DialysisManageList.tsx
  • FollowUpTaskList.tsx
  • AiAnalysisList.tsx

各页面在初始化查询参数时从 useSearchParams() 读取 patient_id 并设为默认筛选条件。

验证

在患者详情页点击各快捷跳转按钮,确认目标页面自动按患者 ID 过滤。


根因

AiAnalysisList.tsxpatient_id 列只显示截断 IDv.slice(0, 8)),无跳转。

修复

文件: apps/web/src/pages/health/AiAnalysisList.tsx

  1. 添加 import { Link } from 'react-router-dom';
  2. 修改列渲染:
{
  title: '患者',
  dataIndex: 'patient_id',
  key: 'patient_id',
  width: 140,
  render: (v: string) => (
    <Link to={`/health/patients/${v}`} style={{ fontFamily: 'monospace', fontSize: 12 }}>
      {v.slice(0, 8)}
    </Link>
  ),
},

验证

AI 分析列表中患者 ID 列变为可点击链接,跳转到对应患者详情页。


#9: 小程序 AI 建议跳转错误 [P2]

根因

apps/miniprogram/src/pages/health/index.tsx 第 179 行AI 建议卡片 onClick 统一跳转到设置页 /pages/pkg-profile/settings/index,而非根据 suggestion_typeappointment/followup跳转到对应功能页。

修复

文件: apps/miniprogram/src/pages/health/index.tsx

修改 onClick 处理逻辑:

// 修改前
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
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`),
};
  1. 替换硬编码空数组为 API 调用:
// 修改前
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 数组中添加入口:

// 在"我的预约"项附近添加
{
  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

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<AbortController> {
  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

export function FamilyMembersTab({ patientId }: { patientId: string }) {
  // Table: 姓名 | 关系 | 电话 | 身份证号 | 备注 | 操作
  // DrawerForm: 添加/编辑家属(关系 Select: 父母/配偶/子女/兄弟姐妹/其他)
  // AuthButton code="health.patient.manage"
}

修改文件: apps/web/src/pages/health/PatientDetail.tsx

在 Tabs items 数组中注册:

{
  key: 'family',
  label: '家属管理',
  children: id ? <FamilyMembersTab patientId={id} /> : null,
},

验证

患者详情页出现"家属管理"Tab可添加/编辑/删除家属。


#14: E2E 测试数据污染 [P3]

根因

E2E 测试无 teardown30 个 E2E患者_* 永久残留。

修复

步骤 1: 立即清理

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

export async function cleanupE2EData(api: ApiClient): Promise<void> {
  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 阶段实施