fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
This commit is contained in:
iven
2026-05-07 23:43:14 +08:00
parent 786f57c151
commit 6d5a711d2c
323 changed files with 15662 additions and 6603 deletions

View File

@@ -1,48 +1,48 @@
#[path = "integration/test_db.rs"]
mod test_db;
#[path = "integration/test_fixture.rs"]
mod test_fixture;
#[path = "integration/ai_prompt_tests.rs"]
mod ai_prompt_tests;
#[path = "integration/auth_tests.rs"]
mod auth_tests;
#[path = "integration/plugin_tests.rs"]
mod plugin_tests;
#[path = "integration/workflow_tests.rs"]
mod workflow_tests;
#[path = "integration/health_patient_tests.rs"]
mod health_patient_tests;
#[path = "integration/health_alert_tests.rs"]
mod health_alert_tests;
#[path = "integration/health_appointment_tests.rs"]
mod health_appointment_tests;
#[path = "integration/health_article_tests.rs"]
mod health_article_tests;
#[path = "integration/health_consent_tests.rs"]
mod health_consent_tests;
#[path = "integration/health_consultation_tests.rs"]
mod health_consultation_tests;
#[path = "integration/health_daily_monitoring_tests.rs"]
mod health_daily_monitoring_tests;
#[path = "integration/health_data_tests.rs"]
mod health_data_tests;
#[path = "integration/health_device_reading_tests.rs"]
mod health_device_reading_tests;
#[path = "integration/health_diagnosis_tests.rs"]
mod health_diagnosis_tests;
#[path = "integration/health_dialysis_prescription_tests.rs"]
mod health_dialysis_prescription_tests;
#[path = "integration/health_dialysis_tests.rs"]
mod health_dialysis_tests;
#[path = "integration/health_doctor_tests.rs"]
mod health_doctor_tests;
#[path = "integration/health_follow_up_template_tests.rs"]
mod health_follow_up_template_tests;
#[path = "integration/health_follow_up_tests.rs"]
mod health_follow_up_tests;
#[path = "integration/health_medication_tests.rs"]
mod health_medication_tests;
#[path = "integration/health_patient_tests.rs"]
mod health_patient_tests;
#[path = "integration/health_pii_encryption_tests.rs"]
mod health_pii_encryption_tests;
#[path = "integration/health_points_tests.rs"]
mod health_points_tests;
#[path = "integration/health_dialysis_tests.rs"]
mod health_dialysis_tests;
#[path = "integration/health_alert_tests.rs"]
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;
#[path = "integration/health_data_tests.rs"]
mod health_data_tests;
#[path = "integration/health_article_tests.rs"]
mod health_article_tests;
#[path = "integration/health_doctor_tests.rs"]
mod health_doctor_tests;
#[path = "integration/health_diagnosis_tests.rs"]
mod health_diagnosis_tests;
#[path = "integration/health_consent_tests.rs"]
mod health_consent_tests;
#[path = "integration/health_medication_tests.rs"]
mod health_medication_tests;
#[path = "integration/health_dialysis_prescription_tests.rs"]
mod health_dialysis_prescription_tests;
#[path = "integration/health_follow_up_template_tests.rs"]
mod health_follow_up_template_tests;
#[path = "integration/health_daily_monitoring_tests.rs"]
mod health_daily_monitoring_tests;
#[path = "integration/ai_prompt_tests.rs"]
mod ai_prompt_tests;
#[path = "integration/plugin_tests.rs"]
mod plugin_tests;
#[path = "integration/test_db.rs"]
mod test_db;
#[path = "integration/test_fixture.rs"]
mod test_fixture;
#[path = "integration/workflow_tests.rs"]
mod workflow_tests;

View File

@@ -1,9 +1,9 @@
use erp_ai::service::prompt::PromptService;
use erp_ai::service::usage::UsageService;
use erp_ai::service::analysis::AnalysisService;
use erp_ai::provider::AiProvider;
use erp_ai::dto::GenerateRequest;
use erp_ai::error::{AiError, AiResult};
use erp_ai::provider::AiProvider;
use erp_ai::service::analysis::AnalysisService;
use erp_ai::service::prompt::PromptService;
use erp_ai::service::usage::UsageService;
use erp_core::types::Pagination;
use sea_orm::ActiveModelTrait;
use sha2::Digest;
@@ -97,7 +97,14 @@ async fn prompt_list_with_category_filter() {
}
let (items, total) = svc
.list_prompts(tenant_id, Some("analysis".into()), &Pagination { page: Some(1), page_size: Some(10) })
.list_prompts(
tenant_id,
Some("analysis".into()),
&Pagination {
page: Some(1),
page_size: Some(10),
},
)
.await
.expect("查询应成功");
@@ -113,25 +120,49 @@ async fn prompt_activate_switches_version() {
let user_id = uuid::Uuid::new_v4();
let v1 = svc
.create_prompt(tenant_id, user_id, "my_prompt".into(), "sys_v1".into(), "usr".into(), serde_json::json!({}), "cat".into())
.create_prompt(
tenant_id,
user_id,
"my_prompt".into(),
"sys_v1".into(),
"usr".into(),
serde_json::json!({}),
"cat".into(),
)
.await
.expect("v1");
let v2 = svc
.update_prompt(v1.id, tenant_id, user_id, Some("sys_v2".into()), None, None, None)
.update_prompt(
v1.id,
tenant_id,
user_id,
Some("sys_v2".into()),
None,
None,
None,
)
.await
.expect("v2");
assert_eq!(v2.version, 2);
// v1 仍然激活update 继承 is_active
let active_before = svc.get_active_prompt(tenant_id, "my_prompt").await.expect("active");
let active_before = svc
.get_active_prompt(tenant_id, "my_prompt")
.await
.expect("active");
assert_eq!(active_before.system_prompt, "sys_v1");
// 激活 v2
svc.activate_prompt(v2.id, tenant_id).await.expect("activate");
svc.activate_prompt(v2.id, tenant_id)
.await
.expect("activate");
let active_after = svc.get_active_prompt(tenant_id, "my_prompt").await.expect("active");
let active_after = svc
.get_active_prompt(tenant_id, "my_prompt")
.await
.expect("active");
assert_eq!(active_after.id, v2.id);
assert_eq!(active_after.system_prompt, "sys_v2");
@@ -148,21 +179,44 @@ async fn prompt_rollback_equals_activate() {
let user_id = uuid::Uuid::new_v4();
let v1 = svc
.create_prompt(tenant_id, user_id, "rb_test".into(), "sys_v1".into(), "usr".into(), serde_json::json!({}), "cat".into())
.create_prompt(
tenant_id,
user_id,
"rb_test".into(),
"sys_v1".into(),
"usr".into(),
serde_json::json!({}),
"cat".into(),
)
.await
.expect("v1");
let v2 = svc
.update_prompt(v1.id, tenant_id, user_id, Some("sys_v2".into()), None, None, None)
.update_prompt(
v1.id,
tenant_id,
user_id,
Some("sys_v2".into()),
None,
None,
None,
)
.await
.expect("v2");
svc.activate_prompt(v2.id, tenant_id).await.expect("activate v2");
svc.activate_prompt(v2.id, tenant_id)
.await
.expect("activate v2");
// 回滚到 v1
svc.rollback_prompt(v1.id, tenant_id).await.expect("rollback");
svc.rollback_prompt(v1.id, tenant_id)
.await
.expect("rollback");
let active = svc.get_active_prompt(tenant_id, "rb_test").await.expect("active");
let active = svc
.get_active_prompt(tenant_id, "rb_test")
.await
.expect("active");
assert_eq!(active.id, v1.id);
}
@@ -174,9 +228,17 @@ async fn prompt_cross_tenant_isolation() {
let tenant_b = uuid::Uuid::new_v4();
let user_id = uuid::Uuid::new_v4();
svc.create_prompt(tenant_a, user_id, "shared_name".into(), "sys".into(), "usr".into(), serde_json::json!({}), "cat".into())
.await
.expect("create");
svc.create_prompt(
tenant_a,
user_id,
"shared_name".into(),
"sys".into(),
"usr".into(),
serde_json::json!({}),
"cat".into(),
)
.await
.expect("create");
let result = svc.get_active_prompt(tenant_b, "shared_name").await;
assert!(result.is_err());
@@ -224,10 +286,16 @@ async fn usage_by_type_aggregation() {
let by_type = svc.get_by_type(tenant_id).await.expect("by_type");
assert_eq!(by_type.len(), 2);
let lab = by_type.iter().find(|t| t.analysis_type == "lab_report").expect("lab");
let lab = by_type
.iter()
.find(|t| t.analysis_type == "lab_report")
.expect("lab");
assert_eq!(lab.count, 2);
let trends = by_type.iter().find(|t| t.analysis_type == "trends").expect("trends");
let trends = by_type
.iter()
.find(|t| t.analysis_type == "trends")
.expect("trends");
assert_eq!(trends.count, 1);
}
@@ -238,7 +306,17 @@ async fn usage_log_creates_record() {
let tenant_id = uuid::Uuid::new_v4();
let record = svc
.log_usage(tenant_id, "claude", "claude-3", "lab_report", 100, 200, 3000, 50, false)
.log_usage(
tenant_id,
"claude",
"claude-3",
"lab_report",
100,
200,
3000,
50,
false,
)
.await
.expect("log");
@@ -275,11 +353,23 @@ async fn analysis_complete_updates_status() {
// 通过内部方法创建 streaming 记录(直接插入 DB
let analysis_id = uuid::Uuid::now_v7();
insert_streaming_analysis(&test_db, analysis_id, tenant_id, user_id, patient_id, "lab_report").await;
insert_streaming_analysis(
&test_db,
analysis_id,
tenant_id,
user_id,
patient_id,
"lab_report",
)
.await;
svc.complete_analysis(analysis_id, "分析结果文本".into(), serde_json::json!({"tokens": 100}))
.await
.expect("complete");
svc.complete_analysis(
analysis_id,
"分析结果文本".into(),
serde_json::json!({"tokens": 100}),
)
.await
.expect("complete");
let record = svc.get_analysis(analysis_id, tenant_id).await.expect("get");
assert_eq!(record.status, "completed");
@@ -295,7 +385,15 @@ async fn analysis_fail_updates_status() {
let patient_id = uuid::Uuid::new_v4();
let analysis_id = uuid::Uuid::now_v7();
insert_streaming_analysis(&test_db, analysis_id, tenant_id, user_id, patient_id, "trends").await;
insert_streaming_analysis(
&test_db,
analysis_id,
tenant_id,
user_id,
patient_id,
"trends",
)
.await;
svc.fail_analysis(analysis_id, "API 超时".into())
.await
@@ -319,14 +417,30 @@ async fn analysis_find_cached() {
// 插入 completed 记录
let analysis_id = uuid::Uuid::now_v7();
insert_completed_analysis_with_hash(&test_db, analysis_id, tenant_id, user_id, patient_id, "lab_report", &hash, 1).await;
insert_completed_analysis_with_hash(
&test_db,
analysis_id,
tenant_id,
user_id,
patient_id,
"lab_report",
&hash,
1,
)
.await;
let cached = svc.find_cached(tenant_id, &hash, 1).await.expect("find_cached");
let cached = svc
.find_cached(tenant_id, &hash, 1)
.await
.expect("find_cached");
assert!(cached.is_some());
assert_eq!(cached.unwrap().id, analysis_id);
// 不同 hash 不命中
let miss = svc.find_cached(tenant_id, "wrong_hash", 1).await.expect("find_cached");
let miss = svc
.find_cached(tenant_id, "wrong_hash", 1)
.await
.expect("find_cached");
assert!(miss.is_none());
}
@@ -339,27 +453,93 @@ async fn analysis_list_with_filters() {
let patient_a = uuid::Uuid::new_v4();
let patient_b = uuid::Uuid::new_v4();
insert_completed_analysis_with_hash(&test_db, uuid::Uuid::now_v7(), tenant_id, user_id, patient_a, "lab_report", "h1", 1).await;
insert_completed_analysis_with_hash(&test_db, uuid::Uuid::now_v7(), tenant_id, user_id, patient_a, "trends", "h2", 1).await;
insert_completed_analysis_with_hash(&test_db, uuid::Uuid::now_v7(), tenant_id, user_id, patient_b, "lab_report", "h3", 1).await;
insert_completed_analysis_with_hash(
&test_db,
uuid::Uuid::now_v7(),
tenant_id,
user_id,
patient_a,
"lab_report",
"h1",
1,
)
.await;
insert_completed_analysis_with_hash(
&test_db,
uuid::Uuid::now_v7(),
tenant_id,
user_id,
patient_a,
"trends",
"h2",
1,
)
.await;
insert_completed_analysis_with_hash(
&test_db,
uuid::Uuid::now_v7(),
tenant_id,
user_id,
patient_b,
"lab_report",
"h3",
1,
)
.await;
// 按 patient 筛选
let (items, total) = svc.list_analysis(tenant_id, Some(patient_a), None, &Pagination { page: Some(1), page_size: Some(10) }).await.expect("list");
let (items, total) = svc
.list_analysis(
tenant_id,
Some(patient_a),
None,
&Pagination {
page: Some(1),
page_size: Some(10),
},
)
.await
.expect("list");
assert_eq!(total, 2);
// 按 type 筛选
let (items, total) = svc.list_analysis(tenant_id, None, Some("lab_report".into()), &Pagination { page: Some(1), page_size: Some(10) }).await.expect("list");
let (items, total) = svc
.list_analysis(
tenant_id,
None,
Some("lab_report".into()),
&Pagination {
page: Some(1),
page_size: Some(10),
},
)
.await
.expect("list");
assert_eq!(total, 2);
// 跨租户
let (items, total) = svc.list_analysis(uuid::Uuid::new_v4(), None, None, &Pagination { page: Some(1), page_size: Some(10) }).await.expect("list");
let (items, total) = svc
.list_analysis(
uuid::Uuid::new_v4(),
None,
None,
&Pagination {
page: Some(1),
page_size: Some(10),
},
)
.await
.expect("list");
assert_eq!(total, 0);
assert!(items.is_empty());
}
// ---- 辅助函数 ----
async fn ai_prompt_find_by_id(test_db: &TestDb, id: uuid::Uuid) -> erp_ai::entity::ai_prompt::Model {
async fn ai_prompt_find_by_id(
test_db: &TestDb,
id: uuid::Uuid,
) -> erp_ai::entity::ai_prompt::Model {
use sea_orm::EntityTrait;
erp_ai::entity::ai_prompt::Entity::find_by_id(id)
.one(test_db.db())

View File

@@ -5,15 +5,21 @@
use erp_health::dto::alert_dto::*;
use erp_health::entity::{alert_rules, vital_signs_hourly};
use erp_health::service::{alert_engine, alert_rule_service, alert_service};
use sea_orm::ActiveValue::Set;
use sea_orm::ActiveModelTrait;
use sea_orm::ActiveValue::Set;
use super::test_fixture::TestApp;
/// 创建告警规则(单阈值)
async fn seed_threshold_rule(app: &TestApp, device_type: &str, threshold: f64) -> alert_rules::Model {
async fn seed_threshold_rule(
app: &TestApp,
device_type: &str,
threshold: f64,
) -> alert_rules::Model {
alert_rule_service::create_rule(
app.health_state(), app.tenant_id(), app.operator_id(),
app.health_state(),
app.tenant_id(),
app.operator_id(),
CreateAlertRuleRequest {
name: "高收缩压".to_string(),
description: Some("收缩压超过阈值".to_string()),
@@ -31,9 +37,7 @@ async fn seed_threshold_rule(app: &TestApp, device_type: &str, threshold: f64) -
}
/// 插入一条 hourly 汇总记录
async fn seed_hourly(
app: &TestApp, patient_id: uuid::Uuid, device_type: &str, avg_val: f64,
) {
async fn seed_hourly(app: &TestApp, patient_id: uuid::Uuid, device_type: &str, avg_val: f64) {
let model = vital_signs_hourly::ActiveModel {
id: Set(uuid::Uuid::now_v7()),
tenant_id: Set(app.tenant_id()),
@@ -64,11 +68,10 @@ async fn test_alert_rule_create_and_list() {
assert_eq!(rule.condition_type, "single_threshold");
assert!(rule.is_active);
let (rules, total) = alert_rule_service::list_rules(
app.health_state(), app.tenant_id(), None, 1, 20,
)
.await
.expect("列表应成功");
let (rules, total) =
alert_rule_service::list_rules(app.health_state(), app.tenant_id(), None, 1, 20)
.await
.expect("列表应成功");
assert_eq!(total, 1);
assert_eq!(rules[0].id, rule.id);
}
@@ -84,7 +87,10 @@ async fn test_alert_rule_deactivate() {
assert!(rule.is_active);
let deactivated = alert_rule_service::deactivate_rule(
app.health_state(), app.tenant_id(), rule.id, rule.version,
app.health_state(),
app.tenant_id(),
rule.id,
rule.version,
)
.await
.expect("停用应成功");
@@ -103,7 +109,10 @@ async fn test_alert_engine_single_threshold_trigger() {
seed_hourly(&app, patient_id, "heart_rate", 155.0).await;
let triggered = alert_engine::evaluate_rules(
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.expect("评估应成功");
@@ -125,7 +134,10 @@ async fn test_alert_engine_single_threshold_no_trigger() {
seed_hourly(&app, patient_id, "heart_rate", 120.0).await;
let triggered = alert_engine::evaluate_rules(
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.expect("评估应成功");
@@ -145,7 +157,10 @@ async fn test_alert_engine_cooldown_suppresses() {
seed_hourly(&app, patient_id, "heart_rate", 160.0).await;
let first = alert_engine::evaluate_rules(
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.expect("首次评估应成功");
@@ -153,7 +168,10 @@ async fn test_alert_engine_cooldown_suppresses() {
// 再次评估cooldown 内不应重复
let second = alert_engine::evaluate_rules(
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.expect("二次评估应成功");
@@ -172,7 +190,10 @@ async fn test_alert_status_flow() {
seed_hourly(&app, patient_id, "heart_rate", 155.0).await;
let triggered = alert_engine::evaluate_rules(
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.unwrap();
@@ -181,7 +202,11 @@ async fn test_alert_status_flow() {
// pending → acknowledged
let acked = alert_service::acknowledge_alert(
app.health_state(), app.tenant_id(), alert.id, app.operator_id(), alert.version,
app.health_state(),
app.tenant_id(),
alert.id,
app.operator_id(),
alert.version,
)
.await
.expect("确认应成功");
@@ -189,11 +214,10 @@ async fn test_alert_status_flow() {
assert!(acked.acknowledged_by.is_some());
// acknowledged → resolved
let resolved = alert_service::resolve_alert(
app.health_state(), app.tenant_id(), acked.id, acked.version,
)
.await
.expect("解决应成功");
let resolved =
alert_service::resolve_alert(app.health_state(), app.tenant_id(), acked.id, acked.version)
.await
.expect("解决应成功");
assert_eq!(resolved.status, "resolved");
assert!(resolved.resolved_at.is_some());
}
@@ -210,14 +234,21 @@ async fn test_alert_dismiss_from_pending() {
seed_hourly(&app, patient_id, "heart_rate", 155.0).await;
let triggered = alert_engine::evaluate_rules(
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.unwrap();
let alert = &triggered[0];
let dismissed = alert_service::dismiss_alert(
app.health_state(), app.tenant_id(), alert.id, app.operator_id(), alert.version,
app.health_state(),
app.tenant_id(),
alert.id,
app.operator_id(),
alert.version,
)
.await
.expect("忽略应成功");
@@ -237,20 +268,22 @@ async fn test_alert_list_filter_and_tenant_isolation() {
seed_hourly(&app, patient_a, "heart_rate", 155.0).await;
seed_hourly(&app, patient_b, "heart_rate", 160.0).await;
alert_engine::evaluate_rules(
app.health_state(), app.tenant_id(), patient_a, "heart_rate",
)
.await
.unwrap();
alert_engine::evaluate_rules(
app.health_state(), app.tenant_id(), patient_b, "heart_rate",
)
.await
.unwrap();
alert_engine::evaluate_rules(app.health_state(), app.tenant_id(), patient_a, "heart_rate")
.await
.unwrap();
alert_engine::evaluate_rules(app.health_state(), app.tenant_id(), patient_b, "heart_rate")
.await
.unwrap();
// 按患者 A 过滤
let (alerts_a, total_a) = alert_service::list_alerts(
app.health_state(), app.tenant_id(), Some(patient_a), None, None, 1, 20,
app.health_state(),
app.tenant_id(),
Some(patient_a),
None,
None,
1,
20,
)
.await
.unwrap();
@@ -260,7 +293,13 @@ async fn test_alert_list_filter_and_tenant_isolation() {
// 租户隔离
let other_tenant = uuid::Uuid::new_v4();
let (_alerts_other, total_other) = alert_service::list_alerts(
app.health_state(), other_tenant, Some(patient_a), None, None, 1, 20,
app.health_state(),
other_tenant,
Some(patient_a),
None,
None,
1,
20,
)
.await
.unwrap();

View File

@@ -4,15 +4,13 @@
//! 使用 TestDb 创建隔离 PostgreSQL 数据库,直接调用 service 层函数。
//! 预约创建依赖患者 + 医护档案 + 排班三条前置数据。
use erp_core::crypto::PiiCrypto;
use erp_core::events::EventBus;
use erp_health::dto::appointment_dto::{CreateAppointmentReq, UpdateAppointmentStatusReq};
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::service::{appointment_service, doctor_service, patient_service};
use erp_health::state::HealthState;
use erp_core::crypto::PiiCrypto;
use super::test_db::TestDb;
@@ -26,11 +24,7 @@ fn make_state(db: &sea_orm::DatabaseConnection) -> HealthState {
}
/// 创建患者并返回其 ID
async fn seed_patient(
state: &HealthState,
tenant_id: uuid::Uuid,
name: &str,
) -> uuid::Uuid {
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()),
@@ -51,11 +45,7 @@ async fn seed_patient(
}
/// 创建医护档案并返回其 ID
async fn seed_doctor(
state: &HealthState,
tenant_id: uuid::Uuid,
name: &str,
) -> uuid::Uuid {
async fn seed_doctor(state: &HealthState, tenant_id: uuid::Uuid, name: &str) -> uuid::Uuid {
let req = CreateDoctorReq {
user_id: None,
name: name.to_string(),
@@ -129,10 +119,7 @@ async fn test_create_appointment() {
assert_eq!(appointment.appointment_type, "outpatient");
assert_eq!(appointment.status, "pending");
assert_eq!(appointment.version, 1);
assert_eq!(
appointment.notes,
Some("首次就诊".to_string())
);
assert_eq!(appointment.notes, Some("首次就诊".to_string()));
// 通过 get_appointment 验证存储正确
let found = appointment_service::get_appointment(&state, tenant_id, appointment.id)
@@ -174,18 +161,10 @@ async fn test_list_appointments() {
.expect("创建预约应成功");
}
let result = appointment_service::list_appointments(
&state,
tenant_id,
1,
10,
None,
None,
None,
None,
)
.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 条");
@@ -217,34 +196,22 @@ async fn test_appointment_tenant_isolation() {
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 创建预约应成功");
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 列表查询应成功");
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(),
"跨租户查询预约应返回错误"
);
assert!(lookup_result.is_err(), "跨租户查询预约应返回错误");
}
// ---------------------------------------------------------------------------
@@ -264,7 +231,9 @@ async fn test_appointment_status_flow() {
seed_schedule(&state, tenant_id, doctor_id, date).await;
let appt = appointment_service::create_appointment(
&state, tenant_id, Some(operator_id),
&state,
tenant_id,
Some(operator_id),
CreateAppointmentReq {
patient_id,
doctor_id: Some(doctor_id),
@@ -281,8 +250,14 @@ async fn test_appointment_status_flow() {
// pending → confirmed
let confirmed = appointment_service::update_appointment_status(
&state, tenant_id, appt.id, Some(operator_id),
UpdateAppointmentStatusReq { status: "confirmed".to_string(), cancel_reason: None },
&state,
tenant_id,
appt.id,
Some(operator_id),
UpdateAppointmentStatusReq {
status: "confirmed".to_string(),
cancel_reason: None,
},
appt.version,
)
.await
@@ -291,8 +266,14 @@ async fn test_appointment_status_flow() {
// confirmed → completed
let completed = appointment_service::update_appointment_status(
&state, tenant_id, appt.id, Some(operator_id),
UpdateAppointmentStatusReq { status: "completed".to_string(), cancel_reason: None },
&state,
tenant_id,
appt.id,
Some(operator_id),
UpdateAppointmentStatusReq {
status: "completed".to_string(),
cancel_reason: None,
},
confirmed.version,
)
.await
@@ -316,7 +297,9 @@ async fn test_appointment_cancel() {
seed_schedule(&state, tenant_id, doctor_id, date).await;
let appt = appointment_service::create_appointment(
&state, tenant_id, None,
&state,
tenant_id,
None,
CreateAppointmentReq {
patient_id,
doctor_id: Some(doctor_id),
@@ -331,7 +314,10 @@ async fn test_appointment_cancel() {
.expect("创建应成功");
let cancelled = appointment_service::update_appointment_status(
&state, tenant_id, appt.id, None,
&state,
tenant_id,
appt.id,
None,
UpdateAppointmentStatusReq {
status: "cancelled".to_string(),
cancel_reason: Some("患者临时有事".to_string()),
@@ -360,7 +346,9 @@ async fn test_appointment_version_conflict() {
seed_schedule(&state, tenant_id, doctor_id, date).await;
let appt = appointment_service::create_appointment(
&state, tenant_id, Some(operator_id),
&state,
tenant_id,
Some(operator_id),
CreateAppointmentReq {
patient_id,
doctor_id: Some(doctor_id),
@@ -376,8 +364,14 @@ async fn test_appointment_version_conflict() {
// 正确版本确认
let _confirmed = appointment_service::update_appointment_status(
&state, tenant_id, appt.id, Some(operator_id),
UpdateAppointmentStatusReq { status: "confirmed".to_string(), cancel_reason: None },
&state,
tenant_id,
appt.id,
Some(operator_id),
UpdateAppointmentStatusReq {
status: "confirmed".to_string(),
cancel_reason: None,
},
appt.version,
)
.await
@@ -385,8 +379,14 @@ async fn test_appointment_version_conflict() {
// 用旧版本再更新应失败
let result = appointment_service::update_appointment_status(
&state, tenant_id, appt.id, Some(operator_id),
UpdateAppointmentStatusReq { status: "cancelled".to_string(), cancel_reason: None },
&state,
tenant_id,
appt.id,
Some(operator_id),
UpdateAppointmentStatusReq {
status: "cancelled".to_string(),
cancel_reason: None,
},
appt.version, // 旧版本
)
.await;

View File

@@ -3,7 +3,7 @@
//! 验证文章 CRUD + 状态流、分类 CRUD、标签 CRUD、租户隔离、乐观锁。
use erp_health::dto::article_dto::*;
use erp_health::service::{article_service, article_category_service, article_tag_service};
use erp_health::service::{article_category_service, article_service, article_tag_service};
use super::test_fixture::TestApp;
@@ -29,7 +29,9 @@ fn default_create_article_req() -> CreateArticleReq {
async fn seed_article(app: &TestApp) -> ArticleResp {
article_service::create_article(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_article_req(),
)
.await
@@ -38,7 +40,9 @@ async fn seed_article(app: &TestApp) -> ArticleResp {
async fn seed_category(app: &TestApp, name: &str) -> CategoryResp {
article_category_service::create_category(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
CreateCategoryReq {
name: name.to_string(),
slug: None,
@@ -53,8 +57,12 @@ async fn seed_category(app: &TestApp, name: &str) -> CategoryResp {
async fn seed_tag(app: &TestApp, name: &str) -> TagResp {
article_tag_service::create_tag(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
CreateTagReq { name: name.to_string() },
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
CreateTagReq {
name: name.to_string(),
},
)
.await
.expect("创建标签应成功")
@@ -86,8 +94,11 @@ async fn test_article_status_flow() {
// draft → pending_review
let submitted = article_service::submit_article(
app.health_state(), app.tenant_id(), article.id,
Some(app.operator_id()), article.version,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
article.version,
)
.await
.expect("提交审核应成功");
@@ -95,9 +106,14 @@ async fn test_article_status_flow() {
// pending_review → published
let published = article_service::approve_article(
app.health_state(), app.tenant_id(), article.id,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
ReviewArticleReq { note: Some("通过".to_string()), version: Some(submitted.version) },
ReviewArticleReq {
note: Some("通过".to_string()),
version: Some(submitted.version),
},
submitted.version,
)
.await
@@ -106,8 +122,11 @@ async fn test_article_status_flow() {
// published → draft取消发布
let unpublished = article_service::unpublish_article(
app.health_state(), app.tenant_id(), article.id,
Some(app.operator_id()), published.version,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
published.version,
)
.await
.expect("取消发布应成功");
@@ -123,17 +142,25 @@ async fn test_article_reject_and_resubmit() {
let article = seed_article(&app).await;
let submitted = article_service::submit_article(
app.health_state(), app.tenant_id(), article.id,
Some(app.operator_id()), article.version,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
article.version,
)
.await
.unwrap();
// pending_review → rejected
let rejected = article_service::reject_article(
app.health_state(), app.tenant_id(), article.id,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
ReviewArticleReq { note: Some("内容需修改".to_string()), version: Some(submitted.version) },
ReviewArticleReq {
note: Some("内容需修改".to_string()),
version: Some(submitted.version),
},
submitted.version,
)
.await
@@ -142,8 +169,11 @@ async fn test_article_reject_and_resubmit() {
// rejected → pending_review
let resubmitted = article_service::submit_article(
app.health_state(), app.tenant_id(), article.id,
Some(app.operator_id()), rejected.version,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
rejected.version,
)
.await
.expect("重新提交应成功");
@@ -159,7 +189,9 @@ async fn test_article_update() {
let article = seed_article(&app).await;
let updated = article_service::update_article(
app.health_state(), app.tenant_id(), article.id,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
UpdateArticleReq {
title: Some("更新标题".to_string()),
@@ -196,24 +228,41 @@ async fn test_article_list_filter() {
// 提交 a1 到 pending_review
article_service::submit_article(
app.health_state(), app.tenant_id(), a1.id,
Some(app.operator_id()), a1.version,
app.health_state(),
app.tenant_id(),
a1.id,
Some(app.operator_id()),
a1.version,
)
.await
.unwrap();
// 按状态过滤
let pending = article_service::list_articles(
app.health_state(), app.tenant_id(), 1, 20,
None, Some("pending_review".to_string()), None, None, None,
app.health_state(),
app.tenant_id(),
1,
20,
None,
Some("pending_review".to_string()),
None,
None,
None,
)
.await
.unwrap();
assert_eq!(pending.total, 1);
let drafts = article_service::list_articles(
app.health_state(), app.tenant_id(), 1, 20,
None, Some("draft".to_string()), None, None, None,
app.health_state(),
app.tenant_id(),
1,
20,
None,
Some("draft".to_string()),
None,
None,
None,
)
.await
.unwrap();
@@ -229,16 +278,17 @@ async fn test_article_soft_delete() {
let article = seed_article(&app).await;
article_service::delete_article(
app.health_state(), app.tenant_id(), article.id,
Some(app.operator_id()), article.version,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
article.version,
)
.await
.expect("删除应成功");
let result = article_service::get_article(
app.health_state(), app.tenant_id(), article.id, true,
)
.await;
let result =
article_service::get_article(app.health_state(), app.tenant_id(), article.id, true).await;
assert!(result.is_err(), "软删除后查询应失败");
}
@@ -251,10 +301,8 @@ async fn test_article_tenant_isolation() {
let article = seed_article(&app).await;
let other_tenant = uuid::Uuid::new_v4();
let result = article_service::get_article(
app.health_state(), other_tenant, article.id, true,
)
.await;
let result =
article_service::get_article(app.health_state(), other_tenant, article.id, true).await;
assert!(result.is_err(), "不同租户不应看到此文章");
}
@@ -271,16 +319,16 @@ async fn test_category_crud_and_isolation() {
assert_eq!(cat.version, 1);
// 列表
let list = article_category_service::list_categories(
app.health_state(), app.tenant_id(),
)
.await
.unwrap();
let list = article_category_service::list_categories(app.health_state(), app.tenant_id())
.await
.unwrap();
assert_eq!(list.len(), 1);
// 更新
let updated = article_category_service::update_category(
app.health_state(), app.tenant_id(), cat.id,
app.health_state(),
app.tenant_id(),
cat.id,
Some(app.operator_id()),
UpdateCategoryReq {
name: Some("透析护理".to_string()),
@@ -297,27 +345,26 @@ async fn test_category_crud_and_isolation() {
// 删除
article_category_service::delete_category(
app.health_state(), app.tenant_id(), cat.id,
Some(app.operator_id()), updated.version,
app.health_state(),
app.tenant_id(),
cat.id,
Some(app.operator_id()),
updated.version,
)
.await
.expect("删除分类应成功");
let list_after = article_category_service::list_categories(
app.health_state(), app.tenant_id(),
)
.await
.unwrap();
let list_after = article_category_service::list_categories(app.health_state(), app.tenant_id())
.await
.unwrap();
assert_eq!(list_after.len(), 0, "删除后列表应为空");
// 租户隔离
let cat2 = seed_category(&app, "隔离分类").await;
let other_tenant = uuid::Uuid::new_v4();
let other_list = article_category_service::list_categories(
app.health_state(), other_tenant,
)
.await
.unwrap();
let other_list = article_category_service::list_categories(app.health_state(), other_tenant)
.await
.unwrap();
assert_eq!(other_list.len(), 0, "不同租户不应看到分类");
// 防止 unused warning
let _ = cat2;
@@ -337,7 +384,9 @@ async fn test_tag_crud_and_article_association() {
// 创建文章并关联标签
let article = article_service::create_article(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
CreateArticleReq {
title: "带标签的文章".to_string(),
tag_ids: vec![tag1.id, tag2.id],
@@ -350,14 +399,24 @@ async fn test_tag_crud_and_article_association() {
// 更新标签(替换为只有 tag1
let updated = article_service::update_article(
app.health_state(), app.tenant_id(), article.id,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
UpdateArticleReq {
tag_ids: Some(vec![tag1.id]),
version: article.version,
title: None, summary: None, content: None, cover_image: None,
category: None, author: None, published_at: None, slug: None,
content_type: None, category_id: None, sort_order: None,
title: None,
summary: None,
content: None,
cover_image: None,
category: None,
author: None,
published_at: None,
slug: None,
content_type: None,
category_id: None,
sort_order: None,
},
)
.await
@@ -365,18 +424,21 @@ async fn test_tag_crud_and_article_association() {
assert_eq!(updated.tags.len(), 1);
// 标签列表
let tags = article_tag_service::list_tags(
app.health_state(), app.tenant_id(),
)
.await
.unwrap();
let tags = article_tag_service::list_tags(app.health_state(), app.tenant_id())
.await
.unwrap();
assert_eq!(tags.len(), 2);
// 更新标签名称
let renamed = article_tag_service::update_tag(
app.health_state(), app.tenant_id(), tag1.id,
app.health_state(),
app.tenant_id(),
tag1.id,
Some(app.operator_id()),
UpdateTagReq { name: "血压高".to_string(), version: tag1.version },
UpdateTagReq {
name: "血压高".to_string(),
version: tag1.version,
},
)
.await
.expect("更新标签应成功");
@@ -384,17 +446,18 @@ async fn test_tag_crud_and_article_association() {
// 删除标签
article_tag_service::delete_tag(
app.health_state(), app.tenant_id(), tag2.id,
Some(app.operator_id()), tag2.version,
app.health_state(),
app.tenant_id(),
tag2.id,
Some(app.operator_id()),
tag2.version,
)
.await
.expect("删除标签应成功");
let tags_after = article_tag_service::list_tags(
app.health_state(), app.tenant_id(),
)
.await
.unwrap();
let tags_after = article_tag_service::list_tags(app.health_state(), app.tenant_id())
.await
.unwrap();
assert_eq!(tags_after.len(), 1);
}
@@ -408,14 +471,24 @@ async fn test_article_version_conflict() {
// 先更新一次version 变为 2
article_service::update_article(
app.health_state(), app.tenant_id(), article.id,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
UpdateArticleReq {
title: Some("第一次更新".to_string()),
version: article.version,
summary: None, content: None, cover_image: None, category: None,
author: None, published_at: None, slug: None, content_type: None,
category_id: None, tag_ids: None, sort_order: None,
summary: None,
content: None,
cover_image: None,
category: None,
author: None,
published_at: None,
slug: None,
content_type: None,
category_id: None,
tag_ids: None,
sort_order: None,
},
)
.await
@@ -423,14 +496,24 @@ async fn test_article_version_conflict() {
// 用旧 version 再次更新应失败
let result = article_service::update_article(
app.health_state(), app.tenant_id(), article.id,
app.health_state(),
app.tenant_id(),
article.id,
Some(app.operator_id()),
UpdateArticleReq {
title: Some("冲突更新".to_string()),
version: article.version, // 旧版本号
summary: None, content: None, cover_image: None, category: None,
author: None, published_at: None, slug: None, content_type: None,
category_id: None, tag_ids: None, sort_order: None,
summary: None,
content: None,
cover_image: None,
category: None,
author: None,
published_at: None,
slug: None,
content_type: None,
category_id: None,
tag_ids: None,
sort_order: None,
},
)
.await;

View File

@@ -21,7 +21,9 @@ fn default_create_consent_req(patient_id: uuid::Uuid) -> CreateConsentReq {
async fn seed_consent(app: &TestApp, patient_id: uuid::Uuid) -> ConsentResp {
consent_service::grant_consent(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_consent_req(patient_id),
)
.await
@@ -56,7 +58,9 @@ async fn test_consent_revoke() {
assert_eq!(consent.status, "granted");
let revoked = consent_service::revoke_consent(
app.health_state(), app.tenant_id(), consent.id,
app.health_state(),
app.tenant_id(),
consent.id,
Some(app.operator_id()),
RevokeConsentReq {
notes: Some("患者要求撤销".to_string()),
@@ -84,18 +88,16 @@ async fn test_consent_list_by_patient() {
seed_consent(&app, patient_a).await;
seed_consent(&app, patient_b).await;
let list_a = consent_service::list_consents(
app.health_state(), app.tenant_id(), patient_a, 1, 20,
)
.await
.unwrap();
let list_a =
consent_service::list_consents(app.health_state(), app.tenant_id(), patient_a, 1, 20)
.await
.unwrap();
assert_eq!(list_a.total, 2);
let list_b = consent_service::list_consents(
app.health_state(), app.tenant_id(), patient_b, 1, 20,
)
.await
.unwrap();
let list_b =
consent_service::list_consents(app.health_state(), app.tenant_id(), patient_b, 1, 20)
.await
.unwrap();
assert_eq!(list_b.total, 1);
}
@@ -109,11 +111,9 @@ async fn test_consent_tenant_isolation() {
seed_consent(&app, patient_id).await;
let other_tenant = uuid::Uuid::new_v4();
let list = consent_service::list_consents(
app.health_state(), other_tenant, patient_id, 1, 20,
)
.await
.unwrap();
let list = consent_service::list_consents(app.health_state(), other_tenant, patient_id, 1, 20)
.await
.unwrap();
assert_eq!(list.total, 0, "不同租户不应看到同意记录");
}
@@ -126,7 +126,9 @@ async fn test_consent_invalid_patient() {
let fake_patient = uuid::Uuid::new_v4();
let result = consent_service::grant_consent(
app.health_state(), app.tenant_id(), None,
app.health_state(),
app.tenant_id(),
None,
default_create_consent_req(fake_patient),
)
.await;
@@ -144,18 +146,28 @@ async fn test_consent_revoke_version_conflict() {
// 先撤销一次
consent_service::revoke_consent(
app.health_state(), app.tenant_id(), consent.id,
app.health_state(),
app.tenant_id(),
consent.id,
Some(app.operator_id()),
RevokeConsentReq { notes: None, version: consent.version },
RevokeConsentReq {
notes: None,
version: consent.version,
},
)
.await
.unwrap();
// 用旧 version 再撤销应失败
let result = consent_service::revoke_consent(
app.health_state(), app.tenant_id(), consent.id,
app.health_state(),
app.tenant_id(),
consent.id,
Some(app.operator_id()),
RevokeConsentReq { notes: None, version: consent.version },
RevokeConsentReq {
notes: None,
version: consent.version,
},
)
.await;
assert!(result.is_err(), "乐观锁冲突应返回错误");

View File

@@ -10,8 +10,14 @@ 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 },
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
CreateSessionReq {
patient_id,
doctor_id: None,
consultation_type: None,
},
)
.await
.expect("创建会话应成功")
@@ -27,7 +33,9 @@ async fn test_consultation_session_create() {
let doctor_id = app.create_doctor("咨询医生").await;
let session = consultation_service::create_session(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
CreateSessionReq {
patient_id,
doctor_id: Some(doctor_id),
@@ -53,11 +61,10 @@ async fn test_consultation_session_get() {
let session = seed_session(&app, patient_id).await;
let fetched = consultation_service::get_session(
app.health_state(), app.tenant_id(), session.id,
)
.await
.expect("查询应成功");
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");
}
@@ -76,14 +83,26 @@ async fn test_consultation_session_list_by_patient() {
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,
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,
app.health_state(),
app.tenant_id(),
1,
20,
None,
Some(patient_b),
None,
)
.await
.unwrap();
@@ -101,8 +120,11 @@ async fn test_consultation_message_send() {
let session = seed_session(&app, patient_id).await;
let msg = consultation_service::create_message(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.operator_id(), "doctor".to_string(),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
app.operator_id(),
"doctor".to_string(),
CreateMessageReq {
session_id: session.id,
content_type: Some("text".to_string()),
@@ -130,8 +152,11 @@ async fn test_consultation_message_list() {
for i in 0..3 {
consultation_service::create_message(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.operator_id(), "doctor".to_string(),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
app.operator_id(),
"doctor".to_string(),
CreateMessageReq {
session_id: session.id,
content_type: None,
@@ -143,7 +168,12 @@ async fn test_consultation_message_list() {
}
let messages = consultation_service::list_messages(
app.health_state(), app.tenant_id(), session.id, 1, 20, None,
app.health_state(),
app.tenant_id(),
session.id,
1,
20,
None,
)
.await
.expect("查询消息应成功");
@@ -163,7 +193,11 @@ async fn test_consultation_session_close() {
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,
app.health_state(),
app.tenant_id(),
session.id,
Some(app.operator_id()),
session.version,
)
.await
.expect("关闭应成功");
@@ -181,10 +215,8 @@ async fn test_consultation_session_tenant_isolation() {
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;
let result =
consultation_service::get_session(app.health_state(), other_tenant, session.id).await;
assert!(result.is_err(), "不同租户不应看到此会话");
}
@@ -197,7 +229,9 @@ async fn test_consultation_session_invalid_patient() {
let fake_patient = uuid::Uuid::new_v4();
let result = consultation_service::create_session(
app.health_state(), app.tenant_id(), None,
app.health_state(),
app.tenant_id(),
None,
CreateSessionReq {
patient_id: fake_patient,
doctor_id: None,

View File

@@ -25,7 +25,9 @@ fn default_create_req(patient_id: uuid::Uuid) -> CreateDailyMonitoringReq {
async fn seed_monitoring(app: &TestApp, patient_id: uuid::Uuid) -> DailyMonitoringResp {
daily_monitoring_service::create_daily_monitoring(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -57,11 +59,10 @@ async fn test_daily_monitoring_get() {
let patient_id = app.create_patient("监测查询患者").await;
let dm = seed_monitoring(&app, patient_id).await;
let fetched = daily_monitoring_service::get_daily_monitoring(
app.health_state(), app.tenant_id(), dm.id,
)
.await
.expect("查询应成功");
let fetched =
daily_monitoring_service::get_daily_monitoring(app.health_state(), app.tenant_id(), dm.id)
.await
.expect("查询应成功");
assert_eq!(fetched.id, dm.id);
assert_eq!(fetched.blood_sugar, Some(5.2));
}
@@ -76,7 +77,9 @@ async fn test_daily_monitoring_update() {
let dm = seed_monitoring(&app, patient_id).await;
let updated = daily_monitoring_service::update_daily_monitoring(
app.health_state(), app.tenant_id(), dm.id,
app.health_state(),
app.tenant_id(),
dm.id,
Some(app.operator_id()),
UpdateDailyMonitoringReq {
weight: Some(67.0),
@@ -113,14 +116,22 @@ async fn test_daily_monitoring_list_by_patient() {
seed_monitoring(&app, patient_b).await;
let list_a = daily_monitoring_service::list_daily_monitoring(
app.health_state(), app.tenant_id(), patient_a, 1, 20,
app.health_state(),
app.tenant_id(),
patient_a,
1,
20,
)
.await
.unwrap();
assert_eq!(list_a.total, 1);
let list_b = daily_monitoring_service::list_daily_monitoring(
app.health_state(), app.tenant_id(), patient_b, 1, 20,
app.health_state(),
app.tenant_id(),
patient_b,
1,
20,
)
.await
.unwrap();
@@ -137,16 +148,18 @@ async fn test_daily_monitoring_soft_delete() {
let dm = seed_monitoring(&app, patient_id).await;
daily_monitoring_service::delete_daily_monitoring(
app.health_state(), app.tenant_id(), dm.id,
Some(app.operator_id()), dm.version,
app.health_state(),
app.tenant_id(),
dm.id,
Some(app.operator_id()),
dm.version,
)
.await
.expect("删除应成功");
let result = daily_monitoring_service::get_daily_monitoring(
app.health_state(), app.tenant_id(), dm.id,
)
.await;
let result =
daily_monitoring_service::get_daily_monitoring(app.health_state(), app.tenant_id(), dm.id)
.await;
assert!(result.is_err(), "软删除后查询应失败");
}
@@ -161,7 +174,11 @@ async fn test_daily_monitoring_tenant_isolation() {
let other_tenant = uuid::Uuid::new_v4();
let list = daily_monitoring_service::list_daily_monitoring(
app.health_state(), other_tenant, patient_id, 1, 20,
app.health_state(),
other_tenant,
patient_id,
1,
20,
)
.await
.unwrap();
@@ -177,7 +194,9 @@ async fn test_daily_monitoring_invalid_patient() {
let fake_patient = uuid::Uuid::new_v4();
let result = daily_monitoring_service::create_daily_monitoring(
app.health_state(), app.tenant_id(), None,
app.health_state(),
app.tenant_id(),
None,
default_create_req(fake_patient),
)
.await;

View File

@@ -36,7 +36,10 @@ async fn test_vital_signs_create() {
let patient_id = app.create_patient("体征患者").await;
let vs = health_data_service::create_vital_signs(
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
Some(app.operator_id()),
default_vital_signs_req(),
)
.await
@@ -58,27 +61,41 @@ async fn test_vital_signs_list() {
let patient_b = app.create_patient("列表B").await;
health_data_service::create_vital_signs(
app.health_state(), app.tenant_id(), patient_a, None,
app.health_state(),
app.tenant_id(),
patient_a,
None,
default_vital_signs_req(),
)
.await
.unwrap();
health_data_service::create_vital_signs(
app.health_state(), app.tenant_id(), patient_b, None,
app.health_state(),
app.tenant_id(),
patient_b,
None,
default_vital_signs_req(),
)
.await
.unwrap();
let list_a = health_data_service::list_vital_signs(
app.health_state(), app.tenant_id(), patient_a, 1, 20,
app.health_state(),
app.tenant_id(),
patient_a,
1,
20,
)
.await
.unwrap();
assert_eq!(list_a.total, 1);
let list_b = health_data_service::list_vital_signs(
app.health_state(), app.tenant_id(), patient_b, 1, 20,
app.health_state(),
app.tenant_id(),
patient_b,
1,
20,
)
.await
.unwrap();
@@ -94,20 +111,35 @@ async fn test_vital_signs_update() {
let patient_id = app.create_patient("更新患者").await;
let vs = health_data_service::create_vital_signs(
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
Some(app.operator_id()),
default_vital_signs_req(),
)
.await
.unwrap();
let updated = health_data_service::update_vital_signs(
app.health_state(), app.tenant_id(), patient_id, vs.id, Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
vs.id,
Some(app.operator_id()),
UpdateVitalSignsReq {
record_date: None, systolic_bp_morning: None, diastolic_bp_morning: None,
systolic_bp_evening: None, diastolic_bp_evening: None,
heart_rate: Some(65), weight: Some(67.0),
blood_sugar: None, body_temperature: None, spo2: None,
blood_sugar_type: None, water_intake_ml: None, urine_output_ml: None,
record_date: None,
systolic_bp_morning: None,
diastolic_bp_morning: None,
systolic_bp_evening: None,
diastolic_bp_evening: None,
heart_rate: Some(65),
weight: Some(67.0),
blood_sugar: None,
body_temperature: None,
spo2: None,
blood_sugar_type: None,
water_intake_ml: None,
urine_output_ml: None,
notes: None,
},
vs.version,
@@ -128,18 +160,20 @@ async fn test_vital_signs_tenant_isolation() {
let patient_id = app.create_patient("隔离患者").await;
health_data_service::create_vital_signs(
app.health_state(), app.tenant_id(), patient_id, None,
app.health_state(),
app.tenant_id(),
patient_id,
None,
default_vital_signs_req(),
)
.await
.unwrap();
let other_tenant = uuid::Uuid::new_v4();
let list = health_data_service::list_vital_signs(
app.health_state(), other_tenant, patient_id, 1, 20,
)
.await
.unwrap();
let list =
health_data_service::list_vital_signs(app.health_state(), other_tenant, patient_id, 1, 20)
.await
.unwrap();
assert_eq!(list.total, 0, "不同租户不应看到体征记录");
}
@@ -183,11 +217,17 @@ async fn test_lab_report_review() {
let patient_id = app.create_patient("审阅患者").await;
let report = health_data_service::create_lab_report(
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
Some(app.operator_id()),
CreateLabReportReq {
report_date: chrono::NaiveDate::from_ymd_opt(2026, 5, 3).unwrap(),
report_type: "blood_routine".to_string(),
source: None, items: None, image_urls: None, doctor_notes: None,
source: None,
items: None,
image_urls: None,
doctor_notes: None,
},
)
.await
@@ -195,8 +235,15 @@ async fn test_lab_report_review() {
assert_eq!(report.status, "pending");
let reviewed = health_data_service::review_lab_report(
app.health_state(), app.tenant_id(), patient_id, report.id, app.operator_id(),
ReviewLabReportReq { doctor_notes: Some("复查确认".to_string()), items: None },
app.health_state(),
app.tenant_id(),
patient_id,
report.id,
app.operator_id(),
ReviewLabReportReq {
doctor_notes: Some("复查确认".to_string()),
items: None,
},
report.version,
)
.await
@@ -216,11 +263,17 @@ async fn test_lab_report_list() {
for pid in &[patient_a, patient_b] {
health_data_service::create_lab_report(
app.health_state(), app.tenant_id(), *pid, None,
app.health_state(),
app.tenant_id(),
*pid,
None,
CreateLabReportReq {
report_date: chrono::NaiveDate::from_ymd_opt(2026, 5, 4).unwrap(),
report_type: "blood_routine".to_string(),
source: None, items: None, image_urls: None, doctor_notes: None,
source: None,
items: None,
image_urls: None,
doctor_notes: None,
},
)
.await
@@ -228,7 +281,11 @@ async fn test_lab_report_list() {
}
let list_a = health_data_service::list_lab_reports(
app.health_state(), app.tenant_id(), patient_a, 1, 20,
app.health_state(),
app.tenant_id(),
patient_a,
1,
20,
)
.await
.unwrap();
@@ -245,7 +302,10 @@ async fn test_vital_signs_invalid_patient() {
let fake_patient = uuid::Uuid::new_v4();
let result = health_data_service::create_vital_signs(
app.health_state(), app.tenant_id(), fake_patient, None,
app.health_state(),
app.tenant_id(),
fake_patient,
None,
default_vital_signs_req(),
)
.await;

View File

@@ -2,11 +2,9 @@
//!
//! 验证批量摄入、设备绑定自动创建、hourly 聚合、查询过滤、参数校验、租户隔离。
use erp_health::service::device_reading_service::{
BatchReadingRequest, ReadingInput,
};
use erp_health::service::device_reading_service;
use chrono::Datelike;
use erp_health::service::device_reading_service;
use erp_health::service::device_reading_service::{BatchReadingRequest, ReadingInput};
use sea_orm::ConnectionTrait;
use super::test_fixture::TestApp;
@@ -27,10 +25,13 @@ async fn ensure_current_month_partition(app: &TestApp) {
let sql = format!(
"CREATE TABLE IF NOT EXISTS device_readings_{suffix} PARTITION OF device_readings FOR VALUES FROM ('{start}') TO ('{next_month}');"
);
app.db().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
sql,
)).await.expect("创建分区应成功");
app.db()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
sql,
))
.await
.expect("创建分区应成功");
}
/// 构建一条心率读数measured_at 用几分钟前的时间)
@@ -53,7 +54,9 @@ async fn test_device_reading_batch_single() {
let patient_id = app.create_patient("读数患者").await;
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
app.health_state(),
app.tenant_id(),
patient_id,
BatchReadingRequest {
device_id: "watch-001".to_string(),
device_model: Some("Apple Watch".to_string()),
@@ -79,7 +82,9 @@ async fn test_device_reading_batch_multiple() {
let patient_id = app.create_patient("批量患者").await;
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
app.health_state(),
app.tenant_id(),
patient_id,
BatchReadingRequest {
device_id: "watch-002".to_string(),
device_model: None,
@@ -106,7 +111,9 @@ async fn test_device_reading_creates_device_binding() {
let patient_id = app.create_patient("绑定患者").await;
device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
app.health_state(),
app.tenant_id(),
patient_id,
BatchReadingRequest {
device_id: "band-001".to_string(),
device_model: Some("Mi Band".to_string()),
@@ -118,7 +125,9 @@ async fn test_device_reading_creates_device_binding() {
// 再次使用同一设备,应更新 last_sync_at 而非重复创建
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
app.health_state(),
app.tenant_id(),
patient_id,
BatchReadingRequest {
device_id: "band-001".to_string(),
device_model: Some("Mi Band".to_string()),
@@ -141,7 +150,9 @@ async fn test_device_reading_hourly_aggregation() {
let patient_id = app.create_patient("聚合患者").await;
device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
app.health_state(),
app.tenant_id(),
patient_id,
BatchReadingRequest {
device_id: "watch-003".to_string(),
device_model: None,
@@ -157,7 +168,13 @@ async fn test_device_reading_hourly_aggregation() {
// 查询 hourly 聚合
let hourly = device_reading_service::query_hourly_readings(
app.health_state(), app.tenant_id(), patient_id, "heart_rate", 1, 1, 20,
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
1,
1,
20,
)
.await
.expect("查询 hourly 应成功");
@@ -179,21 +196,26 @@ async fn test_device_reading_query_filter() {
let patient_id = app.create_patient("查询患者").await;
device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
app.health_state(),
app.tenant_id(),
patient_id,
BatchReadingRequest {
device_id: "watch-004".to_string(),
device_model: None,
readings: vec![
heart_rate_reading(72, 5),
heart_rate_reading(74, 3),
],
readings: vec![heart_rate_reading(72, 5), heart_rate_reading(74, 3)],
},
)
.await
.unwrap();
let readings = device_reading_service::query_device_readings(
app.health_state(), app.tenant_id(), patient_id, Some("heart_rate"), None, 1, 20,
app.health_state(),
app.tenant_id(),
patient_id,
Some("heart_rate"),
None,
1,
20,
)
.await
.expect("查询应成功");
@@ -211,7 +233,9 @@ async fn test_device_reading_invalid_device_type() {
let patient_id = app.create_patient("校验患者").await;
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
app.health_state(),
app.tenant_id(),
patient_id,
BatchReadingRequest {
device_id: "bad-001".to_string(),
device_model: None,
@@ -237,7 +261,9 @@ async fn test_device_reading_future_time_rejected() {
let future_time = (chrono::Utc::now() + chrono::Duration::hours(1)).to_rfc3339();
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
app.health_state(),
app.tenant_id(),
patient_id,
BatchReadingRequest {
device_id: "watch-005".to_string(),
device_model: None,
@@ -264,7 +290,9 @@ async fn test_device_reading_invalid_patient_and_isolation() {
// 无效患者
let fake_patient = uuid::Uuid::new_v4();
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), fake_patient,
app.health_state(),
app.tenant_id(),
fake_patient,
BatchReadingRequest {
device_id: "watch-006".to_string(),
device_model: None,
@@ -277,7 +305,9 @@ async fn test_device_reading_invalid_patient_and_isolation() {
// 租户隔离:创建患者并摄入数据,用不同租户查询
let patient_id = app.create_patient("隔离患者").await;
device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
app.health_state(),
app.tenant_id(),
patient_id,
BatchReadingRequest {
device_id: "watch-007".to_string(),
device_model: None,
@@ -289,7 +319,13 @@ async fn test_device_reading_invalid_patient_and_isolation() {
let other_tenant = uuid::Uuid::new_v4();
let readings = device_reading_service::query_device_readings(
app.health_state(), other_tenant, patient_id, None, None, 1, 20,
app.health_state(),
other_tenant,
patient_id,
None,
None,
1,
20,
)
.await
.expect("查询应成功");

View File

@@ -20,9 +20,17 @@ fn default_create_diagnosis_req() -> CreateDiagnosisReq {
}
}
async fn seed_diagnosis(app: &TestApp, patient_id: uuid::Uuid, icd_code: &str, name: &str) -> DiagnosisResp {
async fn seed_diagnosis(
app: &TestApp,
patient_id: uuid::Uuid,
icd_code: &str,
name: &str,
) -> DiagnosisResp {
diagnosis_service::create_diagnosis(
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
Some(app.operator_id()),
CreateDiagnosisReq {
icd_code: icd_code.to_string(),
diagnosis_name: name.to_string(),
@@ -60,7 +68,9 @@ async fn test_diagnosis_update() {
let diag = seed_diagnosis(&app, patient_id, "N18.8", "CKD更新").await;
let updated = diagnosis_service::update_diagnosis(
app.health_state(), app.tenant_id(), diag.id,
app.health_state(),
app.tenant_id(),
diag.id,
Some(app.operator_id()),
UpdateDiagnosisReq {
status: Some("chronic".to_string()),
@@ -94,18 +104,16 @@ async fn test_diagnosis_list_by_patient() {
seed_diagnosis(&app, patient_a, "N18.2", "CKD 2期").await;
seed_diagnosis(&app, patient_b, "E11.9", "2型糖尿病").await;
let list_a = diagnosis_service::list_diagnoses(
app.health_state(), app.tenant_id(), patient_a, 1, 20,
)
.await
.unwrap();
let list_a =
diagnosis_service::list_diagnoses(app.health_state(), app.tenant_id(), patient_a, 1, 20)
.await
.unwrap();
assert_eq!(list_a.total, 2);
let list_b = diagnosis_service::list_diagnoses(
app.health_state(), app.tenant_id(), patient_b, 1, 20,
)
.await
.unwrap();
let list_b =
diagnosis_service::list_diagnoses(app.health_state(), app.tenant_id(), patient_b, 1, 20)
.await
.unwrap();
assert_eq!(list_b.total, 1);
}
@@ -119,17 +127,19 @@ async fn test_diagnosis_soft_delete() {
let diag = seed_diagnosis(&app, patient_id, "N18.3", "CKD删除").await;
diagnosis_service::delete_diagnosis(
app.health_state(), app.tenant_id(), diag.id,
Some(app.operator_id()), diag.version,
app.health_state(),
app.tenant_id(),
diag.id,
Some(app.operator_id()),
diag.version,
)
.await
.expect("删除应成功");
let list = diagnosis_service::list_diagnoses(
app.health_state(), app.tenant_id(), patient_id, 1, 20,
)
.await
.unwrap();
let list =
diagnosis_service::list_diagnoses(app.health_state(), app.tenant_id(), patient_id, 1, 20)
.await
.unwrap();
assert_eq!(list.total, 0);
}
@@ -143,11 +153,10 @@ async fn test_diagnosis_tenant_isolation() {
seed_diagnosis(&app, patient_id, "N18.4", "CKD隔离").await;
let other_tenant = uuid::Uuid::new_v4();
let list = diagnosis_service::list_diagnoses(
app.health_state(), other_tenant, patient_id, 1, 20,
)
.await
.unwrap();
let list =
diagnosis_service::list_diagnoses(app.health_state(), other_tenant, patient_id, 1, 20)
.await
.unwrap();
assert_eq!(list.total, 0, "不同租户不应看到诊断记录");
}
@@ -160,7 +169,10 @@ async fn test_diagnosis_invalid_patient() {
let fake_patient = uuid::Uuid::new_v4();
let result = diagnosis_service::create_diagnosis(
app.health_state(), app.tenant_id(), fake_patient, None,
app.health_state(),
app.tenant_id(),
fake_patient,
None,
default_create_diagnosis_req(),
)
.await;
@@ -178,13 +190,19 @@ async fn test_diagnosis_version_conflict() {
// 先更新一次
diagnosis_service::update_diagnosis(
app.health_state(), app.tenant_id(), diag.id,
app.health_state(),
app.tenant_id(),
diag.id,
Some(app.operator_id()),
UpdateDiagnosisReq {
status: Some("resolved".to_string()),
icd_code: None, diagnosis_name: None, diagnosis_type: None,
diagnosed_date: None, health_record_id: None,
diagnosed_by: None, notes: None,
icd_code: None,
diagnosis_name: None,
diagnosis_type: None,
diagnosed_date: None,
health_record_id: None,
diagnosed_by: None,
notes: None,
},
diag.version,
)
@@ -193,13 +211,19 @@ async fn test_diagnosis_version_conflict() {
// 用旧 version 再更新应失败
let result = diagnosis_service::update_diagnosis(
app.health_state(), app.tenant_id(), diag.id,
app.health_state(),
app.tenant_id(),
diag.id,
Some(app.operator_id()),
UpdateDiagnosisReq {
status: Some("chronic".to_string()),
icd_code: None, diagnosis_name: None, diagnosis_type: None,
diagnosed_date: None, health_record_id: None,
diagnosed_by: None, notes: None,
icd_code: None,
diagnosis_name: None,
diagnosis_type: None,
diagnosed_date: None,
health_record_id: None,
diagnosed_by: None,
notes: None,
},
diag.version,
)

View File

@@ -33,7 +33,9 @@ fn default_create_req(patient_id: uuid::Uuid) -> CreateDialysisPrescriptionReq {
async fn seed_prescription(app: &TestApp, patient_id: uuid::Uuid) -> DialysisPrescriptionResp {
dialysis_prescription_service::create_prescription(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -67,7 +69,9 @@ async fn test_dialysis_prescription_get() {
let rx = seed_prescription(&app, patient_id).await;
let fetched = dialysis_prescription_service::get_prescription(
app.dialysis_state(), app.tenant_id(), rx.id,
app.dialysis_state(),
app.tenant_id(),
rx.id,
)
.await
.expect("查询应成功");
@@ -84,20 +88,30 @@ async fn test_dialysis_prescription_update() {
let rx = seed_prescription(&app, patient_id).await;
let updated = dialysis_prescription_service::update_prescription(
app.dialysis_state(), app.tenant_id(), rx.id,
app.dialysis_state(),
app.tenant_id(),
rx.id,
Some(app.operator_id()),
UpdateDialysisPrescriptionReq {
blood_flow_rate: Some(350),
frequency_per_week: Some(4),
status: None,
dialyzer_model: None, membrane_area: None,
dialysate_potassium: None, dialysate_calcium: None,
dialysate_bicarbonate: None, anticoagulation_type: None,
anticoagulation_dose: None, target_ultrafiltration_ml: None,
target_dry_weight: None, dialysate_flow_rate: None,
duration_minutes: None, vascular_access_type: None,
vascular_access_location: None, effective_from: None,
effective_to: None, notes: None,
dialyzer_model: None,
membrane_area: None,
dialysate_potassium: None,
dialysate_calcium: None,
dialysate_bicarbonate: None,
anticoagulation_type: None,
anticoagulation_dose: None,
target_ultrafiltration_ml: None,
target_dry_weight: None,
dialysate_flow_rate: None,
duration_minutes: None,
vascular_access_type: None,
vascular_access_location: None,
effective_from: None,
effective_to: None,
notes: None,
},
rx.version,
)
@@ -122,14 +136,24 @@ async fn test_dialysis_prescription_list_by_patient() {
seed_prescription(&app, patient_b).await;
let list_a = dialysis_prescription_service::list_prescriptions(
app.dialysis_state(), app.tenant_id(), 1, 20, Some(patient_a), None,
app.dialysis_state(),
app.tenant_id(),
1,
20,
Some(patient_a),
None,
)
.await
.unwrap();
assert_eq!(list_a.total, 1);
let list_b = dialysis_prescription_service::list_prescriptions(
app.dialysis_state(), app.tenant_id(), 1, 20, Some(patient_b), None,
app.dialysis_state(),
app.tenant_id(),
1,
20,
Some(patient_b),
None,
)
.await
.unwrap();
@@ -146,14 +170,19 @@ async fn test_dialysis_prescription_soft_delete() {
let rx = seed_prescription(&app, patient_id).await;
dialysis_prescription_service::delete_prescription(
app.dialysis_state(), app.tenant_id(), rx.id,
Some(app.operator_id()), rx.version,
app.dialysis_state(),
app.tenant_id(),
rx.id,
Some(app.operator_id()),
rx.version,
)
.await
.expect("删除应成功");
let result = dialysis_prescription_service::get_prescription(
app.dialysis_state(), app.tenant_id(), rx.id,
app.dialysis_state(),
app.tenant_id(),
rx.id,
)
.await;
assert!(result.is_err(), "软删除后查询应失败");
@@ -170,7 +199,12 @@ async fn test_dialysis_prescription_tenant_isolation() {
let other_tenant = uuid::Uuid::new_v4();
let list = dialysis_prescription_service::list_prescriptions(
app.dialysis_state(), other_tenant, 1, 20, None, None,
app.dialysis_state(),
other_tenant,
1,
20,
None,
None,
)
.await
.unwrap();
@@ -188,18 +222,30 @@ async fn test_dialysis_prescription_version_conflict() {
// 先更新一次
dialysis_prescription_service::update_prescription(
app.dialysis_state(), app.tenant_id(), rx.id,
app.dialysis_state(),
app.tenant_id(),
rx.id,
Some(app.operator_id()),
UpdateDialysisPrescriptionReq {
blood_flow_rate: Some(350),
status: None, dialyzer_model: None, membrane_area: None,
dialysate_potassium: None, dialysate_calcium: None,
dialysate_bicarbonate: None, anticoagulation_type: None,
anticoagulation_dose: None, target_ultrafiltration_ml: None,
target_dry_weight: None, frequency_per_week: None,
dialysate_flow_rate: None, duration_minutes: None,
vascular_access_type: None, vascular_access_location: None,
effective_from: None, effective_to: None, notes: None,
status: None,
dialyzer_model: None,
membrane_area: None,
dialysate_potassium: None,
dialysate_calcium: None,
dialysate_bicarbonate: None,
anticoagulation_type: None,
anticoagulation_dose: None,
target_ultrafiltration_ml: None,
target_dry_weight: None,
frequency_per_week: None,
dialysate_flow_rate: None,
duration_minutes: None,
vascular_access_type: None,
vascular_access_location: None,
effective_from: None,
effective_to: None,
notes: None,
},
rx.version,
)
@@ -208,18 +254,30 @@ async fn test_dialysis_prescription_version_conflict() {
// 用旧 version 再更新应失败
let result = dialysis_prescription_service::update_prescription(
app.dialysis_state(), app.tenant_id(), rx.id,
app.dialysis_state(),
app.tenant_id(),
rx.id,
Some(app.operator_id()),
UpdateDialysisPrescriptionReq {
blood_flow_rate: Some(400),
status: None, dialyzer_model: None, membrane_area: None,
dialysate_potassium: None, dialysate_calcium: None,
dialysate_bicarbonate: None, anticoagulation_type: None,
anticoagulation_dose: None, target_ultrafiltration_ml: None,
target_dry_weight: None, frequency_per_week: None,
dialysate_flow_rate: None, duration_minutes: None,
vascular_access_type: None, vascular_access_location: None,
effective_from: None, effective_to: None, notes: None,
status: None,
dialyzer_model: None,
membrane_area: None,
dialysate_potassium: None,
dialysate_calcium: None,
dialysate_bicarbonate: None,
anticoagulation_type: None,
anticoagulation_dose: None,
target_ultrafiltration_ml: None,
target_dry_weight: None,
frequency_per_week: None,
dialysate_flow_rate: None,
duration_minutes: None,
vascular_access_type: None,
vascular_access_location: None,
effective_from: None,
effective_to: None,
notes: None,
},
rx.version,
)

View File

@@ -42,7 +42,10 @@ async fn test_dialysis_create_basic() {
let req = default_create_req(patient_id);
let record = dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()), req,
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
req,
)
.await
.expect("创建透析记录应成功");
@@ -53,11 +56,10 @@ async fn test_dialysis_create_basic() {
assert_eq!(record.ultrafiltration_volume, Some(2500));
// 读取
let fetched = dialysis_service::get_dialysis_record(
app.dialysis_state(), app.tenant_id(), record.id,
)
.await
.expect("查询应成功");
let fetched =
dialysis_service::get_dialysis_record(app.dialysis_state(), app.tenant_id(), record.id)
.await
.expect("查询应成功");
assert_eq!(fetched.id, record.id);
}
@@ -74,7 +76,10 @@ async fn test_dialysis_create_pii_encrypted() {
req.complication_notes = Some("低血压发作".to_string());
let record = dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()), req,
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
req,
)
.await
.expect("创建应成功");
@@ -93,7 +98,9 @@ async fn test_dialysis_update_status_flow() {
let patient_id = app.create_patient("状态流转患者").await;
let record = dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -101,11 +108,17 @@ async fn test_dialysis_update_status_flow() {
assert_eq!(record.status, "draft");
// 先将状态推进到 completeddraft → completed → reviewed
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter};
use erp_dialysis::entity::dialysis_record;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
let _ = dialysis_record::Entity::update_many()
.col_expr(dialysis_record::Column::Status, sea_orm::sea_query::Expr::val("completed").into())
.col_expr(dialysis_record::Column::Version, sea_orm::sea_query::Expr::val(record.version + 1).into())
.col_expr(
dialysis_record::Column::Status,
sea_orm::sea_query::Expr::val("completed").into(),
)
.col_expr(
dialysis_record::Column::Version,
sea_orm::sea_query::Expr::val(record.version + 1).into(),
)
.filter(dialysis_record::Column::Id.eq(record.id))
.exec(app.db())
.await
@@ -113,7 +126,11 @@ async fn test_dialysis_update_status_flow() {
// 审核: completed → reviewed
let reviewed = dialysis_service::review_dialysis_record(
app.dialysis_state(), app.tenant_id(), record.id, app.operator_id(), record.version + 1,
app.dialysis_state(),
app.tenant_id(),
record.id,
app.operator_id(),
record.version + 1,
)
.await
.expect("审核应成功");
@@ -131,35 +148,49 @@ async fn test_dialysis_list_by_patient() {
let patient_b = app.create_patient("列表患者B").await;
dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(patient_a),
)
.await
.unwrap();
dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(patient_a),
)
.await
.unwrap();
dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(patient_b),
)
.await
.unwrap();
let list_a = dialysis_service::list_dialysis_records(
app.dialysis_state(), app.tenant_id(), patient_a, 1, 20,
app.dialysis_state(),
app.tenant_id(),
patient_a,
1,
20,
)
.await
.unwrap();
assert_eq!(list_a.total, 2);
let list_b = dialysis_service::list_dialysis_records(
app.dialysis_state(), app.tenant_id(), patient_b, 1, 20,
app.dialysis_state(),
app.tenant_id(),
patient_b,
1,
20,
)
.await
.unwrap();
@@ -175,7 +206,9 @@ async fn test_dialysis_tenant_isolation() {
let patient_id = app.create_patient("隔离患者").await;
let record = dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -183,10 +216,8 @@ async fn test_dialysis_tenant_isolation() {
// 用不同 tenant_id 查询应失败
let other_tenant = uuid::Uuid::new_v4();
let result = dialysis_service::get_dialysis_record(
app.dialysis_state(), other_tenant, record.id,
)
.await;
let result =
dialysis_service::get_dialysis_record(app.dialysis_state(), other_tenant, record.id).await;
assert!(result.is_err(), "不同租户不应看到此记录");
}
@@ -199,7 +230,9 @@ async fn test_dialysis_version_conflict() {
let patient_id = app.create_patient("乐观锁患者").await;
let record = dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -207,17 +240,29 @@ async fn test_dialysis_version_conflict() {
// 用正确版本更新
let updated = dialysis_service::update_dialysis_record(
app.dialysis_state(), app.tenant_id(), record.id, Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
record.id,
Some(app.operator_id()),
UpdateDialysisRecordReq {
dialysis_date: None, start_time: None, end_time: None,
dry_weight: None, pre_weight: None, post_weight: None,
pre_bp_systolic: None, pre_bp_diastolic: None,
post_bp_systolic: None, post_bp_diastolic: None,
pre_heart_rate: None, post_heart_rate: None,
ultrafiltration_volume: None, dialysis_duration: None,
dialysis_date: None,
start_time: None,
end_time: None,
dry_weight: None,
pre_weight: None,
post_weight: None,
pre_bp_systolic: None,
pre_bp_diastolic: None,
post_bp_systolic: None,
post_bp_diastolic: None,
pre_heart_rate: None,
post_heart_rate: None,
ultrafiltration_volume: None,
dialysis_duration: None,
blood_flow_rate: None,
dialysis_type: Some("HDF".to_string()),
symptoms: None, complication_notes: None,
symptoms: None,
complication_notes: None,
},
record.version,
)
@@ -227,17 +272,29 @@ async fn test_dialysis_version_conflict() {
// 用旧版本更新应失败
let result = dialysis_service::update_dialysis_record(
app.dialysis_state(), app.tenant_id(), record.id, Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
record.id,
Some(app.operator_id()),
UpdateDialysisRecordReq {
dialysis_date: None, start_time: None, end_time: None,
dry_weight: None, pre_weight: None, post_weight: None,
pre_bp_systolic: None, pre_bp_diastolic: None,
post_bp_systolic: None, post_bp_diastolic: None,
pre_heart_rate: None, post_heart_rate: None,
ultrafiltration_volume: None, dialysis_duration: None,
dialysis_date: None,
start_time: None,
end_time: None,
dry_weight: None,
pre_weight: None,
post_weight: None,
pre_bp_systolic: None,
pre_bp_diastolic: None,
post_bp_systolic: None,
post_bp_diastolic: None,
pre_heart_rate: None,
post_heart_rate: None,
ultrafiltration_volume: None,
dialysis_duration: None,
blood_flow_rate: None,
dialysis_type: Some("HD".to_string()),
symptoms: None, complication_notes: None,
symptoms: None,
complication_notes: None,
},
record.version, // 旧版本
)
@@ -254,7 +311,9 @@ async fn test_dialysis_soft_delete() {
let patient_id = app.create_patient("软删除患者").await;
let record = dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -262,21 +321,28 @@ async fn test_dialysis_soft_delete() {
// 删除
dialysis_service::delete_dialysis_record(
app.dialysis_state(), app.tenant_id(), record.id, Some(app.operator_id()), record.version,
app.dialysis_state(),
app.tenant_id(),
record.id,
Some(app.operator_id()),
record.version,
)
.await
.expect("删除应成功");
// 查询应失败
let result = dialysis_service::get_dialysis_record(
app.dialysis_state(), app.tenant_id(), record.id,
)
.await;
let result =
dialysis_service::get_dialysis_record(app.dialysis_state(), app.tenant_id(), record.id)
.await;
assert!(result.is_err(), "软删除后应不可见");
// 列表中不应出现
let list = dialysis_service::list_dialysis_records(
app.dialysis_state(), app.tenant_id(), patient_id, 1, 20,
app.dialysis_state(),
app.tenant_id(),
patient_id,
1,
20,
)
.await
.unwrap();
@@ -292,7 +358,9 @@ async fn test_dialysis_create_without_patient_returns_error() {
let fake_patient = uuid::Uuid::new_v4();
let result = dialysis_service::create_dialysis_record(
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(fake_patient),
)
.await;

View File

@@ -26,7 +26,9 @@ fn default_create_doctor_req() -> CreateDoctorReq {
async fn test_doctor_create() {
let app = TestApp::new().await;
let doctor = doctor_service::create_doctor(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_doctor_req(),
)
.await
@@ -45,17 +47,17 @@ async fn test_doctor_create() {
async fn test_doctor_get() {
let app = TestApp::new().await;
let doctor = doctor_service::create_doctor(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_doctor_req(),
)
.await
.unwrap();
let fetched = doctor_service::get_doctor(
app.health_state(), app.tenant_id(), doctor.id,
)
.await
.expect("查询应成功");
let fetched = doctor_service::get_doctor(app.health_state(), app.tenant_id(), doctor.id)
.await
.expect("查询应成功");
assert_eq!(fetched.id, doctor.id);
assert_eq!(fetched.name, "张三");
}
@@ -67,14 +69,18 @@ async fn test_doctor_get() {
async fn test_doctor_update() {
let app = TestApp::new().await;
let doctor = doctor_service::create_doctor(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_doctor_req(),
)
.await
.unwrap();
let updated = doctor_service::update_doctor(
app.health_state(), app.tenant_id(), doctor.id,
app.health_state(),
app.tenant_id(),
doctor.id,
Some(app.operator_id()),
UpdateDoctorReq {
name: Some("李四".to_string()),
@@ -104,7 +110,9 @@ async fn test_doctor_list_and_search() {
let app = TestApp::new().await;
doctor_service::create_doctor(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
CreateDoctorReq {
name: "王医生".to_string(),
department: Some("心内科".to_string()),
@@ -115,7 +123,9 @@ async fn test_doctor_list_and_search() {
.unwrap();
doctor_service::create_doctor(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
CreateDoctorReq {
name: "赵医生".to_string(),
department: Some("肾内科".to_string()),
@@ -126,17 +136,21 @@ async fn test_doctor_list_and_search() {
.unwrap();
// 全量列表
let all = doctor_service::list_doctors(
app.health_state(), app.tenant_id(), 1, 20, None, None, None,
)
.await
.unwrap();
let all =
doctor_service::list_doctors(app.health_state(), app.tenant_id(), 1, 20, None, None, None)
.await
.unwrap();
assert_eq!(all.total, 2);
// 按科室过滤
let renal = doctor_service::list_doctors(
app.health_state(), app.tenant_id(), 1, 20,
None, Some("肾内科".to_string()), None,
app.health_state(),
app.tenant_id(),
1,
20,
None,
Some("肾内科".to_string()),
None,
)
.await
.unwrap();
@@ -150,23 +164,25 @@ async fn test_doctor_list_and_search() {
async fn test_doctor_soft_delete() {
let app = TestApp::new().await;
let doctor = doctor_service::create_doctor(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_doctor_req(),
)
.await
.unwrap();
doctor_service::delete_doctor(
app.health_state(), app.tenant_id(), doctor.id,
Some(app.operator_id()), doctor.version,
app.health_state(),
app.tenant_id(),
doctor.id,
Some(app.operator_id()),
doctor.version,
)
.await
.expect("删除应成功");
let result = doctor_service::get_doctor(
app.health_state(), app.tenant_id(), doctor.id,
)
.await;
let result = doctor_service::get_doctor(app.health_state(), app.tenant_id(), doctor.id).await;
assert!(result.is_err(), "软删除后查询应失败");
}
@@ -177,24 +193,22 @@ async fn test_doctor_soft_delete() {
async fn test_doctor_tenant_isolation() {
let app = TestApp::new().await;
let doctor = doctor_service::create_doctor(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_doctor_req(),
)
.await
.unwrap();
let other_tenant = uuid::Uuid::new_v4();
let result = doctor_service::get_doctor(
app.health_state(), other_tenant, doctor.id,
)
.await;
let result = doctor_service::get_doctor(app.health_state(), other_tenant, doctor.id).await;
assert!(result.is_err(), "不同租户不应看到此医生");
let other_list = doctor_service::list_doctors(
app.health_state(), other_tenant, 1, 20, None, None, None,
)
.await
.unwrap();
let other_list =
doctor_service::list_doctors(app.health_state(), other_tenant, 1, 20, None, None, None)
.await
.unwrap();
assert_eq!(other_list.total, 0);
}
@@ -205,7 +219,9 @@ async fn test_doctor_tenant_isolation() {
async fn test_doctor_version_conflict() {
let app = TestApp::new().await;
let doctor = doctor_service::create_doctor(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_doctor_req(),
)
.await
@@ -213,12 +229,18 @@ async fn test_doctor_version_conflict() {
// 先更新一次
doctor_service::update_doctor(
app.health_state(), app.tenant_id(), doctor.id,
app.health_state(),
app.tenant_id(),
doctor.id,
Some(app.operator_id()),
UpdateDoctorReq {
name: Some("第一次".to_string()),
department: None, title: None, specialty: None,
license_number: None, bio: None, online_status: None,
department: None,
title: None,
specialty: None,
license_number: None,
bio: None,
online_status: None,
},
doctor.version,
)
@@ -227,12 +249,18 @@ async fn test_doctor_version_conflict() {
// 用旧 version 再更新应失败
let result = doctor_service::update_doctor(
app.health_state(), app.tenant_id(), doctor.id,
app.health_state(),
app.tenant_id(),
doctor.id,
Some(app.operator_id()),
UpdateDoctorReq {
name: Some("冲突".to_string()),
department: None, title: None, specialty: None,
license_number: None, bio: None, online_status: None,
department: None,
title: None,
specialty: None,
license_number: None,
bio: None,
online_status: None,
},
doctor.version,
)

View File

@@ -32,7 +32,9 @@ fn default_create_req() -> CreateFollowUpTemplateReq {
async fn seed_template(app: &TestApp) -> FollowUpTemplateResp {
follow_up_template_service::create_template(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(),
)
.await
@@ -63,11 +65,10 @@ async fn test_template_get_with_fields() {
let app = TestApp::new().await;
let tmpl = seed_template(&app).await;
let fetched = follow_up_template_service::get_template(
app.health_state(), app.tenant_id(), tmpl.id,
)
.await
.expect("查询应成功");
let fetched =
follow_up_template_service::get_template(app.health_state(), app.tenant_id(), tmpl.id)
.await
.expect("查询应成功");
assert_eq!(fetched.id, tmpl.id);
assert_eq!(fetched.fields.len(), 1);
}
@@ -81,7 +82,9 @@ async fn test_template_update_replace_fields() {
let tmpl = seed_template(&app).await;
let updated = follow_up_template_service::update_template(
app.health_state(), app.tenant_id(), tmpl.id,
app.health_state(),
app.tenant_id(),
tmpl.id,
Some(app.operator_id()),
UpdateFollowUpTemplateReq {
name: Some("更新后的模板".to_string()),
@@ -130,7 +133,9 @@ async fn test_template_list_filter() {
let app = TestApp::new().await;
follow_up_template_service::create_template(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
CreateFollowUpTemplateReq {
name: "门诊随访".to_string(),
follow_up_type: "outpatient".to_string(),
@@ -143,7 +148,9 @@ async fn test_template_list_filter() {
.unwrap();
follow_up_template_service::create_template(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_req(),
)
.await
@@ -151,7 +158,12 @@ async fn test_template_list_filter() {
// 全量
let all = follow_up_template_service::list_templates(
app.health_state(), app.tenant_id(), 1, 20, None, None,
app.health_state(),
app.tenant_id(),
1,
20,
None,
None,
)
.await
.unwrap();
@@ -159,8 +171,12 @@ async fn test_template_list_filter() {
// 按类型过滤
let phone = follow_up_template_service::list_templates(
app.health_state(), app.tenant_id(), 1, 20,
Some("phone".to_string()), None,
app.health_state(),
app.tenant_id(),
1,
20,
Some("phone".to_string()),
None,
)
.await
.unwrap();
@@ -176,16 +192,18 @@ async fn test_template_soft_delete() {
let tmpl = seed_template(&app).await;
follow_up_template_service::delete_template(
app.health_state(), app.tenant_id(), tmpl.id,
Some(app.operator_id()), tmpl.version,
app.health_state(),
app.tenant_id(),
tmpl.id,
Some(app.operator_id()),
tmpl.version,
)
.await
.expect("删除应成功");
let result = follow_up_template_service::get_template(
app.health_state(), app.tenant_id(), tmpl.id,
)
.await;
let result =
follow_up_template_service::get_template(app.health_state(), app.tenant_id(), tmpl.id)
.await;
assert!(result.is_err(), "软删除后查询应失败");
}
@@ -199,7 +217,12 @@ async fn test_template_tenant_isolation() {
let other_tenant = uuid::Uuid::new_v4();
let list = follow_up_template_service::list_templates(
app.health_state(), other_tenant, 1, 20, None, None,
app.health_state(),
other_tenant,
1,
20,
None,
None,
)
.await
.unwrap();
@@ -216,12 +239,17 @@ async fn test_template_version_conflict() {
// 先更新一次
follow_up_template_service::update_template(
app.health_state(), app.tenant_id(), tmpl.id,
app.health_state(),
app.tenant_id(),
tmpl.id,
Some(app.operator_id()),
UpdateFollowUpTemplateReq {
name: Some("第一次".to_string()),
description: None, follow_up_type: None,
applicable_scope: None, status: None, fields: None,
description: None,
follow_up_type: None,
applicable_scope: None,
status: None,
fields: None,
},
tmpl.version,
)
@@ -230,12 +258,17 @@ async fn test_template_version_conflict() {
// 用旧 version 再更新应失败
let result = follow_up_template_service::update_template(
app.health_state(), app.tenant_id(), tmpl.id,
app.health_state(),
app.tenant_id(),
tmpl.id,
Some(app.operator_id()),
UpdateFollowUpTemplateReq {
name: Some("冲突".to_string()),
description: None, follow_up_type: None,
applicable_scope: None, status: None, fields: None,
description: None,
follow_up_type: None,
applicable_scope: None,
status: None,
fields: None,
},
tmpl.version,
)

View File

@@ -27,7 +27,9 @@ async fn test_follow_up_task_create_and_get() {
let patient_id = app.create_patient("随访患者").await;
let task = follow_up_service::create_task(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_task(patient_id),
)
.await
@@ -54,33 +56,51 @@ async fn test_follow_up_task_list_by_patient() {
let patient_b = app.create_patient("列表B").await;
follow_up_service::create_task(
app.health_state(), app.tenant_id(), None,
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,
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,
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,
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,
app.health_state(),
app.tenant_id(),
1,
20,
Some(patient_b),
None,
None,
)
.await
.unwrap();
@@ -96,7 +116,9 @@ async fn test_follow_up_task_status_flow() {
let patient_id = app.create_patient("流转患者").await;
let task = follow_up_service::create_task(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_task(patient_id),
)
.await
@@ -105,10 +127,15 @@ async fn test_follow_up_task_status_flow() {
// pending → in_progress
let started = follow_up_service::update_task(
app.health_state(), app.tenant_id(), task.id, Some(app.operator_id()),
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,
assigned_to: None,
follow_up_type: None,
planned_date: None,
content_template: None,
},
task.version,
@@ -119,10 +146,15 @@ async fn test_follow_up_task_status_flow() {
// in_progress → completed
let completed = follow_up_service::update_task(
app.health_state(), app.tenant_id(), task.id, Some(app.operator_id()),
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,
assigned_to: None,
follow_up_type: None,
planned_date: None,
content_template: None,
},
started.version,
@@ -141,7 +173,9 @@ async fn test_follow_up_task_version_conflict() {
let patient_id = app.create_patient("乐观锁患者").await;
let task = follow_up_service::create_task(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_task(patient_id),
)
.await
@@ -149,10 +183,15 @@ async fn test_follow_up_task_version_conflict() {
// 正确版本更新
follow_up_service::update_task(
app.health_state(), app.tenant_id(), task.id, Some(app.operator_id()),
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,
assigned_to: None,
follow_up_type: None,
planned_date: None,
content_template: None,
},
task.version,
@@ -162,10 +201,15 @@ async fn test_follow_up_task_version_conflict() {
// 旧版本更新应失败
let result = follow_up_service::update_task(
app.health_state(), app.tenant_id(), task.id, Some(app.operator_id()),
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,
assigned_to: None,
follow_up_type: None,
planned_date: None,
content_template: None,
},
task.version,
@@ -183,14 +227,20 @@ async fn test_follow_up_task_soft_delete() {
let patient_id = app.create_patient("软删除患者").await;
let task = follow_up_service::create_task(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
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,
app.health_state(),
app.tenant_id(),
task.id,
Some(app.operator_id()),
task.version,
)
.await
.expect("删除应成功");
@@ -208,7 +258,9 @@ async fn test_follow_up_task_tenant_isolation() {
let patient_id = app.create_patient("隔离患者").await;
let task = follow_up_service::create_task(
app.health_state(), app.tenant_id(), None,
app.health_state(),
app.tenant_id(),
None,
default_create_task(patient_id),
)
.await
@@ -229,7 +281,9 @@ async fn test_follow_up_batch_create() {
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()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
BatchCreateTasksReq {
patient_ids: vec![patient_a, patient_b],
assigned_to: None,
@@ -254,14 +308,18 @@ async fn test_follow_up_record_create() {
let patient_id = app.create_patient("记录患者").await;
let task = follow_up_service::create_task(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
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()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
CreateFollowUpRecordReq {
task_id: task.id,
executed_by: Some(app.operator_id()),
@@ -289,7 +347,9 @@ async fn test_follow_up_task_invalid_patient() {
let fake_patient = uuid::Uuid::new_v4();
let result = follow_up_service::create_task(
app.health_state(), app.tenant_id(), None,
app.health_state(),
app.tenant_id(),
None,
default_create_task(fake_patient),
)
.await;

View File

@@ -26,7 +26,9 @@ fn default_create_medication_req(patient_id: uuid::Uuid) -> CreateMedicationReco
async fn seed_medication(app: &TestApp, patient_id: uuid::Uuid) -> MedicationRecordResp {
medication_record_service::create_medication(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
default_create_medication_req(patient_id),
)
.await
@@ -59,11 +61,10 @@ async fn test_medication_get() {
let patient_id = app.create_patient("用药查询患者").await;
let med = seed_medication(&app, patient_id).await;
let fetched = medication_record_service::get_medication(
app.health_state(), app.tenant_id(), med.id,
)
.await
.expect("查询应成功");
let fetched =
medication_record_service::get_medication(app.health_state(), app.tenant_id(), med.id)
.await
.expect("查询应成功");
assert_eq!(fetched.id, med.id);
assert_eq!(fetched.medication_name, "缬沙坦");
}
@@ -78,14 +79,22 @@ async fn test_medication_update() {
let med = seed_medication(&app, patient_id).await;
let updated = medication_record_service::update_medication(
app.health_state(), app.tenant_id(), med.id,
app.health_state(),
app.tenant_id(),
med.id,
Some(app.operator_id()),
UpdateMedicationRecordReq {
dosage: Some("160mg".to_string()),
is_current: Some(false),
medication_name: None, generic_name: None, unit: None,
frequency: None, route: None, start_date: None,
end_date: None, prescribed_by: None, notes: None,
medication_name: None,
generic_name: None,
unit: None,
frequency: None,
route: None,
start_date: None,
end_date: None,
prescribed_by: None,
notes: None,
},
med.version,
)
@@ -110,14 +119,22 @@ async fn test_medication_list_by_patient() {
seed_medication(&app, patient_b).await;
let list_a = medication_record_service::list_medications(
app.health_state(), app.tenant_id(), patient_a, 1, 20,
app.health_state(),
app.tenant_id(),
patient_a,
1,
20,
)
.await
.unwrap();
assert_eq!(list_a.total, 1);
let list_b = medication_record_service::list_medications(
app.health_state(), app.tenant_id(), patient_b, 1, 20,
app.health_state(),
app.tenant_id(),
patient_b,
1,
20,
)
.await
.unwrap();
@@ -134,16 +151,18 @@ async fn test_medication_soft_delete() {
let med = seed_medication(&app, patient_id).await;
medication_record_service::delete_medication(
app.health_state(), app.tenant_id(), med.id,
Some(app.operator_id()), med.version,
app.health_state(),
app.tenant_id(),
med.id,
Some(app.operator_id()),
med.version,
)
.await
.expect("删除应成功");
let result = medication_record_service::get_medication(
app.health_state(), app.tenant_id(), med.id,
)
.await;
let result =
medication_record_service::get_medication(app.health_state(), app.tenant_id(), med.id)
.await;
assert!(result.is_err(), "软删除后查询应失败");
}
@@ -158,7 +177,11 @@ async fn test_medication_tenant_isolation() {
let other_tenant = uuid::Uuid::new_v4();
let list = medication_record_service::list_medications(
app.health_state(), other_tenant, patient_id, 1, 20,
app.health_state(),
other_tenant,
patient_id,
1,
20,
)
.await
.unwrap();
@@ -176,13 +199,22 @@ async fn test_medication_version_conflict() {
// 先更新一次
medication_record_service::update_medication(
app.health_state(), app.tenant_id(), med.id,
app.health_state(),
app.tenant_id(),
med.id,
Some(app.operator_id()),
UpdateMedicationRecordReq {
dosage: Some("160mg".to_string()),
medication_name: None, generic_name: None, unit: None,
frequency: None, route: None, start_date: None,
end_date: None, is_current: None, prescribed_by: None, notes: None,
medication_name: None,
generic_name: None,
unit: None,
frequency: None,
route: None,
start_date: None,
end_date: None,
is_current: None,
prescribed_by: None,
notes: None,
},
med.version,
)
@@ -191,13 +223,22 @@ async fn test_medication_version_conflict() {
// 用旧 version 再更新应失败
let result = medication_record_service::update_medication(
app.health_state(), app.tenant_id(), med.id,
app.health_state(),
app.tenant_id(),
med.id,
Some(app.operator_id()),
UpdateMedicationRecordReq {
dosage: Some("320mg".to_string()),
medication_name: None, generic_name: None, unit: None,
frequency: None, route: None, start_date: None,
end_date: None, is_current: None, prescribed_by: None, notes: None,
medication_name: None,
generic_name: None,
unit: None,
frequency: None,
route: None,
start_date: None,
end_date: None,
is_current: None,
prescribed_by: None,
notes: None,
},
med.version,
)
@@ -214,7 +255,9 @@ async fn test_medication_invalid_patient() {
let fake_patient = uuid::Uuid::new_v4();
let result = medication_record_service::create_medication(
app.health_state(), app.tenant_id(), None,
app.health_state(),
app.tenant_id(),
None,
default_create_medication_req(fake_patient),
)
.await;

View File

@@ -3,11 +3,11 @@
//! 验证患者 CRUD、租户隔离、字段校验、软删除等核心行为。
//! 使用 TestDb 创建隔离 PostgreSQL 数据库,直接调用 service 层函数。
use erp_core::crypto::PiiCrypto;
use erp_core::events::EventBus;
use erp_health::dto::patient_dto::{CreatePatientReq, UpdatePatientReq};
use erp_health::service::patient_service;
use erp_health::state::HealthState;
use erp_core::crypto::PiiCrypto;
use super::test_db::TestDb;
@@ -70,7 +70,11 @@ async fn test_list_patients() {
for i in 0..2 {
let req = CreatePatientReq {
name: format!("患者{}", i + 1),
gender: if i == 0 { Some("male".to_string()) } else { Some("female".to_string()) },
gender: if i == 0 {
Some("male".to_string())
} else {
Some("female".to_string())
},
birth_date: None,
blood_type: None,
id_number: None,
@@ -128,10 +132,7 @@ async fn test_patient_tenant_isolation() {
// 租户 B 通过 ID 查询租户 A 的患者应返回 PatientNotFound
let lookup_result = patient_service::get_patient(&state, tenant_b, patient_a.id).await;
assert!(
lookup_result.is_err(),
"跨租户查询应返回错误"
);
assert!(lookup_result.is_err(), "跨租户查询应返回错误");
}
#[tokio::test]
@@ -214,27 +215,47 @@ async fn test_patient_update_and_optimistic_lock() {
let tenant_id = uuid::Uuid::new_v4();
let operator_id = uuid::Uuid::new_v4();
let patient = patient_service::create_patient(&state, tenant_id, Some(operator_id), CreatePatientReq {
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,
Some(operator_id),
CreatePatientReq {
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,
},
)
.await
.expect("创建应成功");
// 正确版本更新
let updated = patient_service::update_patient(
&state, tenant_id, patient.id, Some(operator_id),
&state,
tenant_id,
patient.id,
Some(operator_id),
UpdatePatientReq {
name: Some("更新后".to_string()),
gender: None, 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,
status: None, verification_status: None,
gender: None,
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,
status: None,
verification_status: None,
},
patient.version,
)
@@ -245,14 +266,24 @@ async fn test_patient_update_and_optimistic_lock() {
// 旧版本更新应失败
let result = patient_service::update_patient(
&state, tenant_id, patient.id, Some(operator_id),
&state,
tenant_id,
patient.id,
Some(operator_id),
UpdatePatientReq {
name: Some("冲突".to_string()),
gender: None, 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,
status: None, verification_status: None,
gender: None,
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,
status: None,
verification_status: None,
},
patient.version, // 旧版本
)
@@ -266,17 +297,24 @@ async fn test_patient_pii_encrypted() {
let state = make_state(test_db.db());
let tenant_id = uuid::Uuid::new_v4();
let patient = patient_service::create_patient(&state, tenant_id, None, CreatePatientReq {
name: "加密患者".to_string(),
gender: None,
birth_date: None, blood_type: None,
id_number: Some("330102199001011234".to_string()),
allergy_history: Some("花粉过敏".to_string()),
medical_history_summary: Some("高血压".to_string()),
emergency_contact_name: Some("王五".to_string()),
emergency_contact_phone: Some("13900139000".to_string()),
source: None, notes: None,
})
let patient = patient_service::create_patient(
&state,
tenant_id,
None,
CreatePatientReq {
name: "加密患者".to_string(),
gender: None,
birth_date: None,
blood_type: None,
id_number: Some("330102199001011234".to_string()),
allergy_history: Some("花粉过敏".to_string()),
medical_history_summary: Some("高血压".to_string()),
emergency_contact_name: Some("王五".to_string()),
emergency_contact_phone: Some("13900139000".to_string()),
source: None,
notes: None,
},
)
.await
.expect("创建应成功");
@@ -300,20 +338,32 @@ async fn test_patient_search_by_name() {
let tenant_id = uuid::Uuid::new_v4();
for name in &["赵一", "钱二", "孙三"] {
patient_service::create_patient(&state, tenant_id, None, CreatePatientReq {
name: name.to_string(),
gender: None, 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,
})
patient_service::create_patient(
&state,
tenant_id,
None,
CreatePatientReq {
name: name.to_string(),
gender: None,
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,
},
)
.await
.unwrap();
}
let result = patient_service::list_patients(&state, tenant_id, 1, 10, Some("".to_string()), None)
.await
.expect("搜索应成功");
let result =
patient_service::list_patients(&state, tenant_id, 1, 10, Some("".to_string()), None)
.await
.expect("搜索应成功");
assert_eq!(result.total, 1);
assert_eq!(result.data[0].name, "钱二");
}

View File

@@ -68,17 +68,11 @@ async fn test_patient_tier1_fields_encrypted_in_db() {
let stored_id = row.id_number.as_deref().unwrap_or("");
assert_ne!(stored_id, "110101199001151234", "身份证号不应以明文存储");
// AES-GCM 输出为 Base64不应有中文
assert!(
!stored_id.contains("1101"),
"密文不应包含身份证号片段"
);
assert!(!stored_id.contains("1101"), "密文不应包含身份证号片段");
// allergy_history 同理
let stored_allergy = row.allergy_history.as_deref().unwrap_or("");
assert!(
!stored_allergy.contains("青霉素"),
"过敏史不应以明文存储"
);
assert!(!stored_allergy.contains("青霉素"), "过敏史不应以明文存储");
}
// ── 2. Patient: 详情接口返回解密明文 ──
@@ -140,14 +134,8 @@ async fn test_patient_list_hides_tier1_fields() {
assert_eq!(list.data.len(), 1);
let item = &list.data[0];
assert!(
item.id_number.is_none(),
"列表不应返回身份证号"
);
assert!(
item.allergy_history.is_none(),
"列表不应返回过敏史"
);
assert!(item.id_number.is_none(), "列表不应返回身份证号");
assert!(item.allergy_history.is_none(), "列表不应返回过敏史");
assert!(
item.medical_history_summary.is_none(),
"列表不应返回病史摘要"
@@ -409,14 +397,8 @@ async fn test_follow_up_record_fields_encrypted() {
// API 应返回解密后的明文
assert_eq!(record.result, "随访结果:病情稳定");
assert_eq!(
record.patient_condition.as_deref(),
Some("血压控制良好")
);
assert_eq!(
record.medical_advice.as_deref(),
Some("继续服药,定期复查")
);
assert_eq!(record.patient_condition.as_deref(), Some("血压控制良好"));
assert_eq!(record.medical_advice.as_deref(), Some("继续服药,定期复查"));
// DB 中应为密文
let row: Option<erp_health::entity::follow_up_record::Model> =
@@ -489,7 +471,11 @@ async fn test_family_member_phone_encrypted_and_masked() {
.await
.expect("DB 查询应成功");
let row = row.expect("应找到记录");
assert_ne!(row.phone.as_deref(), Some("13987654321"), "DB 中 phone 应为密文");
assert_ne!(
row.phone.as_deref(),
Some("13987654321"),
"DB 中 phone 应为密文"
);
}
// ═══════════════════════════════════════════════════════════════

View File

@@ -20,9 +20,14 @@ async fn seed_rule(app: &TestApp, event_type: &str, points_value: i32) -> Points
streak_14d_bonus: 20,
streak_30d_bonus: 50,
};
points_service::create_rule(app.health_state(), app.tenant_id(), Some(app.operator_id()), req)
.await
.expect("创建规则应成功")
points_service::create_rule(
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
req,
)
.await
.expect("创建规则应成功")
}
/// 创建测试用商品(无限库存)
@@ -37,9 +42,14 @@ async fn seed_product(app: &TestApp, name: &str, points_cost: i32) -> PointsProd
service_config: None,
sort_order: None,
};
points_service::create_product(app.health_state(), app.tenant_id(), Some(app.operator_id()), req)
.await
.expect("创建商品应成功")
points_service::create_product(
app.health_state(),
app.tenant_id(),
Some(app.operator_id()),
req,
)
.await
.expect("创建商品应成功")
}
// ---------------------------------------------------------------------------
@@ -53,7 +63,10 @@ async fn test_points_earn_sign_in() {
// 首次签到
let result = points_service::daily_checkin(
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
Some(app.operator_id()),
)
.await
.expect("签到应成功");
@@ -77,7 +90,11 @@ async fn test_points_earn_custom() {
seed_rule(&app, "custom_event", 20).await;
let tx = points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "custom_event", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"custom_event",
Some(app.operator_id()),
)
.await
.expect("赚取积分应成功");
@@ -100,20 +117,32 @@ async fn test_points_consume_fifo_deduction() {
// 赚两笔: 10 + 30 = 40
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "earn_a", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"earn_a",
Some(app.operator_id()),
)
.await
.unwrap();
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "earn_b", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"earn_b",
Some(app.operator_id()),
)
.await
.unwrap();
// 消费 25: FIFO 先消耗第一笔 10全部用完再从第二笔消耗 15剩 15
let order = points_service::exchange_product(
app.health_state(), app.tenant_id(), patient_id,
ExchangeReq { product_id: product.id },
app.health_state(),
app.tenant_id(),
patient_id,
ExchangeReq {
product_id: product.id,
},
Some(app.operator_id()),
)
.await
@@ -141,15 +170,23 @@ async fn test_points_consume_balance_insufficient() {
// 只赚 5 分
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "small_earn", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"small_earn",
Some(app.operator_id()),
)
.await
.unwrap();
// 消费 100 应失败
let result = points_service::exchange_product(
app.health_state(), app.tenant_id(), patient_id,
ExchangeReq { product_id: product.id },
app.health_state(),
app.tenant_id(),
patient_id,
ExchangeReq {
product_id: product.id,
},
Some(app.operator_id()),
)
.await;
@@ -168,14 +205,22 @@ async fn test_points_consume_exact_balance() {
let product = seed_product(&app, "等价商品", 50).await;
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "exact_earn", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"exact_earn",
Some(app.operator_id()),
)
.await
.unwrap();
let _order = points_service::exchange_product(
app.health_state(), app.tenant_id(), patient_id,
ExchangeReq { product_id: product.id },
app.health_state(),
app.tenant_id(),
patient_id,
ExchangeReq {
product_id: product.id,
},
Some(app.operator_id()),
)
.await
@@ -198,14 +243,22 @@ async fn test_points_consume_partial() {
let product = seed_product(&app, "小商品", 30).await;
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "big_earn", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"big_earn",
Some(app.operator_id()),
)
.await
.unwrap();
let _order = points_service::exchange_product(
app.health_state(), app.tenant_id(), patient_id,
ExchangeReq { product_id: product.id },
app.health_state(),
app.tenant_id(),
patient_id,
ExchangeReq {
product_id: product.id,
},
Some(app.operator_id()),
)
.await
@@ -228,7 +281,11 @@ async fn test_points_account_create_on_first_earn() {
// earn_points 应自动创建账户
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "first_earn", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"first_earn",
Some(app.operator_id()),
)
.await
.unwrap();
@@ -250,17 +307,19 @@ async fn test_points_checkin_streak() {
seed_rule(&app, "daily_checkin", 5).await;
// 连续签到 3 天验证 consecutive_days 递增
let status = points_service::get_checkin_status(
app.health_state(), app.tenant_id(), patient_id,
)
.await
.expect("查询签到状态应成功");
let status =
points_service::get_checkin_status(app.health_state(), app.tenant_id(), patient_id)
.await
.expect("查询签到状态应成功");
assert!(!status.checked_in_today);
assert_eq!(status.consecutive_days, 0);
// 第 1 天签到
let result = points_service::daily_checkin(
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
Some(app.operator_id()),
)
.await
.unwrap();
@@ -279,14 +338,22 @@ async fn test_points_order_create() {
let product = seed_product(&app, "兑换商品", 50).await;
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "order_earn", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"order_earn",
Some(app.operator_id()),
)
.await
.unwrap();
let order = points_service::exchange_product(
app.health_state(), app.tenant_id(), patient_id,
ExchangeReq { product_id: product.id },
app.health_state(),
app.tenant_id(),
patient_id,
ExchangeReq {
product_id: product.id,
},
Some(app.operator_id()),
)
.await
@@ -308,14 +375,22 @@ async fn test_points_order_insufficient_cancel() {
let product = seed_product(&app, "昂贵商品", 999).await;
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "tiny_earn", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"tiny_earn",
Some(app.operator_id()),
)
.await
.unwrap();
let result = points_service::exchange_product(
app.health_state(), app.tenant_id(), patient_id,
ExchangeReq { product_id: product.id },
app.health_state(),
app.tenant_id(),
patient_id,
ExchangeReq {
product_id: product.id,
},
Some(app.operator_id()),
)
.await;
@@ -334,21 +409,28 @@ async fn test_points_transaction_history() {
// 赚两笔
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "history_earn", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"history_earn",
Some(app.operator_id()),
)
.await
.unwrap();
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_id, "history_earn", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_id,
"history_earn",
Some(app.operator_id()),
)
.await
.unwrap();
let history = points_service::list_transactions(
app.health_state(), app.tenant_id(), patient_id, 1, 20,
)
.await
.expect("查询记录应成功");
let history =
points_service::list_transactions(app.health_state(), app.tenant_id(), patient_id, 1, 20)
.await
.expect("查询记录应成功");
assert_eq!(history.total, 2);
assert_eq!(history.data.len(), 2);
@@ -364,7 +446,11 @@ async fn test_points_tenant_isolation() {
seed_rule(&app, "iso_earn", 50).await;
points_service::earn_points(
app.health_state(), app.tenant_id(), patient_a, "iso_earn", Some(app.operator_id()),
app.health_state(),
app.tenant_id(),
patient_a,
"iso_earn",
Some(app.operator_id()),
)
.await
.unwrap();

View File

@@ -132,7 +132,8 @@ async fn test_dynamic_table_create_and_query() {
"sort_order": 1
});
let (sql, values) = DynamicTableManager::build_insert_sql(&table_name, tenant_id, user_id, &data);
let (sql, values) =
DynamicTableManager::build_insert_sql(&table_name, tenant_id, user_id, &data);
db.execute(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
@@ -182,18 +183,31 @@ async fn test_tenant_isolation_in_dynamic_table() {
// 租户 A 插入数据
let data_a = serde_json::json!({"code": "A001", "name": "租户A数据", "status": "active", "sort_order": 1});
let (sql, values) = DynamicTableManager::build_insert_sql(&table_name, tenant_a, user_id, &data_a);
let (sql, values) =
DynamicTableManager::build_insert_sql(&table_name, tenant_a, user_id, &data_a);
db.execute(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres, sql, values,
)).await.unwrap();
sea_orm::DatabaseBackend::Postgres,
sql,
values,
))
.await
.unwrap();
// 租户 B 查询不应看到租户 A 的数据
let (sql, values) = DynamicTableManager::build_query_sql(&table_name, tenant_b, 10, 0);
#[derive(FromQueryResult)]
struct Row { id: uuid::Uuid, data: serde_json::Value }
struct Row {
id: uuid::Uuid,
data: serde_json::Value,
}
let rows = Row::find_by_statement(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres, sql, values,
)).all(db).await.unwrap();
sea_orm::DatabaseBackend::Postgres,
sql,
values,
))
.all(db)
.await
.unwrap();
assert!(rows.is_empty(), "租户 B 不应看到租户 A 的数据");
}

View File

@@ -1,4 +1,4 @@
use sea_orm::{Database, ConnectionTrait, Statement, DatabaseBackend};
use sea_orm::{ConnectionTrait, Database, DatabaseBackend, Statement};
use std::sync::Arc;
use erp_server_migration::MigratorTrait;
@@ -22,7 +22,11 @@ pub struct TestDb {
impl TestDb {
pub async fn new() -> Self {
let permit = db_semaphore().clone().acquire_owned().await.expect("信号量获取失败");
let permit = db_semaphore()
.clone()
.acquire_owned()
.await
.expect("信号量获取失败");
let db_name = format!("erp_test_{}", uuid::Uuid::now_v7().simple());
@@ -58,7 +62,11 @@ impl TestDb {
.await
.expect("执行数据库迁移失败");
Self { db: Some(db), db_name, _permit: Some(permit) }
Self {
db: Some(db),
db_name,
_permit: Some(permit),
}
}
/// 获取数据库连接引用

View File

@@ -1,12 +1,12 @@
use chrono::{NaiveDate, NaiveTime};
use erp_core::crypto::PiiCrypto;
use erp_core::events::EventBus;
use erp_dialysis::state::DialysisState;
use erp_health::dto::appointment_dto::{CreateAppointmentReq, CreateScheduleReq};
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_dialysis::state::DialysisState;
use super::test_db::TestDb;
@@ -78,7 +78,10 @@ impl TestApp {
notes: None,
};
let patient = patient_service::create_patient(
self.health_state(), self.tenant_id, Some(self.operator_id), req,
self.health_state(),
self.tenant_id,
Some(self.operator_id),
req,
)
.await
.expect("创建患者应成功");
@@ -96,18 +99,17 @@ impl TestApp {
bio: None,
};
let doctor = doctor_service::create_doctor(
self.health_state(), self.tenant_id, Some(self.operator_id), req,
self.health_state(),
self.tenant_id,
Some(self.operator_id),
req,
)
.await
.expect("创建医护档案应成功");
doctor.id
}
pub async fn create_schedule(
&self,
doctor_id: uuid::Uuid,
date: NaiveDate,
) -> uuid::Uuid {
pub async fn create_schedule(&self, doctor_id: uuid::Uuid, date: NaiveDate) -> uuid::Uuid {
let req = CreateScheduleReq {
doctor_id,
schedule_date: date,
@@ -117,7 +119,10 @@ impl TestApp {
max_appointments: 10,
};
let schedule = appointment_service::create_schedule(
self.health_state(), self.tenant_id, Some(self.operator_id), req,
self.health_state(),
self.tenant_id,
Some(self.operator_id),
req,
)
.await
.expect("创建排班应成功");
@@ -140,7 +145,10 @@ impl TestApp {
notes: Some("测试预约".to_string()),
};
let appt = appointment_service::create_appointment(
self.health_state(), self.tenant_id, Some(self.operator_id), req,
self.health_state(),
self.tenant_id,
Some(self.operator_id),
req,
)
.await
.expect("创建预约应成功");

View File

@@ -1,8 +1,7 @@
use erp_core::events::EventBus;
use erp_core::types::Pagination;
use erp_workflow::dto::{
CompleteTaskReq, CreateProcessDefinitionReq, EdgeDef, NodeDef, NodeType,
StartInstanceReq,
CompleteTaskReq, CreateProcessDefinitionReq, EdgeDef, NodeDef, NodeType, StartInstanceReq,
};
use erp_workflow::service::definition_service::DefinitionService;
use erp_workflow::service::instance_service::InstanceService;
@@ -12,7 +11,11 @@ use super::test_db::TestDb;
/// 构建一个最简单的线性流程:开始 → 审批 → 结束
/// assignee 指向 operator_id使 list_pending 能查到任务
fn make_simple_definition(name: &str, key: &str, assignee_id: Option<uuid::Uuid>) -> CreateProcessDefinitionReq {
fn make_simple_definition(
name: &str,
key: &str,
assignee_id: Option<uuid::Uuid>,
) -> CreateProcessDefinitionReq {
CreateProcessDefinitionReq {
name: name.to_string(),
key: key.to_string(),
@@ -232,11 +235,8 @@ async fn test_event_bus_pub_sub() {
);
event_bus.broadcast(event);
let other_event = erp_core::events::DomainEvent::new(
"workflow.started",
tenant_id,
serde_json::json!({}),
);
let other_event =
erp_core::events::DomainEvent::new("workflow.started", tenant_id, serde_json::json!({}));
event_bus.broadcast(other_event);
let received = receiver.recv().await;