Files
hms/docs/superpowers/specs/2026-04-24-health-module-iteration-design.md
iven 07f4ba41ba
Some checks failed
CI / frontend-build (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix(health): 穷尽审计修复 — 权限同步/编译错误/前端bug/审计日志
审计发现并修复的问题:

HIGH:
- H1: ConsultationDetail 使用 getSession(id) 替代错误的列表搜索
- H2: SessionResp 添加 version/updated_at 字段
- H3: 移除 FollowUpRecordList 调用不存在的导出端点
- H4: 新增 articles.ts 前端 API 模块

MEDIUM:
- M1: article delete 添加乐观锁 (expected_version)
- M2: 取消预约排班释放传播错误 (log::warn -> ?)
- M3: FollowUpTaskList 日期格式 Dayjs -> string
- M4: 补充 15 个缺失审计日志

LOW:
- L1: 替换 follow_up_service 中的 .unwrap()
- L2: PatientListItem 添加 version 字段

CRITICAL (新发现):
- 权限未同步: 健康模块 14 个权限从未写入数据库,添加启动时自动同步
- migration 表名错误: patients -> patient
- 编译错误: health_trend entity 未导入, ToPrimitive trait 未导入
- HealthError 缺少 From<AppError> 实现
2026-04-25 08:58:58 +08:00

672 lines
26 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.
# 健康管理模块全面迭代设计
> **文档版本**: 1.0
> **日期**: 2026-04-24
> **状态**: 待评审
> **基于**: 5 位专家(后端架构/前端架构/医疗业务/安全质量/产品策略)深度审查
---
## 0. 审查发现总览
### 0.1 V1 发布阻塞项
| # | 阻塞项 | 来源 | 影响 |
|---|--------|------|------|
| B1 | Web 健康模块 10 页面未实现 | 前端架构/产品策略 | 无法演示和交付 |
| B2 | 医疗数据安全不合规 | 安全质量 | 零 sanitize / 零审计 / 身证明文 / 零测试 |
| B3 | 数据一致性缺陷 | 医疗业务/后端架构 | 排班可超额 / 名额释放可能失败 / 随访逾期未实现 |
| B4 | 事件处理器空壳 | 后端架构 | 随访状态/咨询消息不联动 |
### 0.2 当前完成度
| 层级 | 模块 | 完成度 |
|------|------|--------|
| 后端 | erp-health16 实体/8 服务/7 handler/40+ API | 95% |
| 后端 | 事件处理器业务逻辑 | 0%(框架已搭建,需填充 db 操作) |
| 后端 | sanitize / 审计 / 加密 | 0% |
| 后端 | 测试覆盖 | 0% |
| Web 前端 | 健康模块页面 | 0% |
| Web 前端 | 健康模块 API 服务层 | 0% |
| 小程序 | 初版 21 页面 | 85% |
---
## 1. 安全省基(阶段 11.5-2 周)
### 1.1 sanitize 全覆盖
**问题**: erp-health 模块没有任何对 `strip_html_tags` 的调用,攻击者可在患者姓名、病史等字段注入 XSS payload。
**参考实现**: `crates/erp-auth/src/dto.rs` 第 96-118 行,`CreateUserReq``UpdateUserReq` 已实现 `sanitize()` 方法。
**修复方案**: 为每个 DTO 的字符串输入字段添加 sanitize。
**覆盖字段清单**:
| DTO 文件 | 字段 |
|----------|------|
| `patient_dto.rs` CreatePatientReq / UpdatePatientReq | name, notes, allergy_history, medical_history_summary, emergency_contact_name, source |
| `patient_dto.rs` FamilyMemberReqcreate + update 共用) | name, notes |
| `patient_handler.rs` AssignDoctorReq位于 handler 非 dto | — (无字符串字段) |
| `health_data_dto.rs` CreateVitalSignsReq | notes |
| `health_data_dto.rs` CreateLabReportReq | doctor_interpretation |
| `health_data_dto.rs` CreateHealthRecordReq | source, overall_assessment, notes |
| `appointment_dto.rs` CreateAppointmentReq | notes, cancel_reason |
| `follow_up_dto.rs` CreateFollowUpTaskReq / UpdateFollowUpTaskReq | content_template |
| `follow_up_dto.rs` CreateFollowUpRecordReq | patient_condition, medical_advice |
| `consultation_dto.rs` CreateMessageReq | content |
| `consultation_dto.rs` CreateSessionReq | — (无字符串字段) |
| `doctor_dto.rs` CreateDoctorReq / UpdateDoctorReq | department, title, specialty, bio |
**实现模式**:
```rust
// 封装 sanitize 辅助函数(与 erp-auth 的 sanitize_option 模式一致)
fn sanitize_option_string(opt: Option<String>) -> Option<String> {
opt.map(|s| strip_html_tags(&s))
}
// 在每个 DTO 的 impl 中添加 sanitize 方法
impl CreatePatientReq {
pub fn sanitize(&mut self) {
self.name = strip_html_tags(&self.name);
self.notes = sanitize_option_string(self.notes.take());
self.allergy_history = sanitize_option_string(self.allergy_history.take());
self.medical_history_summary = sanitize_option_string(self.medical_history_summary.take());
// ...
}
}
// 在 handler 调用 service 前执行
async fn create_patient(/* ... */) -> AppResult<Json<ApiResponse<PatientResp>>> {
let mut req: CreatePatientReq = Json(req).0;
req.sanitize();
// ...
}
```
**前端安全**: ChatBubble 组件必须使用 React 默认 JSX 转义渲染文本内容(不使用 `dangerouslySetInnerHTML`),图片消息 URL 需做白名单校验。
### 1.2 审计日志注入
**问题**: erp-health 整个模块没有任何对 `audit_service::record` 的调用。
**参考实现**: `crates/erp-auth/src/service/auth_service.rs` 第 168-177 行。
**修复方案**: 在所有写入操作的 service 层添加审计记录。
**覆盖操作清单**:
| Service | 操作 | 审计 action |
|---------|------|------------|
| patient_service | create_patient | `patient.created` |
| patient_service | update_patient | `patient.updated` |
| patient_service | delete_patient | `patient.deleted` |
| patient_service | manage_patient_tags | `patient.tags_updated` |
| health_data_service | create_vital_signs | `vital_signs.created` |
| health_data_service | create_lab_report | `lab_report.created` |
| health_data_service | create_health_record | `health_record.created` |
| appointment_service | create_appointment | `appointment.created` |
| appointment_service | update_appointment_status | `appointment.status_changed` |
| follow_up_service | create_task | `follow_up_task.created` |
| follow_up_service | create_record | `follow_up_record.created` |
| consultation_service | create_session | `consultation.opened` |
| consultation_service | close_session | `consultation.closed` |
| consultation_service | create_message | `consultation.message_sent` |
| doctor_service | create/update/delete_doctor | `doctor.*` |
**审计日志内容**: tenant_id、user_id、action、resource_type、resource_id、变更前后值摘要。
**注意**: 当前 `audit_service::record` 是 fire-and-forget审计日志丢失对医疗合规不可接受。修复方案
1. 新增 `record_in_txn(log: AuditLog, txn: &DatabaseTransaction)` 方法,在事务内 await 写入
2. 保留原 `record` 方法用于不要求事务保证的场景
3. erp-health 的关键写入操作使用 `record_in_txn`,失败时回滚整个事务
4. 需要改为事务包裹的 service 方法create_patient、update_patient、delete_patient、create_appointment、update_appointment_status、create_record随访、create_message咨询
### 1.3 身份证号加密存储
**问题**: `patient.id_number` 明文存储在数据库中,违反《个人信息保护法》。
**方案**: AES-256-GCM 应用层加密。
**新增文件**: `crates/erp-health/src/crypto.rs`
```rust
pub struct HealthCrypto { key: [u8; 32] }
impl HealthCrypto {
pub fn from_env() -> Self { /* 从 ERP__HEALTH__ENCRYPTION_KEY 读取 */ }
pub fn encrypt(&self, plaintext: &str) -> AppResult<String> { /* AES-256-GCM + Base64 */ }
pub fn decrypt(&self, ciphertext: &str) -> AppResult<String> { /* 解密 */ }
}
```
**集成点**:
- `patient_service::create_patient` — 加密 id_number 后存储
- `patient_service::update_patient` — 同上
- `patient_service::get_patient` — 解密后返回
- `patient_service::list_patients` — 列表不返回 id_number脱敏
**密钥管理**: 环境变量 `ERP__HEALTH__ENCRYPTION_KEY`32 字节 hex必须在 `default.toml` 中标记为 `__MUST_SET_VIA_ENV__`
**搜索兼容**: `patient.id_number` 的模糊搜索(`contains`)改为精确匹配(`eq`),在加密后使用 HMAC 索引做等值查询。
**HMAC 索引详情**:
- 新增数据库列 `id_number_hash VARCHAR(64)`,存储 HMAC-SHA256 哈希
- HMAC 密钥独立于 AES 密钥,从环境变量 `ERP__HEALTH__HMAC_KEY` 读取
- 创建/更新患者时同时写入 hash 列,等值查询使用 `WHERE id_number_hash = hmac(输入值)`
- 迁移 SQL新增列 → 批量加密现有明文 → 删除原明文列(可选)
**数据迁移方案**:
1. 停机窗口(预估 1-2 小时,视数据量)
2. 迁移脚本:`SELECT id, id_number FROM patients WHERE id_number IS NOT NULL AND deleted_at IS NULL` → 批量加密 → `UPDATE patients SET id_number = $encrypted WHERE id = $id`
3. 同步写入 `id_number_hash`
4. 验证脚本:抽样解密比对原值
5. 回滚方案:保留明文备份表 `patients_id_number_backup`72 小时后确认无误再删除
**问题**: 列表接口直接返回完整身份证号、病史等敏感字段。
**修复方案**: 拆分响应 DTO。
```rust
// 列表用 — 不含敏感字段
pub struct PatientListResp {
pub id: Uuid,
pub name: String,
pub gender: Option<String>,
pub birth_date: Option<NaiveDate>,
pub status: String,
pub tags: Vec<TagResp>,
// 无 id_number, allergy_history, medical_history_summary, emergency_contact_phone 等
}
// 详情用 — 敏感字段掩码
pub struct PatientDetailResp {
// ... 全部字段
pub id_number: Option<String>, // "320***********1234"
pub emergency_contact_phone: Option<String>, // "138****1234"
}
```
---
## 2. 后端补完(阶段 21.5 周)
### 2.1 事件处理器实现
**问题**: `event.rs` 中两个事件处理器只有 `tracing::info`,无实际业务逻辑。且 handler 中没有 `DatabaseConnection`,无法执行数据库操作。
**方案**: 在 `HealthModule::on_startup` 中创建 `HealthState` 并注册需要数据库访问的事件处理器。将现有 `register_event_handlers` 中的空壳代码迁移到 `on_startup``register_event_handlers` 改为空实现。
**修改 `crates/erp-health/src/module.rs`**:
```rust
// register_event_handlers 改为空实现
fn register_event_handlers(&self, _bus: &EventBus) {
// 事件处理器迁移到 on_startup此处不再注册
}
// on_startup 中注册带 db 的事件处理器
async fn on_startup(&self, ctx: &ModuleContext) -> AppResult<()> {
let state = HealthState {
db: ctx.db.clone(),
event_bus: ctx.event_bus.clone(),
};
crate::event::register_handlers_with_state(state);
Ok(())
}
```
**修改 `crates/erp-health/src/event.rs`**:
新增 `register_handlers_with_state(state: HealthState)` 函数替代原有 `register_handlers`
**事件处理器业务逻辑**:
`workflow.task.completed`:
1. 从 payload 中提取 `task_id`
2. 查询 `follow_up_task WHERE related_appointment_id` 或通过 payload 映射
3. 更新随访任务状态为 `completed`
`message.sent`:
1. 从 payload 中提取 `session_id`(或通过 sender/recipient 关联)
2. 更新 `consultation_session SET last_message_at = NOW(), unread_count = unread_count + 1`
3. 使用 `check_version` 乐观锁
### 2.2 数据一致性修复
#### 2.2.1 排班名额保护
**问题**: `update_schedule` 可以将 `max_appointments` 改为小于 `current_appointments` 的值。
**修复**: 在 `appointment_service.rs``update_schedule` 方法中增加校验:
```rust
if req.max_appointments < model.current_appointments {
return Err(HealthError::Validation(
"max_appointments 不能小于当前已预约数".into()
).into());
}
```
#### 2.2.2 取消预约名额释放
**问题**: `update_appointment_status` 中取消时名额释放失败只 log error 不回滚。
**修复**: 将名额释放作为事务的一部分,失败时回滚整个操作(包括状态更新)。
#### 2.2.3 咨询消息原子性
**问题**: `create_message` 中消息已插入,但后续 CAS 更新 session 失败时返回错误 — 消息已持久化但 session 元数据未更新。
**修复**: 将消息 INSERT + session CAS 更新放在同一个事务中。
### 2.3 随访逾期定时任务
**问题**: 设计规格定义了 `overdue` 状态和定时任务自动标记,但代码中:
- `validation.rs` 不允许转换到 `overdue`
- 没有后台定时任务
**修复**:
1.`validation.rs` 中添加 `overdue` 转换规则:`pending -> overdue`(仅限系统自动触发)
2.`erp-server/src/main.rs` 后台任务区增加逾期检查器,使用与现有 `start_timeout_checker` 一致的 `tokio::spawn` + `loop` + `tokio::time::interval` 模式(每 6 小时执行一次,非 cron 表达式):
```rust
// erp-server/src/main.rs 后台任务区
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(6 * 3600));
loop {
interval.tick().await;
// 调用 health module 的 check_overdue_tasks
}
});
```
3.`erp-health` module 中添加一个公开方法 `check_overdue_tasks` 供定时任务调用。
### 2.4 article 管理 CRUD
**问题**: 权限声明中有 `health.articles.manage`,但 service/handler 只有 list 和 get。
**修复**: 在 `article_service.rs``article_handler.rs` 中补充 create/update/delete 方法。在 `module.rs` 中添加路由。**工时估算**: 0.5 天。
---
## 3. Web 前端 10 页面(阶段 33.5-4 周)
### 3.1 页面文件组织
```
apps/web/src/
├── api/health/
│ ├── patients.ts # 12 端点
│ ├── healthData.ts # 13 端点
│ ├── appointments.ts # 6 端点
│ ├── followUp.ts # 6 端点
│ ├── consultations.ts # 6 端点
│ └── doctors.ts # 4 端点
├── pages/health/
│ ├── PatientList.tsx # 患者列表
│ ├── PatientDetail.tsx # 患者详情5 Tab
│ ├── PatientTagManage.tsx # 标签管理
│ ├── DoctorList.tsx # 医护列表
│ ├── AppointmentList.tsx # 预约管理
│ ├── DoctorSchedule.tsx # 排班管理
│ ├── FollowUpTaskList.tsx # 随访任务
│ ├── FollowUpRecordList.tsx # 随访台账
│ ├── ConsultationList.tsx # 会话管理
│ ├── ConsultationDetail.tsx # 对话详情
│ └── components/
│ ├── StatusTag.tsx # 通用状态标签
│ ├── PatientSelect.tsx # 患者搜索选择器
│ ├── DoctorSelect.tsx # 医护选择器
│ ├── VitalSignsChart.tsx # ECharts 趋势图
│ ├── CalendarView.tsx # 日历视图
│ ├── ChatBubble.tsx # 聊天气泡
│ ├── ImagePreview.tsx # 图片预览
│ └── ExportButton.tsx # 导出按钮
```
### 3.2 API 服务层设计
每个 service 文件遵循现有 `api/users.ts` 的解构模式:
```typescript
// api/health/patients.ts
import client from '../client';
export interface Patient {
id: string;
name: string;
gender?: string;
birth_date?: string;
status: string;
tags: Tag[];
// ...
}
export interface CreatePatientReq {
name: string;
gender?: string;
// ...
}
export const patientApi = {
list: async (params: ListParams) => {
const { data } = await client.get<{ success: boolean; data: PaginatedResponse<Patient> }>(
'/health/patients', { params }
);
return data.data;
},
get: async (id: string) => {
const { data } = await client.get<{ success: boolean; data: Patient }>(
`/health/patients/${id}`
);
return data.data;
},
create: async (req: CreatePatientReq) => {
const { data } = await client.post<{ success: boolean; data: Patient }>(
'/health/patients', req
);
return data.data;
},
// ...
};
```
### 3.3 路由注册
`App.tsx` 中新增:
```typescript
// lazy imports
const PatientList = lazy(() => import('./pages/health/PatientList'));
const PatientDetail = lazy(() => import('./pages/health/PatientDetail'));
// ... 共 10 个路由组件
// Routes 内
<Route path="/health/patients" element={<PatientList />} />
<Route path="/health/patients/:id" element={<PatientDetail />} />
<Route path="/health/tags" element={<PatientTagManage />} />
<Route path="/health/doctors" element={<DoctorList />} />
<Route path="/health/appointments" element={<AppointmentList />} />
<Route path="/health/schedules" element={<DoctorSchedule />} />
<Route path="/health/follow-up-tasks" element={<FollowUpTaskList />} />
<Route path="/health/follow-up-records" element={<FollowUpRecordList />} />
<Route path="/health/consultations" element={<ConsultationList />} />
<Route path="/health/consultations/:id" element={<ConsultationDetail />} />
```
### 3.4 侧边栏菜单
`MainLayout.tsx` 中新增 `healthMenuItems` 数组(参照现有 `bizMenuItems` 模式),使用 `@ant-design/icons` 图标(如 `MedicineBoxOutlined``HeartOutlined``CalendarOutlined``PhoneOutlined``CommentOutlined``TagsOutlined`
```
侧边栏布局:
├── 首页 (HomeOutlined)
├── 用户管理 (UserOutlined)
├── 权限管理 (SafetyOutlined)
├── 工作流 (ApartmentOutlined)
├── 消息中心 (BellOutlined)
├── ─────────
├── 健康管理 (MedicineBoxOutlined) ← 新增组
│ ├── 患者管理 (TeamOutlined)
│ ├── 医护管理 (HeartOutlined)
│ ├── 预约排班 (CalendarOutlined)
│ ├── 随访管理 (PhoneOutlined)
│ ├── 咨询管理 (CommentOutlined)
│ └── 标签管理 (TagsOutlined)
├── ─────────
├── 插件管理 (AppstoreOutlined)
├── 系统设置 (SettingOutlined)
```
### 3.5 前端权限集成
后端已有完整权限体系14 个权限码),前端 V1 阶段采用以下策略:
1. **路由级权限**: 所有健康模块路由在 `PrivateRoute` 内(已实现),后端 `require_permission` 拦截无权限请求返回 403
2. **按钮级权限V1 简化)**: 不做前端按钮级权限控制,依赖后端 403 响应。后续可扩展 `usePermission` hook
3. **菜单可见性**: 健康模块菜单组始终显示,但无权限用户点击任何页面会收到 403 提示
### 3.5 13 页面逐一设计
#### PatientList.tsx中复杂度1.5 天)
- Ant Design `Table` 组件(与 Users.tsx 模式一致,不使用 ProTable
- 搜索:姓名模糊 + 状态筛选 + 标签多选筛选
- 每行显示患者标签为 `Tag` 组件列表
- 行点击跳转 `/health/patients/:id`
- 批量操作:批量打标
- 导出功能
#### PatientDetail.tsx高复杂度3 天)
- 顶部:患者摘要卡片(姓名/性别/年龄/状态/标签)
- Ant Design `Tabs` 5 个 Tab
1. **基本信息**`Descriptions` 展示 + 编辑 Modal
2. **健康趋势**`VitalSignsChart` 组件 + 时间范围选择器
3. **化验报告** — 报告卡片列表 + `ImagePreview` 指标详情
4. **就诊记录** — 嵌套列表(体检/门诊/住院)
5. **随访记录** — 嵌套列表 + 关联的随访记录
#### PatientTagManage.tsx低复杂度0.5 天)
- 标准 CRUD 表格
- 颜色选择器Ant Design `ColorPicker`
- 批量打标功能
#### DoctorList.tsx低复杂度0.5 天)
- 标准 CRUD 表格
- 科室筛选 + 在线状态 Badgeonline=绿/busy=黄/offline=灰)
- 详情 Drawer
#### AppointmentList.tsx中复杂度2 天)
- `Segmented` 切换列表/日历视图
- 列表模式:表格 + 状态筛选 + 日期筛选
- 日历模式:`Calendar` + `cellRender` 显示当日预约数
- 状态流转 Dropdownpending → confirmed → completed/no_show/cancelled
- 创建预约 Modal选择患者 + 医生 + 日期时段 + 检查排班余量)
#### DoctorSchedule.tsx高复杂度2.5 天)
- 选择医生后展示其排班
- 周视图(自定义 7 列网格,每列显示一天的排班时段)
- 月视图Ant Design Calendar
- 批量创建排班(选择日期范围 + 时段模板)
- 显示已预约/最大预约数
#### FollowUpTaskList.tsx中复杂度1.5 天)
- 表格 + 状态筛选pending/in_progress/completed/overdue/cancelled
- 分配给医护(`DoctorSelect`
- 创建任务 Modal
- 快捷"填写随访记录"按钮打开子 Modal
#### FollowUpRecordList.tsx低复杂度0.5 天)
- 纯只读台账
- 筛选:日期范围、患者、任务、结果
- 导出功能(`ExportButton`
#### ConsultationList.tsx中复杂度1 天)
- 表格 + 状态筛选waiting/active/closed
- 未读消息数 Badge
- 最后消息时间
- 关闭会话操作
- 点击跳转 `/health/consultations/:id`
#### ConsultationDetail.tsx高复杂度2 天)
- `ChatBubble` 组件渲染聊天气泡
- 根据 `sender_role` 区分左右对齐
- 支持内容类型text / image`ImagePreview`/ voice / file
- 消息按时间排列,支持滚动加载更多(分页)
- 导出按钮
### 3.6 技术难点方案
#### ECharts 趋势图
使用已安装的 `@ant-design/charts``Line` 组件。
- 后端 API `/patients/:id/trends/:indicator` 返回时序数据
- 前端转换为 `{ date: string, value: number }[]`
- 支持多指标叠加(血压收缩压/舒张压双线)
- 封装为 `VitalSignsChart`,接收 `patientId` + `indicators` 参数
- 时间范围选择器7天/30天/90天
#### 日历视图
Ant Design `Calendar` + 自定义 `cellRender`
- DoctorSchedule每个日期格显示排班时段标签
- AppointmentList每个日期格显示预约数量气泡
#### 聊天 UI
自定义 `ChatBubble` 组件,基于 Ant Design `Typography.Paragraph` + `Avatar`
- 根据 `sender_role` 区分样式
- 只读模式PC 后台只查看不发送)
- 图片消息使用 `Image.PreviewGroup`
#### 导出
后端 blob 导出 + 前端触发下载,参照 `PluginCRUDPage` 中已有的 `exportPluginDataAsBlob` 模式。
#### 文件上传/预览
- 上传Ant Design `Upload.Dragger`,上传到后端文件接口
- 图片预览Ant Design `Image.PreviewGroup`
- PDF 预览新窗口打开V1 简化方案)
### 3.7 开发顺序
| Phase | 内容 | 天数 | 依赖 |
|-------|------|------|------|
| 1 | API 层 6 文件 + 通用组件 + 路由菜单 | 1.5 | 无 |
| 2 | PatientList + PatientTagManage + PatientDetail 基本信息Tab | 2 | Phase 1 |
| 3 | VitalSignsChart + 健康趋势 Tab + LabReportList + HealthRecordList | 3 | Phase 2 |
| 4 | DoctorList + AppointmentList + DoctorSchedule | 3 | Phase 1 |
| 5 | FollowUpTaskList + FollowUpRecordList + ConsultationList + ConsultationDetail | 3 | Phase 1 |
| 6 | 打磨(暗色主题 + 响应式 + 联调) | 1 | Phase 2-5 |
| **合计** | | **13.5 天** | |
---
## 4. 测试策略(阶段 2-3 交叉进行)
### 4.1 优先级排序
| 优先级 | 测试目标 | 预估用例数 | 工作量 |
|--------|---------|-----------|--------|
| P0 | `validation.rs` 纯函数 | 20-30 | 1 天 |
| P0 | `appointment_service` CAS + 状态流转 | 15-20 | 2 天 |
| P0 | `patient_service` CRUD + 状态机 | 15-20 | 2 天 |
| P1 | `consultation_service` 消息原子性 | 10-15 | 2 天 |
| P1 | `health_data_service` 指标数据 | 10-15 | 1 天 |
| P2 | `follow_up_service` 链式任务 | 10 | 1 天 |
### 4.2 测试基础设施
`erp-health/Cargo.toml` 中添加 `[dev-dependencies]`
- `tokio``test``macros` feature
- `sea-orm``mock` feature用于简单单元测试如 validation 纯函数)
对于涉及事务和 CAS 的集成测试(预约并发、消息原子性),使用 testcontainers-postgreSQL 做真实数据库测试,因为 SeaORM 的 `MockDatabaseConnection` 不支持复杂事务模拟。
创建 `tests/test_helpers.rs` 提供:
- `create_test_health_state()` — 带 mock db 的 HealthState单元测试用
- `create_integration_db()` — testcontainers PostgreSQL 实例(集成测试用)
- 共享 fixture 工厂
### 4.3 关键测试场景
**预约 CAS 并发**:
- 排班已满 → 创建预约失败
- 排班有余 → CAS 成功 + 名额减 1
- 并发创建 → 只有 max_appointments 个成功
**状态机转换**:
- 合法转换pending → confirmed → completed
- 非法转换completed → pending → 拒绝
- 取消:任意状态 → cancelled填 cancel_reason
**随访链式任务**:
- next_follow_up_date 不为空 → 自动创建新任务
- 新任务的 assigned_to 沿用当前医护
- next_follow_up_date 为空 → 不创建新任务
---
## 5. 实施路线图
### 5.1 总时间线(调整为 7 周)
```
Week 1-2 | 安全地基1.5-2 周)
| ├── sanitize 全覆盖2 天)
| ├── 审计日志注入2 天)
| ├── 身份证号加密 + HMAC 索引 + 数据迁移3-4 天)
| └── 字段级脱敏1-2 天)
Week 2-4 | 后端补完 + 测试1.5-2 周)
| ├── 事件处理器实现2 天)
| ├── 数据一致性修复2 天)
| ├── 随访逾期定时任务1 天)
| ├── article CRUD0.5 天)
| └── 核心路径测试5-6 天)
Week 4-7 | Web 前端3.5-4 周)
| ├── Phase 1: API 层 + 通用组件 + 路由菜单1.5 天)
| ├── Phase 2: 核心入口页面2 天)
| ├── Phase 3: 健康数据页面3 天)
| ├── Phase 4: 预约排班页面3 天)
| ├── Phase 5: 随访咨询页面3 天)
| └── Phase 6: 打磨联调1 天)
Week 7-8 | 端到端验证1 周)
| ├── 小程序联调
| ├── 种子数据填充
| ├── Docker 演示环境
| └── 文档更新
```
### 5.2 里程碑
| 里程碑 | 交付物 | 验收标准 |
|--------|--------|---------|
| M1 | 安全省基完成 | sanitize + 审计 + 加密 + 脱敏全部到位cargo test 通过 |
| M2 | 后端功能完整 | 事件处理器 + 数据一致性 + 测试覆盖cargo test 通过 |
| M3 | Web 3 核心页面 | PatientList + AppointmentList + DoctorSchedule 可操作 |
| M4 | Web 10 页面完成 | 所有页面功能可用pnpm build 通过 |
| M5 | 端到端验证 | Web + 小程序 + 后端全链路可演示 |
### 5.3 风险和缓解
| 风险 | 概率 | 缓解 |
|------|------|------|
| ECharts 集成复杂度高 | 中 | 使用 @ant-design/charts 已安装,降低自研成本 |
| 身份证加密影响现有查询 | 中 | HMAC 索引 + 数据迁移脚本 + 备份表 + 回滚方案 |
| 10 页面开发时间超预期 | 高 | 按优先级裁剪MVP 先做 3 核心页面 |
| 文件上传能力未就绪 | 中 | V1 先支持 URL 存储,文件上传推迟到 V1.1 |
---
## 6. 不在本设计范围内(推迟到 V2
- 积分商城
- 数据统计中心 / 运营驾驶舱
- AI 辅助诊断/报告解读
- 实时 WebSocket 在线咨询
- 咨询消息按月分区
- 事件幂等性processed_events 去重表)
- Polling Outbox 重试机制
- HealthState 扩展 Redis 缓存
- 国际化(英文等多语言)
- 小程序医护端