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> 实现
This commit is contained in:
1626
docs/superpowers/plans/2026-04-24-health-module-iteration.md
Normal file
1626
docs/superpowers/plans/2026-04-24-health-module-iteration.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,671 @@
|
||||
# 健康管理模块全面迭代设计
|
||||
|
||||
> **文档版本**: 1.0
|
||||
> **日期**: 2026-04-24
|
||||
> **状态**: 待评审
|
||||
> **基于**: 5 位专家(后端架构/前端架构/医疗业务/安全质量/产品策略)深度审查
|
||||
|
||||
---
|
||||
|
||||
## 0. 审查发现总览
|
||||
|
||||
### 0.1 V1 发布阻塞项
|
||||
|
||||
| # | 阻塞项 | 来源 | 影响 |
|
||||
|---|--------|------|------|
|
||||
| B1 | Web 健康模块 10 页面未实现 | 前端架构/产品策略 | 无法演示和交付 |
|
||||
| B2 | 医疗数据安全不合规 | 安全质量 | 零 sanitize / 零审计 / 身证明文 / 零测试 |
|
||||
| B3 | 数据一致性缺陷 | 医疗业务/后端架构 | 排班可超额 / 名额释放可能失败 / 随访逾期未实现 |
|
||||
| B4 | 事件处理器空壳 | 后端架构 | 随访状态/咨询消息不联动 |
|
||||
|
||||
### 0.2 当前完成度
|
||||
|
||||
| 层级 | 模块 | 完成度 |
|
||||
|------|------|--------|
|
||||
| 后端 | erp-health(16 实体/8 服务/7 handler/40+ API) | 95% |
|
||||
| 后端 | 事件处理器业务逻辑 | 0%(框架已搭建,需填充 db 操作) |
|
||||
| 后端 | sanitize / 审计 / 加密 | 0% |
|
||||
| 后端 | 测试覆盖 | 0% |
|
||||
| Web 前端 | 健康模块页面 | 0% |
|
||||
| Web 前端 | 健康模块 API 服务层 | 0% |
|
||||
| 小程序 | 初版 21 页面 | 85% |
|
||||
|
||||
---
|
||||
|
||||
## 1. 安全省基(阶段 1,1.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` FamilyMemberReq(create + 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. 后端补完(阶段 2,1.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 页面(阶段 3,3.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 表格
|
||||
- 科室筛选 + 在线状态 Badge(online=绿/busy=黄/offline=灰)
|
||||
- 详情 Drawer
|
||||
|
||||
#### AppointmentList.tsx(中复杂度,2 天)
|
||||
|
||||
- `Segmented` 切换列表/日历视图
|
||||
- 列表模式:表格 + 状态筛选 + 日期筛选
|
||||
- 日历模式:`Calendar` + `cellRender` 显示当日预约数
|
||||
- 状态流转 Dropdown(pending → 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 CRUD(0.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 缓存
|
||||
- 国际化(英文等多语言)
|
||||
- 小程序医护端
|
||||
Reference in New Issue
Block a user