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 index 765ab99..18b4add 100644 --- 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 @@ -3,7 +3,7 @@ title: 三端联调审计问题修复设计规格 created: 2026-05-01 status: draft scope: 全量修复(15 项) -estimated_effort: 24h +estimated_effort: 23h phases: 3 --- @@ -30,13 +30,13 @@ phases: 3 | P3 | 12 | AI 分析 SSE 无触发入口 | 孤岛 | 4h | | P3 | 13 | 家属管理无 UI | 孤岛 | 3h | | P3 | 14 | E2E 测试数据污染 | 基础设施 | 1h | -| P4 | 15 | 统计仪表盘消费不足 | 孤岛 | 2h | +| P4 | 15 | 统计仪表盘消费验证 | 孤岛 | 1h | ## 实施阶段 - **Phase 1(快速修复,~5h)**: #1-#6 — 主题设置联动、菜单入口、测试修复、危急值 500、事件堆积 - **Phase 2(体验补全,~5.5h)**: #7-#11 — 导航关联、小程序修复 -- **Phase 3(功能闭环,~10h)**: #12-#15 — AI SSE 入口、家属管理、E2E 清理、统计消费 +- **Phase 3(功能闭环,~9h)**: #12-#15 — AI SSE 入口、家属管理、E2E 清理、统计验证 --- @@ -119,6 +119,26 @@ fn default_theme() -> ThemeResp { } ``` +**新增公开端点**: `crates/erp-config/src/handler/theme_handler.rs` + +```rust +/// GET /api/v1/public/theme — 公开主题端点(无需认证) +/// 只返回品牌信息,用于登录页等未认证场景。 +pub async fn get_public_theme( + State(state): State, + Extension(ctx): Extension, +) -> Result>, AppError> +where + ConfigState: FromRef, + S: Clone + Send + Sync + 'static, +{ + // 从 settings 读取,返回品牌字段即可 + // 如果租户上下文不存在,返回默认品牌信息 +} +``` + +此端点需要在 `erp-server` 的公开路由组(无需认证)中注册。 + ### 前端修改 **文件**: `apps/web/src/api/themes.ts` — 扩展 ThemeConfig @@ -162,12 +182,40 @@ export interface ThemeConfig { **文件**: `apps/web/src/pages/Login.tsx` — 从主题配置读取 -```tsx -const { themeConfig } = useAppStore(); +**关键问题**: 登录页在未认证状态下无法调用 `/api/v1/themes`(需要 token)。解决方案: -

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

-

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

-

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

+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)); + }); +}, []); + +

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

+

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

+

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

``` 底部版权同理。 @@ -236,7 +284,7 @@ ExperimentOutlined: , ### 修复 -**新建迁移**: `crates/erp-server/migration/src/m20260501_000098_seed_action_inbox_menu.rs` +**新建迁移**: `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, @@ -273,21 +321,38 @@ WHERE NOT EXISTS ( ### 修复 -**步骤 1**: 确认迁移状态和表存在 +**步骤 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 ``` -**步骤 2**: 补齐 RLS 策略 +启动后端并调用端点,查看 tracing 日志中的具体错误信息: +```bash +cargo run +curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/v1/health/critical-alerts +# 查看 tracing 输出确认根因 +``` -**新建迁移**: `crates/erp-server/migration/src/m20260501_000099_rls_for_post_migration_tables.rs` +**步骤 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 日志 +**步骤 3**: 在 `critical_alert_service.rs` 添加 tracing 日志(无论根因是 RLS 还是其他,都应加日志) ```rust .map_err(|e| { @@ -484,14 +549,17 @@ onClick={() => { 1. 新建通知 API 服务:`apps/miniprogram/src/services/notification.ts` +后端 `erp-message` 模块目前只有 `GET /api/v1/messages` 和 `PUT /api/v1/messages/{id}/read`,没有独立的通知端点。方案:对接现有消息列表端点,按类型筛选通知类消息。 + ```typescript import { http } from '@/utils/http'; export const notificationService = { + // 复用现有消息列表端点,按 category 筛选通知 list: (params?: { page?: number; page_size?: number }) => - http.get('/api/v1/messages/notifications', { params }), + http.get('/api/v1/messages', { params: { ...params, category: 'notification' } }), markRead: (id: string) => - http.put(`/api/v1/messages/notifications/${id}/read`), + http.put(`/api/v1/messages/${id}/read`), }; ``` @@ -502,12 +570,14 @@ export const notificationService = { setNotifications([]); // 修改后 -const res = await notificationService.list({ page: 1, page_size: 20 }); -setNotifications(res.data?.data || []); +try { + const res = await notificationService.list({ page: 1, page_size: 20 }); + setNotifications(res.data?.data || []); +} catch { + setNotifications([]); +} ``` -**前提条件**: 后端 `erp-message` 模块需确认通知 API 端点路径。如果尚无独立通知端点,可先对接消息列表端点 `/api/v1/messages` 并按类型筛选。 - ### 验证 小程序消息页"通知"Tab 展示从后端获取的数据列表。 @@ -667,27 +737,23 @@ export async function cleanupE2EData(api: ApiClient): Promise { --- -## #15: 统计仪表盘消费不足 [P4] +## #15: 统计仪表盘消费验证 [P4] ### 根因 -9 个统计端点中仅 `get_personal_stats`(个人维度统计)未被前端消费。其余端点已被聚合端点 `health-data` 覆盖。 +审计报告称"统计仪表盘消费不足"。经代码验证,`DoctorDashboard.tsx` 和 `NurseDashboard.tsx` 已在消费 `pointsApi.getPersonalStats()`(分别在第 43 行和第 20 行)。9 个后端统计端点中 6 个已被前端消费,仅 `get_dashboard_stats`(聚合端点,被分别调用替代)和 3 个单独端点(被 `health-data` 聚合端点覆盖)未被直接调用,但功能上已覆盖。 ### 修复 -**文件**: `apps/web/src/pages/health/StatisticsDashboard/DoctorDashboard.tsx` +降级为验证任务: -消费 `pointsApi.getPersonalStats()`,展示: -- 今日预约数、逾期随访、今日待随访 -- 待审化验报告、异常体征患者、我的患者数 - -**文件**: `apps/web/src/pages/health/StatisticsDashboard/NurseDashboard.tsx` - -类似消费,侧重体征上报率、今日随访。 +1. 确认 DoctorDashboard/NurseDashboard 展示的个人统计指标是否完整(对比后端 `personal_stats` DTO 返回的全部字段) +2. 如果有遗漏字段(如 `abnormal_vital_signs`、`pending_lab_reviews`),在对应仪表盘中补充展示 +3. 确认 `useStatsData.ts` 的 `tryFetch` 调用链无报错 ### 验证 -医生/护士仪表盘新增个人统计指标卡片。 +以医生角色登录 → 统计报表页 → 确认所有统计指标正常展示。 ---