# 系统硬编码清理设计规格 > 日期: 2026-05-02 | 状态: DRAFT | 范围: 前端硬编码清理 + 后端 API 补建 + 常量统一 + 医疗阈值配置化 --- ## 1. 背景与动机 HMS 健康管理平台已进入关键节点 — 后端 328 条路由、45 个 Entity、772 个测试函数已就位。但前端工作台页面存在大量硬编码假数据,直接影响系统可信度: - **AdminDashboard** 中 70% 面板数据为假值(系统健康条、用户活跃度、模块状态、角色分布) - **OperatorWorkbench** 中积分动态使用假姓名、内容矩阵数字硬编码、待办事项不可操作 - 20+ 处状态映射在多个文件中重复定义(严重度映射 5 处、性别映射 4 处、设备类型 3 处) - 医疗报警阈值(血压/心率/血糖)硬编码在小程序前端,不同患者/年龄段无法差异化 **目标**:清除所有 CRITICAL 级硬编码,确保用户看到的每个数字都来自真实 API,同时建立常量管理规范防止回退。 **原则**: - 新建 API 全部遵循现有 `/api/v1/` 前缀 + `ApiResponse` 包装 + 多租户隔离 - 常量统一采用混合策略:静态枚举收敛到 `constants/health.ts`,动态选项对接字典 API - 医疗阈值复用现有 `critical_value_threshold` 表及 CRUD API,补充患者端只读接口 --- ## 2. 影响范围与严重度矩阵 ### 2.1 CRITICAL — 用户看到虚假数据 | ID | 文件 | 硬编码内容 | 影响面 | |----|------|-----------|--------| | C-1 | `AdminDashboard.tsx` 行 108-115 | 系统健康条 6 项全部假数据 | 所有管理员看到的"API服务正常""队列积压12"等均为虚假 | | C-2 | `AdminDashboard.tsx` 行 230-258 | 用户活跃度 4 项 + 角色分布 5 项全部假数据 | 今日活跃 23 人、医生 12 人等均为虚假 | | C-3 | `AdminDashboard.tsx` 行 41-50 | 模块状态 8 项硬编码 | "44 实体 · 328 路由"等描述不反映真实状态 | | C-4 | `AdminDashboard.tsx` 行 87 | 待处理工单硬编码为 5 | 管理员误以为有 5 个工单待处理 | | C-5 | `OperatorWorkbench.tsx` 行 40-44 | 积分动态 3 人假姓名 | 张伟/王建国/李秀英均为虚构 | | C-6 | `OperatorWorkbench.tsx` 行 142-147 | 内容矩阵"已发布 24 / 草稿箱 3"硬编码 | 不反映真实文章数量 | | C-7 | `OperatorWorkbench.tsx` 行 61 | AI Hero 卡片固定文案"3 个运营洞察" | 不随实际数据变化 | ### 2.2 HIGH — 限制扩展性 | ID | 类别 | 数量 | 典型案例 | |----|------|------|---------| | H-1 | 重复状态映射 | 20+ 处 | SEVERITY_COLOR 在 5 个文件各自定义 | | H-2 | 动态选项硬编码 | 6 类 | 科室/职称/设备类型/随访类型/咨询类型/家庭关系 | | H-3 | 默认角色为 admin | 1 处 | `useDashboardRole.ts` 无角色时返回 admin | | H-4 | 医疗阈值硬编码 | 2 处 | 血压 140/90、心率 100/60、血糖 6.1/7.8 | ### 2.3 MEDIUM — 代码质量 | ID | 类别 | 说明 | |----|------|------| | M-1 | 小程序菜单用中文做 key | `MENU_PATHS` 用中文字符串做映射 key | | M-2 | 待办模板硬编码 | 5 条固定待办文本,不可操作 | --- ## 3. 轨道 1:工作台真实数据化 ### 3.1 现有 API 清单与缺口分析 **已有且已被前端调用的 API:** | API | 路径 | 使用者 | |-----|------|--------| | 工作台统计 | `GET /health/action-inbox/stats` → `WorkbenchStats` | DoctorWorkbench, OperatorWorkbench | | 团队概览 | `GET /health/action-inbox/team` → `TeamOverview` | DoctorWorkbench | | 行动收件箱 | `GET /health/action-inbox` → `ActionItem[]` | DoctorWorkbench | | 6 类统计 | `useStatsData` → patient/consultation/followup/points/healthData/dialysis | 三个工作台 | | 个人统计 | `pointsApi.getPersonalStats` → `PersonalStats` | DoctorWorkbench | | 审计日志 | `listAuditLogs` | AdminDashboard | **缺口 — 需要新建的 5 个 API:** ### 3.2 新增后端 API 设计 #### API-1: 系统健康检查 ``` GET /health/admin/system-health (完整路径: /api/v1/health/admin/system-health,前缀由 erp-server nest 自动添加) 权限: health.dashboard.manage(新增,需在 module.rs 权限描述符中注册) 响应: ApiResponse ``` ```typescript interface SystemHealth { services: { name: string; // "API 服务" / "数据库" / "Redis" / ... status: 'healthy' | 'degraded' | 'down'; message: string; // "正常" / "队列积压 12" / "连接超时" response_ms?: number; }[]; checked_at: string; // ISO timestamp } ``` 实现:后端 handler 逐项检查 DB 连接(`SELECT 1`)、Redis PING、SMTP 配置状态、存储路径可用性、定时任务心跳。结果缓存 30 秒避免频繁检查。 #### API-2: 用户活跃度统计 ``` GET /health/admin/user-activity 权限: health.dashboard.manage(同 API-1) 响应: ApiResponse ``` ```typescript interface UserActivity { daily_active: number; weekly_active: number; monthly_active: number; total_registered: number; by_role: { role: string; // "医生" / "护士" / ... count: number; }[]; } ``` 实现:基于 `users` 表 `last_login_at` 字段统计,角色分布通过 `user_roles` JOIN `roles` 聚合。 #### API-3: 模块状态 ``` GET /health/admin/modules 权限: health.dashboard.manage(同 API-1) 响应: ApiResponse ``` ```typescript interface ModuleStatus { name: string; // "erp-health" / "erp-auth" / ... display_name: string; // "健康管理" / "身份权限" / ... description: string; active: boolean; entity_count?: number; route_count?: number; } ``` 实现:从 AppState 中读取已注册的 `ErpModule` 列表 + 查询 `plugins` 表补充插件状态。 #### API-4: 积分动态流 ``` GET /health/points/recent-activity?limit=10 权限: health.points.list 响应: ApiResponse ``` > **命名约定**:TypeScript 接口字段使用 snake_case 镜像后端字段名,与本项目既有模式一致。 ```typescript interface PointsActivityItem { id: string; user_name: string; // 患者姓名 detail: string; // "兑换 · 血压计袖带" / "每日上报 · 血压" amount: string; // "+10" / "-500" type: 'earn' | 'spend'; created_at: string; } ``` 实现:查询 `points_account_transactions` 最新 N 条,JOIN `patients` 获取姓名。 #### API-5: 内容统计 ``` GET /health/articles/stats 权限: health.articles.list(注意复数形式,匹配 module.rs 注册的权限码) 响应: ApiResponse ``` ```typescript interface ArticleStats { published: number; draft: number; pending_review: number; rejected: number; total_views: number; } ``` 实现:`SELECT status, COUNT(*) FROM articles WHERE tenant_id = $1 AND deleted_at IS NULL GROUP BY status`。 ### 3.3 工作台页面改造方案 #### AdminDashboard 改造 | 面板 | 当前(假数据) | 改造后(真实数据) | |------|---------------|-------------------| | 系统健康条 | 6 项硬编码 | 调用 API-1 `systemHealth` | | 统计卡片 - 注册用户 | useStatsData (已真实) | 保持不变 | | 统计卡片 - 业务模块 | 硬编码 MODULES.length | 调用 API-3 计算活跃数 | | 统计卡片 - 今日操作 | auditLogs.length (已真实) | 保持不变 | | 统计卡片 - 待处理工单 | 硬编码 5 | 改用 actionInboxApi.stats().total_pending | | 用户活跃度 | 4 项百分比 + 角色分布全部假 | 调用 API-2 `userActivity` | | 模块状态 | MODULES 数组硬编码 | 调用 API-3 `modules` | | 快捷管理 | QUICK_ACTIONS 硬编码 | 保留(属 UI 配置) | | 问候语 "X主任" | 硬编码"主任" | 移除称呼后缀,只显示姓 | #### OperatorWorkbench 改造 | 面板 | 当前 | 改造后 | |------|------|--------| | AI Hero 卡片 | 固定文案 "3 个运营洞察" | 动态生成文案基于 stats 数据 | | 统计卡片 | useStatsData + actionInbox (已真实) | 保持不变 | | 今日待办 | 5 条硬编码 todo 模板 | 改用 actionInboxApi.list 筛选 pending 项 | | 积分动态 | 3 人假姓名 | 调用 API-4 `recentActivity` | | 内容矩阵 | "已发布 24 / 草稿箱 3" | 调用 API-5 `articleStats` | | 问候语 "X美玲" | 硬编码"美玲" | 移除,只显示姓 | #### DoctorWorkbench 基本已是真实数据(80%),仅需微调: - 问候语 "X医生" 中"医生"后缀 → 移除,只显示姓 - 确认 `personalStats.consultations_this_month` 字段后端已实现(否则用 0 占位) --- ## 4. 轨道 2:常量统一 ### 4.1 分类原则 | 分类 | 存放位置 | 判断标准 | 例子 | |------|---------|---------|------| | 静态映射 | `constants/health.ts` | 枚举固定,后端不可能新增项 | 性别、血型、严重度颜色、状态颜色 | | 动态选项 | 后端字典 API | 业务运营可能新增 | 科室、职称、设备类型、随访类型 | | UI 配置 | 各组件内部 | 纯前端行为,无后端对应 | 快捷操作按钮、Tab 标签文案 | ### 4.2 静态映射收敛 统一以下映射组到 `constants/health.ts`,消除所有重复定义: | 导出名 | 当前分散位置 | 统一后 | |--------|------------|--------| | `SEVERITY_CONFIG` | AlertDashboard, AlertList, AlertRuleList, ActionInbox, DoctorDashboard (5处) | 1 处 | | `GENDER_OPTIONS` | constants/health.ts, PatientDetail, PatientTagManage, PatientSelect (4处) | 1 处 | | `DEVICE_TYPE_OPTIONS` + `DEVICE_TYPE_COLOR` | DeviceManage, DeviceReadingsTab, AlertRuleList (3处) | 1 处 | | `ALERT_STATUS_CONFIG` | AlertDashboard, AlertList, StatusTag (3处) | 1 处 | | `APPOINTMENT_STATUS_CONFIG` | AppointmentList (1处但含复杂流转) | 1 处 | | `CONSULTATION_STATUS_CONFIG` | ConsultationList (1处) | 1 处 | | `BLOOD_TYPE_OPTIONS` | constants/health.ts (1处) | 保持 | | `STATUS_OPTIONS` (患者状态) | constants/health.ts (1处) | 保持 | 每个映射组的统一格式: ```typescript export const SEVERITY_CONFIG: Record = { critical: { label: '危急', color: '#DC2626', bg: '#FEF2F2' }, high: { label: '高', color: '#D97706', bg: '#FFFBEB' }, medium: { label: '中', color: '#2563EB', bg: '#EFF6FF' }, low: { label: '低', color: '#6B7280', bg: '#F9FAFB' }, }; ``` ### 4.3 动态选项字典化 新增以下字典编码到 `erp-config` 的字典系统(后端已有 `GET /config/dictionaries/items?code=xxx` API,前端 `apps/web/src/api/dictionaries.ts` 已封装 `listItemsByCode`): | 字典编码 | 用途 | 种子数据来源 | 影响文件 | |----------|------|-------------|---------| | `health_department` | 科室列表 | DoctorList 现有 DEPARTMENT_OPTIONS | DoctorList, DoctorSchedule | | `health_title` | 医护职称 | DoctorList 现有 TITLE_OPTIONS | DoctorList | | `health_device_type` | 设备类型 | DeviceManage 现有 DEVICE_TYPE_OPTIONS | DeviceManage, DeviceReadingsTab, AlertRuleList | | `health_follow_up_type` | 随访类型 | FollowUpTaskList 现有 FOLLOW_UP_TYPE_OPTIONS | FollowUpTaskList | | `health_consultation_type` | 咨询类型 | ConsultationList 现有 CONSULTATION_TYPE_OPTIONS | ConsultationList | | `health_relationship` | 家庭关系 | FamilyMembersTab 现有 RELATIONSHIP_OPTIONS | FamilyMembersTab, family-add(MP) | 前端新增 `useDictionary(code)` hook 封装字典获取 + 缓存逻辑: ```typescript export function useDictionary(code: string) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { listItemsByCode(code) .then(setItems) .catch(() => setItems([])) .finally(() => setLoading(false)); }, [code]); return { items, loading }; } ``` ### 4.4 小程序联动 小程序对应的硬编码同步改为字典 API: | 文件 | 当前硬编码 | 改造 | |------|-----------|------| | `pages/pkg-health/input/index.tsx` INDICATORS | 6 个体征指标 | 从字典获取 + 缓存 | | `pages/health/index.tsx` VITAL_TABS | 4 个体征 tab | 从字典获取 | | `pages/pkg-profile/family-add/index.tsx` RELATION_OPTIONS | 3 个关系选项 | 从字典获取 | | `pages/pkg-profile/family-add/index.tsx` GENDER_OPTIONS | 3 个性别选项 | 保留为静态映射(不在字典化范围) | | `pages/mall/index.tsx` PRODUCT_TYPE_TABS | 商品类型 tab | 从字典获取 | 小程序端新增 `useDict(code)` hook + Taro.storage 本地缓存(24h TTL),离线时使用缓存或内置默认值。 --- ## 5. 轨道 3:医疗阈值配置 ### 5.1 复用现有 `critical_value_threshold` 表 系统已有完整的危急值阈值基础设施,**无需新建表**: - **表**: `critical_value_threshold`(迁移 `m20260426_000060`) - **Entity**: `crates/erp-health/src/entity/critical_value_threshold.rs` - **Handler**: `crates/erp-health/src/handler/critical_value_threshold_handler.rs` - **Service**: `crates/erp-health/src/service/critical_value_threshold_service.rs` - **路由**: `/health/critical-value-thresholds`(CRUD 已注册,见 `module.rs` 行 602-609) - **权限**: `health.critical-value-thresholds.list` + `health.critical-value-thresholds.manage`(已定义) - **种子数据**: 8 条默认记录已存在 现有表字段:`indicator`、`direction`(高/低)、`threshold_value`、`level`(警告/危急)、`department`、`age_min`、`age_max`、`is_active` + 标准审计字段。 ### 5.2 需补充的工作 | 工作项 | 说明 | |--------|------| | 补充 warning 级别种子数据 | 现有种子仅有 critical 级别,需新增 warning 级别阈值(血压 140/90、心率 100/60、血糖 6.1/7.8) | | 新增患者端只读 API | `GET /health/critical-value-thresholds/public` — 认证即可,无需管理权限,返回当前租户所有 `is_active` 阈值 | | Web 端阈值管理 UI | 在告警规则管理页面增加"阈值配置"Tab,复用现有 CRUD API | ### 5.3 前端改造 **小程序改造流程:** 1. App 启动时调用 `GET /health/critical-value-thresholds/public` 获取全量阈值 2. 存入 `Taro.storage`(key: `health_thresholds`,TTL: 24h) 3. 体征页 `health/index.tsx` 从缓存读取阈值替代 `REF_RANGES` 和判断逻辑 4. 输入页 `pkg-health/input/index.tsx` 从缓存读取阈值替代 `WARN_THRESHOLDS` 5. 缓存未命中时使用内置默认值(与当前硬编码值一致) ### 5.4 补充种子数据(warning 级别) 新增迁移脚本插入 warning 级别阈值(现有 critical 级别已由 `m20260426_000060` 种子覆盖): | indicator | level | direction | threshold_value | 说明 | |-----------|-------|-----------|-----------------|------| | blood_pressure_systolic | warning | high | 140 | 收缩压参考上限 | | blood_pressure_diastolic | warning | high | 90 | 舒张压参考上限 | | heart_rate | warning | high | 100 | 心率参考上限 | | heart_rate | warning | low | 60 | 心率参考下限 | | blood_sugar_fasting | warning | high | 6.1 | 空腹血糖参考上限 | | blood_sugar_postprandial | warning | high | 7.8 | 餐后血糖参考上限 | 共 6 条 warning 级别配置,与现有 8 条 critical 级别共同构成完整的阈值体系。 --- ## 6. 跨切面关注点 ### 6.1 错误处理 - 所有新建 API 遵循现有 `AppError` → `ApiResponse` 错误链 - 前端调用失败时降级显示:统计卡片显示 "—",列表显示"暂无数据" - 系统健康检查 API 本身失败时,前端显示"检查中..."而非假数据 ### 6.2 测试策略 | 层级 | 测试内容 | 工具 | |------|---------|------| | 后端单元测试 | 每个 handler + service 函数 | `#[tokio::test]` | | 后端集成测试 | 5 个新 API 的完整请求/响应 | Testcontainers | | 前端单元测试 | `useDictionary` hook + 常量导出一致性 | vitest | | 前端组件测试 | 工作台页面 loading/empty/data 三态渲染 | vitest + testing-library | | E2E 测试 | 工作台页面加载无硬编码假数据 | playwright | ### 6.3 迁移与部署顺序 三条轨道互不依赖,但建议按以下顺序部署: 1. **轨道 2(常量统一)** — 纯前端重构,零后端改动,可立即部署 2. **轨道 3(医疗阈值)** — 复用现有表,仅需补充种子数据 + 新增患者端只读 API,独立部署 3. **轨道 1(工作台 API)** — 需要新建 5 个 API,最后部署 轨道 1 内部的 API 实现顺序: 1. API-5 内容统计(最简单,单表 COUNT) 2. API-4 积分动态(单表 JOIN) 3. API-3 模块状态(读取 AppState) 4. API-2 用户活跃度(多表聚合) 5. API-1 系统健康检查(外部连接检测) --- ## 7. 验证清单 ### 轨道 1 验证 - [ ] AdminDashboard 系统健康条数据来自 API(不出现假数据) - [ ] AdminDashboard 用户活跃度数据来自 API - [ ] AdminDashboard 模块状态来自 API - [ ] OperatorWorkbench 积分动态来自 API - [ ] OperatorWorkbench 内容矩阵来自 API - [ ] OperatorWorkbench 待办来自 actionInbox - [ ] 全部 5 个新 API 在 Swagger UI 可测试 - [ ] `cargo test --workspace` 全部通过 - [ ] `pnpm build` 前端构建通过 ### 轨道 2 验证 - [ ] `SEVERITY_CONFIG` 仅在 `constants/health.ts` 定义一次,其余 4 处改为引用 - [ ] `GENDER_OPTIONS` 仅在 `constants/health.ts` 定义一次 - [ ] 6 个字典编码在后端种子数据中存在 - [ ] `useDictionary` hook 可正常获取字典数据 - [ ] 小程序 `useDict` hook + storage 缓存工作正常 ### 轨道 3 验证 - [ ] `critical_value_threshold` 表已有 + warning 级别种子数据 6 条正确插入 - [ ] 患者端只读 API `GET /health/critical-value-thresholds/public` 可正常访问 - [ ] 小程序体征页使用 API 阈值(非硬编码) - [ ] 小程序输入页使用 API 阈值(非硬编码) - [ ] 离线场景下降级到内置默认值