799 lines
25 KiB
Markdown
799 lines
25 KiB
Markdown
---
|
||
title: 三端联调审计问题修复设计规格
|
||
created: 2026-05-01
|
||
status: draft
|
||
scope: 全量修复(15 项)
|
||
estimated_effort: 23h
|
||
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 | 统计仪表盘消费验证 | 孤岛 | 1h |
|
||
|
||
## 实施阶段
|
||
|
||
- **Phase 1(快速修复,~5h)**: #1-#6 — 主题设置联动、菜单入口、测试修复、危急值 500、事件堆积
|
||
- **Phase 2(体验补全,~5.5h)**: #7-#11 — 导航关联、小程序修复
|
||
- **Phase 3(功能闭环,~9h)**: #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<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` — 默认值更新
|
||
|
||
```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()),
|
||
}
|
||
}
|
||
```
|
||
|
||
**新增公开端点**: `crates/erp-config/src/handler/theme_handler.rs`
|
||
|
||
```rust
|
||
/// GET /api/v1/public/theme — 公开主题端点(无需认证)
|
||
/// 只返回品牌信息,用于登录页等未认证场景。
|
||
pub async fn get_public_theme<S>(
|
||
State(state): State<ConfigState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
) -> Result<JsonResponse<ApiResponse<PublicThemeResp>>, AppError>
|
||
where
|
||
ConfigState: FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
// 从 settings 读取,返回品牌字段即可
|
||
// 如果租户上下文不存在,返回默认品牌信息
|
||
}
|
||
```
|
||
|
||
此端点需要在 `erp-server` 的公开路由组(无需认证)中注册。
|
||
|
||
### 前端修改
|
||
|
||
**文件**: `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
|
||
<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` — 从主题配置读取
|
||
|
||
**关键问题**: 登录页在未认证状态下无法调用 `/api/v1/themes`(需要 token)。解决方案:
|
||
|
||
1. 登录页先尝试从 `localStorage('hms-theme-config')` 读取缓存的主题配置
|
||
2. 同时将后端 `GET /api/v1/themes` 端点增加一个**无需认证**的公开端点 `GET /api/v1/public/theme`(只返回品牌信息,不含 sidebar_style 等内部配置)
|
||
3. Login 组件 mount 时调用公开端点获取品牌信息,失败则用 localStorage 缓存,再失败用硬编码默认值
|
||
|
||
```tsx
|
||
// Login.tsx
|
||
const [brandConfig, setBrandConfig] = useState<{
|
||
brand_name?: string;
|
||
brand_slogan?: string;
|
||
brand_features?: string;
|
||
brand_copyright?: string;
|
||
} | null>(null);
|
||
|
||
useEffect(() => {
|
||
// 尝试从公开端点获取品牌信息
|
||
fetch('/api/v1/public/theme')
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
const config = data?.data;
|
||
setBrandConfig(config);
|
||
localStorage.setItem('hms-theme-config', JSON.stringify(config));
|
||
})
|
||
.catch(() => {
|
||
// fallback: localStorage 缓存 → 默认值
|
||
const cached = localStorage.getItem('hms-theme-config');
|
||
if (cached) setBrandConfig(JSON.parse(cached));
|
||
});
|
||
}, []);
|
||
|
||
<h1 className="brand-title">{brandConfig?.brand_name || 'HMS 健康管理平台'}</h1>
|
||
<p className="brand-desc">{brandConfig?.brand_slogan || '新一代健康管理平台'}</p>
|
||
<p className="brand-sub-desc">{brandConfig?.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.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: <AlertOutlined />,
|
||
BellOutlined: <BellOutlined />,
|
||
ControlOutlined: <ControlOutlined />,
|
||
InboxOutlined: <InboxOutlined />,
|
||
ApiOutlined: <ApiOutlined />,
|
||
ReadOutlined: <ReadOutlined />,
|
||
ExperimentOutlined: <ExperimentOutlined />,
|
||
```
|
||
|
||
### 验证
|
||
|
||
登录后侧边栏"健康管理"分组下出现:告警仪表盘、告警列表、告警规则、设备管理、透析管理、资讯管理菜单项,图标正确渲染。
|
||
|
||
---
|
||
|
||
## #4: 行动收件箱侧边栏无入口 [P1]
|
||
|
||
### 根因
|
||
|
||
前端代码已完整:`ActionInbox.tsx`(页面)、`ActionThreadDrawer.tsx`(组件)、`actionInbox.ts`(API)、`App.tsx:260`(路由注册)。仅缺数据库菜单种子数据。
|
||
|
||
### 修复
|
||
|
||
**新建迁移**: `crates/erp-server/migration/src/m20260501_000100_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');
|
||
|
||
-- 确认 RLS 状态(关键:判断是否为 RLS 导致 500)
|
||
SELECT relname, relrowsecurity, relforcerowsecurity
|
||
FROM pg_class WHERE relname = 'critical_alerts';
|
||
-- 如果 relrowsecurity = false → 不是 RLS 问题,需查看 tracing 日志
|
||
-- 如果 relrowsecurity = true 且无 policy → 需要步骤 2
|
||
```
|
||
|
||
启动后端并调用端点,查看 tracing 日志中的具体错误信息:
|
||
```bash
|
||
cargo run
|
||
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/v1/health/critical-alerts
|
||
# 查看 tracing 输出确认根因
|
||
```
|
||
|
||
**步骤 2**: 根据步骤 1 结果决定是否补齐 RLS 策略
|
||
|
||
如果 RLS 已启用但缺少策略,**新建迁移**: `crates/erp-server/migration/src/m20260501_000101_rls_for_post_migration_tables.rs`
|
||
|
||
使用动态 SQL 扫描所有含 `tenant_id` 列但缺少 `tenant_isolation` 策略的表,自动补齐 RLS。
|
||
> **注意**: 此迁移需在 `migration/src/lib.rs` 中注册。
|
||
|
||
**步骤 3**: 在 `critical_alert_service.rs` 添加 tracing 日志(无论根因是 RLS 还是其他,都应加日志)
|
||
|
||
```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
|
||
<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 过滤。
|
||
|
||
---
|
||
|
||
## #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) => (
|
||
<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_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`
|
||
|
||
后端 `erp-message` 模块通知端点完整:`GET /messages`(列表)、`PUT /messages/{id}/read`(标记已读)、`PUT /messages/read-all`(全部已读)、`GET /messages/unread-count`(未读计数)、`GET /messages/stream`(SSE 实时推送)。事件消费者覆盖 20 种事件类型。直接对接即可。
|
||
|
||
```typescript
|
||
import { api } from '@/services/request';
|
||
|
||
export const notificationService = {
|
||
list: (params?: { page?: number; page_size?: number }) =>
|
||
api.get('/messages', params),
|
||
markRead: (id: string) =>
|
||
api.put(`/messages/${id}/read`),
|
||
markAllRead: () =>
|
||
api.put('/messages/read-all'),
|
||
getUnreadCount: () =>
|
||
api.get('/messages/unread-count'),
|
||
};
|
||
```
|
||
|
||
2. 替换硬编码空数组为 API 调用:
|
||
|
||
```typescript
|
||
// 修改前
|
||
setNotifications([]);
|
||
|
||
// 修改后
|
||
try {
|
||
const res = await notificationService.list({ page: 1, page_size: 20 });
|
||
setNotifications(res?.data || []);
|
||
} catch {
|
||
setNotifications([]);
|
||
}
|
||
```
|
||
|
||
### 验证
|
||
|
||
小程序消息页"通知"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<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`
|
||
|
||
```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 ? <FamilyMembersTab patientId={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<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]
|
||
|
||
### 根因
|
||
|
||
审计报告称"统计仪表盘消费不足"。经代码验证,`DoctorDashboard.tsx` 和 `NurseDashboard.tsx` 已在消费 `pointsApi.getPersonalStats()`(分别在第 43 行和第 20 行)。9 个后端统计端点中 6 个已被前端消费,仅 `get_dashboard_stats`(聚合端点,被分别调用替代)和 3 个单独端点(被 `health-data` 聚合端点覆盖)未被直接调用,但功能上已覆盖。
|
||
|
||
### 修复
|
||
|
||
降级为验证任务:
|
||
|
||
1. 确认 DoctorDashboard/NurseDashboard 展示的个人统计指标是否完整(对比后端 `personal_stats` DTO 返回的全部字段)
|
||
2. 如果有遗漏字段(如 `abnormal_vital_signs`、`pending_lab_reviews`),在对应仪表盘中补充展示
|
||
3. 确认 `useStatsData.ts` 的 `tryFetch` 调用链无报错
|
||
|
||
### 验证
|
||
|
||
以医生角色登录 → 统计报表页 → 确认所有统计指标正常展示。
|
||
|
||
---
|
||
|
||
## 关键文件清单
|
||
|
||
| 文件 | 修改类型 | 问题# |
|
||
|------|---------|-------|
|
||
| `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 阶段实施 |
|