fix: QA 第二轮修复 — PatientDetail 重构/测试覆盖/id_number 列宽/小程序 URL 规范化
- refactor(web): PatientDetail.tsx 拆分为 4 个子组件(737→334行) - refactor(web): 提取 usePaginatedData hook 消除重复分页状态 - feat(db): patient.id_number varchar(20)→varchar(255) 容纳加密值 - test(health): 添加预约模块集成测试(创建/列表/租户隔离) - test(plugin): 添加 6 个 SQL 注入 sanitize 测试 - fix(miniprogram): 7 个 service 文件 URL 构建规范化(params 对象) - fix(miniprogram): 跨平台字段名对齐(birth_date/start_time/end_time)
This commit is contained in:
@@ -8,3 +8,5 @@ mod plugin_tests;
|
||||
mod workflow_tests;
|
||||
#[path = "integration/health_patient_tests.rs"]
|
||||
mod health_patient_tests;
|
||||
#[path = "integration/health_appointment_tests.rs"]
|
||||
mod health_appointment_tests;
|
||||
|
||||
248
crates/erp-server/tests/integration/health_appointment_tests.rs
Normal file
248
crates/erp-server/tests/integration/health_appointment_tests.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
//! erp-health 预约排班集成测试
|
||||
//!
|
||||
//! 验证预约 CRUD、租户隔离、CAS 排班名额等核心行为。
|
||||
//! 使用 TestDb 创建隔离 PostgreSQL 数据库,直接调用 service 层函数。
|
||||
//! 预约创建依赖患者 + 医护档案 + 排班三条前置数据。
|
||||
|
||||
use erp_core::events::EventBus;
|
||||
use erp_health::dto::appointment_dto::CreateAppointmentReq;
|
||||
use erp_health::dto::doctor_dto::CreateDoctorReq;
|
||||
use erp_health::dto::patient_dto::CreatePatientReq;
|
||||
use erp_health::service::{
|
||||
appointment_service, doctor_service, patient_service,
|
||||
};
|
||||
use erp_health::state::HealthState;
|
||||
use erp_health::HealthCrypto;
|
||||
|
||||
use super::test_db::TestDb;
|
||||
|
||||
/// 构建测试用 HealthState
|
||||
fn make_state(db: &sea_orm::DatabaseConnection) -> HealthState {
|
||||
HealthState {
|
||||
db: db.clone(),
|
||||
event_bus: EventBus::new(100),
|
||||
crypto: HealthCrypto::dev_default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建患者并返回其 ID
|
||||
async fn seed_patient(
|
||||
state: &HealthState,
|
||||
tenant_id: uuid::Uuid,
|
||||
name: &str,
|
||||
) -> uuid::Uuid {
|
||||
let req = CreatePatientReq {
|
||||
name: name.to_string(),
|
||||
gender: Some("male".to_string()),
|
||||
birth_date: None,
|
||||
blood_type: None,
|
||||
id_number: None,
|
||||
allergy_history: None,
|
||||
medical_history_summary: None,
|
||||
emergency_contact_name: None,
|
||||
emergency_contact_phone: None,
|
||||
source: None,
|
||||
notes: None,
|
||||
};
|
||||
let patient = patient_service::create_patient(state, tenant_id, None, req)
|
||||
.await
|
||||
.expect("创建患者应成功");
|
||||
patient.id
|
||||
}
|
||||
|
||||
/// 创建医护档案并返回其 ID
|
||||
async fn seed_doctor(
|
||||
state: &HealthState,
|
||||
tenant_id: uuid::Uuid,
|
||||
name: &str,
|
||||
) -> uuid::Uuid {
|
||||
let req = CreateDoctorReq {
|
||||
user_id: None,
|
||||
name: name.to_string(),
|
||||
department: Some("内科".to_string()),
|
||||
title: Some("主治医师".to_string()),
|
||||
specialty: Some("心血管内科".to_string()),
|
||||
license_number: None,
|
||||
bio: None,
|
||||
};
|
||||
let doctor = doctor_service::create_doctor(state, tenant_id, None, req)
|
||||
.await
|
||||
.expect("创建医护档案应成功");
|
||||
doctor.id
|
||||
}
|
||||
|
||||
/// 创建排班并返回其 ID
|
||||
async fn seed_schedule(
|
||||
state: &HealthState,
|
||||
tenant_id: uuid::Uuid,
|
||||
doctor_id: uuid::Uuid,
|
||||
date: chrono::NaiveDate,
|
||||
) -> uuid::Uuid {
|
||||
let req = erp_health::dto::appointment_dto::CreateScheduleReq {
|
||||
doctor_id,
|
||||
schedule_date: date,
|
||||
period_type: Some("am".to_string()),
|
||||
start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
|
||||
end_time: chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
|
||||
max_appointments: 10,
|
||||
};
|
||||
let schedule = appointment_service::create_schedule(state, tenant_id, None, req)
|
||||
.await
|
||||
.expect("创建排班应成功");
|
||||
schedule.id
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 测试 1: 创建预约
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_appointment() {
|
||||
let test_db = TestDb::new().await;
|
||||
let state = make_state(test_db.db());
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
let operator_id = uuid::Uuid::new_v4();
|
||||
|
||||
// 前置:患者 + 医护 + 排班
|
||||
let patient_id = seed_patient(&state, tenant_id, "预约测试患者").await;
|
||||
let doctor_id = seed_doctor(&state, tenant_id, "预约测试医生").await;
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 10).unwrap();
|
||||
seed_schedule(&state, tenant_id, doctor_id, date).await;
|
||||
|
||||
let req = CreateAppointmentReq {
|
||||
patient_id,
|
||||
doctor_id: Some(doctor_id),
|
||||
appointment_type: Some("outpatient".to_string()),
|
||||
appointment_date: date,
|
||||
start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
|
||||
end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
|
||||
notes: Some("首次就诊".to_string()),
|
||||
};
|
||||
|
||||
let appointment =
|
||||
appointment_service::create_appointment(&state, tenant_id, Some(operator_id), req)
|
||||
.await
|
||||
.expect("创建预约应成功");
|
||||
|
||||
assert_eq!(appointment.patient_id, patient_id);
|
||||
assert_eq!(appointment.doctor_id, Some(doctor_id));
|
||||
assert_eq!(appointment.appointment_type, "outpatient");
|
||||
assert_eq!(appointment.status, "pending");
|
||||
assert_eq!(appointment.version, 1);
|
||||
assert_eq!(
|
||||
appointment.notes,
|
||||
Some("首次就诊".to_string())
|
||||
);
|
||||
|
||||
// 通过 get_appointment 验证存储正确
|
||||
let found = appointment_service::get_appointment(&state, tenant_id, appointment.id)
|
||||
.await
|
||||
.expect("查询预约应成功");
|
||||
assert_eq!(found.id, appointment.id);
|
||||
assert_eq!(found.status, "pending");
|
||||
assert_eq!(found.version, 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 测试 2: 列表查询 — 创建 2 条预约后验证分页计数
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_appointments() {
|
||||
let test_db = TestDb::new().await;
|
||||
let state = make_state(test_db.db());
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
|
||||
let patient_id = seed_patient(&state, tenant_id, "列表测试患者").await;
|
||||
let doctor_id = seed_doctor(&state, tenant_id, "列表测试医生").await;
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 12).unwrap();
|
||||
seed_schedule(&state, tenant_id, doctor_id, date).await;
|
||||
|
||||
// 创建 2 条预约(同一个排班时段,CAS 按排班 start_time 匹配)
|
||||
for _i in 0..2 {
|
||||
let req = CreateAppointmentReq {
|
||||
patient_id,
|
||||
doctor_id: Some(doctor_id),
|
||||
appointment_type: None,
|
||||
appointment_date: date,
|
||||
start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
|
||||
end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
|
||||
notes: None,
|
||||
};
|
||||
appointment_service::create_appointment(&state, tenant_id, None, req)
|
||||
.await
|
||||
.expect("创建预约应成功");
|
||||
}
|
||||
|
||||
let result = appointment_service::list_appointments(
|
||||
&state,
|
||||
tenant_id,
|
||||
1,
|
||||
10,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("列表查询应成功");
|
||||
|
||||
assert_eq!(result.total, 2, "应有 2 条预约记录");
|
||||
assert_eq!(result.data.len(), 2, "当前页应返回 2 条");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 测试 3: 租户隔离 — 租户 A 的预约对租户 B 不可见
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_appointment_tenant_isolation() {
|
||||
let test_db = TestDb::new().await;
|
||||
let state = make_state(test_db.db());
|
||||
let tenant_a = uuid::Uuid::new_v4();
|
||||
let tenant_b = uuid::Uuid::new_v4();
|
||||
|
||||
// 租户 A 创建完整链路:患者 + 医护 + 排班 + 预约
|
||||
let patient_a = seed_patient(&state, tenant_a, "租户A患者").await;
|
||||
let doctor_a = seed_doctor(&state, tenant_a, "租户A医生").await;
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2026, 6, 1).unwrap();
|
||||
seed_schedule(&state, tenant_a, doctor_a, date).await;
|
||||
|
||||
let req = CreateAppointmentReq {
|
||||
patient_id: patient_a,
|
||||
doctor_id: Some(doctor_a),
|
||||
appointment_type: None,
|
||||
appointment_date: date,
|
||||
start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
|
||||
end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
|
||||
notes: None,
|
||||
};
|
||||
let appointment_a =
|
||||
appointment_service::create_appointment(&state, tenant_a, None, req)
|
||||
.await
|
||||
.expect("租户 A 创建预约应成功");
|
||||
|
||||
// 租户 B 列表查询应看不到租户 A 的预约
|
||||
let result_b = appointment_service::list_appointments(
|
||||
&state,
|
||||
tenant_b,
|
||||
1,
|
||||
10,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("租户 B 列表查询应成功");
|
||||
assert_eq!(result_b.total, 0, "租户 B 不应看到租户 A 的预约");
|
||||
assert!(result_b.data.is_empty());
|
||||
|
||||
// 租户 B 通过 ID 查询租户 A 的预约应返回错误
|
||||
let lookup_result =
|
||||
appointment_service::get_appointment(&state, tenant_b, appointment_a.id).await;
|
||||
assert!(
|
||||
lookup_result.is_err(),
|
||||
"跨租户查询预约应返回错误"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user