416 lines
18 KiB
Markdown
416 lines
18 KiB
Markdown
# 系统硬编码清理设计规格
|
||
|
||
> 日期: 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 阈值(非硬编码)
|
||
- [ ] 离线场景下降级到内置默认值
|