fix(health): 穷尽审计修复 — 权限同步/编译错误/前端bug/审计日志
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

审计发现并修复的问题:

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> 实现
This commit is contained in:
iven
2026-04-25 08:58:58 +08:00
parent 9ffb938128
commit 07f4ba41ba
31 changed files with 3373 additions and 445 deletions

View File

@@ -0,0 +1,671 @@
# 健康管理模块全面迭代设计
> **文档版本**: 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 缓存
- 国际化(英文等多语言)
- 小程序医护端