diff --git a/crates/erp-server/tests/integration.rs b/crates/erp-server/tests/integration.rs index bdecb30..0220122 100644 --- a/crates/erp-server/tests/integration.rs +++ b/crates/erp-server/tests/integration.rs @@ -22,3 +22,7 @@ mod health_dialysis_tests; mod health_alert_tests; #[path = "integration/health_device_reading_tests.rs"] mod health_device_reading_tests; +#[path = "integration/health_follow_up_tests.rs"] +mod health_follow_up_tests; +#[path = "integration/health_consultation_tests.rs"] +mod health_consultation_tests; diff --git a/crates/erp-server/tests/integration/health_consultation_tests.rs b/crates/erp-server/tests/integration/health_consultation_tests.rs new file mode 100644 index 0000000..6157d32 --- /dev/null +++ b/crates/erp-server/tests/integration/health_consultation_tests.rs @@ -0,0 +1,211 @@ +//! erp-health 咨询管理集成测试 +//! +//! 验证咨询会话 CRUD、消息收发、会话关闭、未读计数、租户隔离。 + +use erp_health::dto::consultation_dto::*; +use erp_health::service::consultation_service; + +use super::test_fixture::TestApp; + +/// 创建测试用会话(无医护) +async fn seed_session(app: &TestApp, patient_id: uuid::Uuid) -> SessionResp { + consultation_service::create_session( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + CreateSessionReq { patient_id, doctor_id: None, consultation_type: None }, + ) + .await + .expect("创建会话应成功") +} + +// --------------------------------------------------------------------------- +// 测试 1: 创建会话(带医护) +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consultation_session_create() { + let app = TestApp::new().await; + let patient_id = app.create_patient("咨询患者").await; + let doctor_id = app.create_doctor("咨询医生").await; + + let session = consultation_service::create_session( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + CreateSessionReq { + patient_id, + doctor_id: Some(doctor_id), + consultation_type: Some("doctor".to_string()), + }, + ) + .await + .expect("创建会话应成功"); + + assert_eq!(session.patient_id, patient_id); + assert_eq!(session.doctor_id, Some(doctor_id)); + assert_eq!(session.consultation_type, "doctor"); + assert_eq!(session.status, "waiting"); +} + +// --------------------------------------------------------------------------- +// 测试 2: 查询会话 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consultation_session_get() { + let app = TestApp::new().await; + let patient_id = app.create_patient("查询患者").await; + + let session = seed_session(&app, patient_id).await; + + let fetched = consultation_service::get_session( + app.health_state(), app.tenant_id(), session.id, + ) + .await + .expect("查询应成功"); + assert_eq!(fetched.id, session.id); + assert_eq!(fetched.status, "waiting"); +} + +// --------------------------------------------------------------------------- +// 测试 3: 列表按患者过滤 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consultation_session_list_by_patient() { + let app = TestApp::new().await; + let patient_a = app.create_patient("列表患者A").await; + let patient_b = app.create_patient("列表患者B").await; + + seed_session(&app, patient_a).await; + seed_session(&app, patient_a).await; + seed_session(&app, patient_b).await; + + let list_a = consultation_service::list_sessions( + app.health_state(), app.tenant_id(), 1, 20, None, Some(patient_a), None, + ) + .await + .unwrap(); + assert_eq!(list_a.total, 2); + + let list_b = consultation_service::list_sessions( + app.health_state(), app.tenant_id(), 1, 20, None, Some(patient_b), None, + ) + .await + .unwrap(); + assert_eq!(list_b.total, 1); +} + +// --------------------------------------------------------------------------- +// 测试 4: 发送消息 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consultation_message_send() { + let app = TestApp::new().await; + let patient_id = app.create_patient("消息患者").await; + + let session = seed_session(&app, patient_id).await; + + let msg = consultation_service::create_message( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + CreateMessageReq { + session_id: session.id, + sender_id: app.operator_id(), + sender_role: "doctor".to_string(), + content_type: Some("text".to_string()), + content: "您好,有什么可以帮您?".to_string(), + }, + ) + .await + .expect("发送消息应成功"); + + assert_eq!(msg.session_id, session.id); + assert_eq!(msg.content, "您好,有什么可以帮您?"); + assert_eq!(msg.sender_role, "doctor"); + assert!(!msg.is_read); +} + +// --------------------------------------------------------------------------- +// 测试 5: 查询消息列表 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consultation_message_list() { + let app = TestApp::new().await; + let patient_id = app.create_patient("消息列表患者").await; + + let session = seed_session(&app, patient_id).await; + + for i in 0..3 { + consultation_service::create_message( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + CreateMessageReq { + session_id: session.id, + sender_id: app.operator_id(), + sender_role: "doctor".to_string(), + content_type: None, + content: format!("消息{}", i + 1), + }, + ) + .await + .unwrap(); + } + + let messages = consultation_service::list_messages( + app.health_state(), app.tenant_id(), session.id, 1, 20, None, + ) + .await + .expect("查询消息应成功"); + + assert_eq!(messages.total, 3); +} + +// --------------------------------------------------------------------------- +// 测试 6: 关闭会话 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consultation_session_close() { + let app = TestApp::new().await; + let patient_id = app.create_patient("关闭患者").await; + + let session = seed_session(&app, patient_id).await; + assert_eq!(session.status, "waiting"); + + let closed = consultation_service::close_session( + app.health_state(), app.tenant_id(), session.id, Some(app.operator_id()), session.version, + ) + .await + .expect("关闭应成功"); + assert_eq!(closed.status, "closed"); +} + +// --------------------------------------------------------------------------- +// 测试 7: 租户隔离 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consultation_session_tenant_isolation() { + let app = TestApp::new().await; + let patient_id = app.create_patient("隔离患者").await; + + let session = seed_session(&app, patient_id).await; + + let other_tenant = uuid::Uuid::new_v4(); + let result = consultation_service::get_session( + app.health_state(), other_tenant, session.id, + ) + .await; + assert!(result.is_err(), "不同租户不应看到此会话"); +} + +// --------------------------------------------------------------------------- +// 测试 8: 无效患者创建会话返回错误 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consultation_session_invalid_patient() { + let app = TestApp::new().await; + let fake_patient = uuid::Uuid::new_v4(); + + let result = consultation_service::create_session( + app.health_state(), app.tenant_id(), None, + CreateSessionReq { + patient_id: fake_patient, + doctor_id: None, + consultation_type: None, + }, + ) + .await; + assert!(result.is_err(), "无效患者应返回错误"); +} diff --git a/crates/erp-server/tests/integration/health_follow_up_tests.rs b/crates/erp-server/tests/integration/health_follow_up_tests.rs new file mode 100644 index 0000000..1ef9253 --- /dev/null +++ b/crates/erp-server/tests/integration/health_follow_up_tests.rs @@ -0,0 +1,297 @@ +//! erp-health 随访管理集成测试 +//! +//! 验证随访任务 CRUD、状态流转、批量操作、记录创建、租户隔离、乐观锁。 + +use erp_health::dto::follow_up_dto::*; +use erp_health::service::follow_up_service; + +use super::test_fixture::TestApp; + +fn default_create_task(patient_id: uuid::Uuid) -> CreateFollowUpTaskReq { + CreateFollowUpTaskReq { + patient_id, + assigned_to: None, + follow_up_type: "phone".to_string(), + planned_date: chrono::NaiveDate::from_ymd_opt(2026, 5, 15).unwrap(), + content_template: Some("请询问血压情况".to_string()), + related_appointment_id: None, + } +} + +// --------------------------------------------------------------------------- +// 测试 1: 创建 + 查询随访任务 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_follow_up_task_create_and_get() { + let app = TestApp::new().await; + let patient_id = app.create_patient("随访患者").await; + + let task = follow_up_service::create_task( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_task(patient_id), + ) + .await + .expect("创建任务应成功"); + + assert_eq!(task.patient_id, patient_id); + assert_eq!(task.follow_up_type, "phone"); + assert_eq!(task.status, "pending"); + assert_eq!(task.version, 1); + + let fetched = follow_up_service::get_task(app.health_state(), app.tenant_id(), task.id) + .await + .expect("查询应成功"); + assert_eq!(fetched.id, task.id); +} + +// --------------------------------------------------------------------------- +// 测试 2: 列表按患者过滤 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_follow_up_task_list_by_patient() { + let app = TestApp::new().await; + let patient_a = app.create_patient("列表A").await; + let patient_b = app.create_patient("列表B").await; + + follow_up_service::create_task( + app.health_state(), app.tenant_id(), None, + default_create_task(patient_a), + ) + .await + .unwrap(); + follow_up_service::create_task( + app.health_state(), app.tenant_id(), None, + default_create_task(patient_a), + ) + .await + .unwrap(); + follow_up_service::create_task( + app.health_state(), app.tenant_id(), None, + default_create_task(patient_b), + ) + .await + .unwrap(); + + let list_a = follow_up_service::list_tasks( + app.health_state(), app.tenant_id(), 1, 20, Some(patient_a), None, None, + ) + .await + .unwrap(); + assert_eq!(list_a.total, 2); + + let list_b = follow_up_service::list_tasks( + app.health_state(), app.tenant_id(), 1, 20, Some(patient_b), None, None, + ) + .await + .unwrap(); + assert_eq!(list_b.total, 1); +} + +// --------------------------------------------------------------------------- +// 测试 3: 状态流转 pending → in_progress → completed +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_follow_up_task_status_flow() { + let app = TestApp::new().await; + let patient_id = app.create_patient("流转患者").await; + + let task = follow_up_service::create_task( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_task(patient_id), + ) + .await + .unwrap(); + assert_eq!(task.status, "pending"); + + // pending → in_progress + let started = follow_up_service::update_task( + app.health_state(), app.tenant_id(), task.id, Some(app.operator_id()), + UpdateFollowUpTaskReq { + status: Some("in_progress".to_string()), + assigned_to: None, follow_up_type: None, planned_date: None, + content_template: None, + }, + task.version, + ) + .await + .expect("开始应成功"); + assert_eq!(started.status, "in_progress"); + + // in_progress → completed + let completed = follow_up_service::update_task( + app.health_state(), app.tenant_id(), task.id, Some(app.operator_id()), + UpdateFollowUpTaskReq { + status: Some("completed".to_string()), + assigned_to: None, follow_up_type: None, planned_date: None, + content_template: None, + }, + started.version, + ) + .await + .expect("完成应成功"); + assert_eq!(completed.status, "completed"); +} + +// --------------------------------------------------------------------------- +// 测试 4: 乐观锁冲突 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_follow_up_task_version_conflict() { + let app = TestApp::new().await; + let patient_id = app.create_patient("乐观锁患者").await; + + let task = follow_up_service::create_task( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_task(patient_id), + ) + .await + .unwrap(); + + // 正确版本更新 + follow_up_service::update_task( + app.health_state(), app.tenant_id(), task.id, Some(app.operator_id()), + UpdateFollowUpTaskReq { + status: Some("in_progress".to_string()), + assigned_to: None, follow_up_type: None, planned_date: None, + content_template: None, + }, + task.version, + ) + .await + .expect("更新应成功"); + + // 旧版本更新应失败 + let result = follow_up_service::update_task( + app.health_state(), app.tenant_id(), task.id, Some(app.operator_id()), + UpdateFollowUpTaskReq { + status: Some("cancelled".to_string()), + assigned_to: None, follow_up_type: None, planned_date: None, + content_template: None, + }, + task.version, + ) + .await; + assert!(result.is_err(), "旧版本应失败"); +} + +// --------------------------------------------------------------------------- +// 测试 5: 软删除后不可见 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_follow_up_task_soft_delete() { + let app = TestApp::new().await; + let patient_id = app.create_patient("软删除患者").await; + + let task = follow_up_service::create_task( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_task(patient_id), + ) + .await + .unwrap(); + + follow_up_service::delete_task( + app.health_state(), app.tenant_id(), task.id, Some(app.operator_id()), task.version, + ) + .await + .expect("删除应成功"); + + let result = follow_up_service::get_task(app.health_state(), app.tenant_id(), task.id).await; + assert!(result.is_err(), "软删除后应不可见"); +} + +// --------------------------------------------------------------------------- +// 测试 6: 租户隔离 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_follow_up_task_tenant_isolation() { + let app = TestApp::new().await; + let patient_id = app.create_patient("隔离患者").await; + + let task = follow_up_service::create_task( + app.health_state(), app.tenant_id(), None, + default_create_task(patient_id), + ) + .await + .unwrap(); + + let other_tenant = uuid::Uuid::new_v4(); + let result = follow_up_service::get_task(app.health_state(), other_tenant, task.id).await; + assert!(result.is_err(), "不同租户不应看到此任务"); +} + +// --------------------------------------------------------------------------- +// 测试 7: 批量创建随访任务 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_follow_up_batch_create() { + let app = TestApp::new().await; + let patient_a = app.create_patient("批量A").await; + let patient_b = app.create_patient("批量B").await; + + let result = follow_up_service::batch_create_tasks( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + BatchCreateTasksReq { + patient_ids: vec![patient_a, patient_b], + assigned_to: None, + follow_up_type: "phone".to_string(), + planned_date: chrono::NaiveDate::from_ymd_opt(2026, 5, 20).unwrap(), + content_template: None, + }, + ) + .await + .expect("批量创建应成功"); + + assert_eq!(result.succeeded, 2); + assert_eq!(result.failed, 0); +} + +// --------------------------------------------------------------------------- +// 测试 8: 创建随访记录 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_follow_up_record_create() { + let app = TestApp::new().await; + let patient_id = app.create_patient("记录患者").await; + + let task = follow_up_service::create_task( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_task(patient_id), + ) + .await + .unwrap(); + + let record = follow_up_service::create_record( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + CreateFollowUpRecordReq { + task_id: task.id, + executed_by: Some(app.operator_id()), + executed_date: chrono::NaiveDate::from_ymd_opt(2026, 5, 16).unwrap(), + result: "已跟进".to_string(), + patient_condition: Some("血压正常".to_string()), + medical_advice: Some("继续服药".to_string()), + next_follow_up_date: Some(chrono::NaiveDate::from_ymd_opt(2026, 6, 16).unwrap()), + }, + ) + .await + .expect("创建记录应成功"); + + assert_eq!(record.task_id, task.id); + assert_eq!(record.result, "已跟进"); + assert_eq!(record.medical_advice, Some("继续服药".to_string())); +} + +// --------------------------------------------------------------------------- +// 测试 9: 无效患者返回错误 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_follow_up_task_invalid_patient() { + let app = TestApp::new().await; + let fake_patient = uuid::Uuid::new_v4(); + + let result = follow_up_service::create_task( + app.health_state(), app.tenant_id(), None, + default_create_task(fake_patient), + ) + .await; + assert!(result.is_err(), "无效患者应返回错误"); +}