Files
hms/docs/superpowers/specs/2026-05-02-hardcoding-cleanup-design.md
iven c208dcc6f5 docs(specs): 7 份设计规格 — 工作台/适老化/硬编码清理/项目分析
新增: 适老化小程序/Action Inbox/统一工作台/医生操作台/
硬编码清理/健康管理台/全项目深度分析报告
2026-05-03 19:32:25 +08:00

416 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 系统硬编码清理设计规格
> 日期: 2026-05-02 | 状态: DRAFT | 范围: 前端硬编码清理 + 后端 API 补建 + 常量统一 + 医疗阈值配置化
---
## 1. 背景与动机
HMS 健康管理平台已进入关键节点 — 后端 328 条路由、45 个 Entity、772 个测试函数已就位。但前端工作台页面存在大量硬编码假数据,直接影响系统可信度:
- **AdminDashboard** 中 70% 面板数据为假值(系统健康条、用户活跃度、模块状态、角色分布)
- **OperatorWorkbench** 中积分动态使用假姓名、内容矩阵数字硬编码、待办事项不可操作
- 20+ 处状态映射在多个文件中重复定义(严重度映射 5 处、性别映射 4 处、设备类型 3 处)
- 医疗报警阈值(血压/心率/血糖)硬编码在小程序前端,不同患者/年龄段无法差异化
**目标**:清除所有 CRITICAL 级硬编码,确保用户看到的每个数字都来自真实 API同时建立常量管理规范防止回退。
**原则**
- 新建 API 全部遵循现有 `/api/v1/` 前缀 + `ApiResponse<T>` 包装 + 多租户隔离
- 常量统一采用混合策略:静态枚举收敛到 `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<SystemHealth>
```
```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<UserActivity>
```
```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<ModuleStatus[]>
```
```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<PointsActivityItem[]>
```
> **命名约定**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<ArticleStats>
```
```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<string, { label: string; color: string; bg: string }> = {
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<DictionaryItem[]>([]);
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 阈值(非硬编码)
- [ ] 离线场景下降级到内置默认值