功能修复: 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 统一格式化
308 lines
9.8 KiB
Rust
308 lines
9.8 KiB
Rust
//! erp-health 告警系统集成测试
|
||
//!
|
||
//! 验证告警规则 CRUD、引擎评估(阈值/连续/趋势)、告警状态流转、cooldown、租户隔离。
|
||
|
||
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::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 {
|
||
alert_rule_service::create_rule(
|
||
app.health_state(),
|
||
app.tenant_id(),
|
||
app.operator_id(),
|
||
CreateAlertRuleRequest {
|
||
name: "高收缩压".to_string(),
|
||
description: Some("收缩压超过阈值".to_string()),
|
||
device_type: device_type.to_string(),
|
||
condition_type: "single_threshold".to_string(),
|
||
condition_params: serde_json::json!({"direction": "above", "value": threshold}),
|
||
severity: Some("warning".to_string()),
|
||
apply_tags: None,
|
||
notify_roles: None,
|
||
cooldown_minutes: Some(60),
|
||
},
|
||
)
|
||
.await
|
||
.expect("创建规则应成功")
|
||
}
|
||
|
||
/// 插入一条 hourly 汇总记录
|
||
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()),
|
||
patient_id: Set(patient_id),
|
||
device_type: Set(device_type.to_string()),
|
||
hour_start: Set(chrono::Utc::now()),
|
||
min_val: Set(Some(avg_val - 1.0)),
|
||
max_val: Set(Some(avg_val + 1.0)),
|
||
avg_val: Set(avg_val),
|
||
sample_count: Set(1),
|
||
created_at: Set(chrono::Utc::now()),
|
||
updated_at: Set(chrono::Utc::now()),
|
||
deleted_at: Set(None),
|
||
version: Set(1),
|
||
};
|
||
model.insert(app.db()).await.expect("插入 hourly 应成功");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 1: 告警规则 CRUD — 创建 + 列表
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_alert_rule_create_and_list() {
|
||
let app = TestApp::new().await;
|
||
|
||
let rule = seed_threshold_rule(&app, "heart_rate", 140.0).await;
|
||
assert_eq!(rule.name, "高收缩压");
|
||
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("列表应成功");
|
||
assert_eq!(total, 1);
|
||
assert_eq!(rules[0].id, rule.id);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 2: 告警规则停用
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_alert_rule_deactivate() {
|
||
let app = TestApp::new().await;
|
||
|
||
let rule = seed_threshold_rule(&app, "heart_rate", 140.0).await;
|
||
assert!(rule.is_active);
|
||
|
||
let deactivated = alert_rule_service::deactivate_rule(
|
||
app.health_state(),
|
||
app.tenant_id(),
|
||
rule.id,
|
||
rule.version,
|
||
)
|
||
.await
|
||
.expect("停用应成功");
|
||
assert!(!deactivated.is_active);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 3: 单阈值引擎评估 — 超阈值触发告警
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_alert_engine_single_threshold_trigger() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("阈值患者").await;
|
||
|
||
seed_threshold_rule(&app, "heart_rate", 140.0).await;
|
||
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",
|
||
)
|
||
.await
|
||
.expect("评估应成功");
|
||
|
||
assert_eq!(triggered.len(), 1);
|
||
assert_eq!(triggered[0].status, "pending");
|
||
assert_eq!(triggered[0].severity, "warning");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 4: 单阈值引擎评估 — 未超阈值不触发
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_alert_engine_single_threshold_no_trigger() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("正常患者").await;
|
||
|
||
seed_threshold_rule(&app, "heart_rate", 140.0).await;
|
||
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",
|
||
)
|
||
.await
|
||
.expect("评估应成功");
|
||
|
||
assert!(triggered.is_empty());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 5: cooldown — 重复评估不产生重复告警
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_alert_engine_cooldown_suppresses() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("冷却患者").await;
|
||
|
||
seed_threshold_rule(&app, "heart_rate", 140.0).await;
|
||
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",
|
||
)
|
||
.await
|
||
.expect("首次评估应成功");
|
||
assert_eq!(first.len(), 1);
|
||
|
||
// 再次评估,cooldown 内不应重复
|
||
let second = alert_engine::evaluate_rules(
|
||
app.health_state(),
|
||
app.tenant_id(),
|
||
patient_id,
|
||
"heart_rate",
|
||
)
|
||
.await
|
||
.expect("二次评估应成功");
|
||
assert!(second.is_empty(), "cooldown 内不应重复触发");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 6: 告警状态流转 — pending → acknowledged → resolved
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_alert_status_flow() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("流转患者").await;
|
||
|
||
seed_threshold_rule(&app, "heart_rate", 140.0).await;
|
||
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",
|
||
)
|
||
.await
|
||
.unwrap();
|
||
let alert = &triggered[0];
|
||
assert_eq!(alert.status, "pending");
|
||
|
||
// pending → acknowledged
|
||
let acked = alert_service::acknowledge_alert(
|
||
app.health_state(),
|
||
app.tenant_id(),
|
||
alert.id,
|
||
app.operator_id(),
|
||
alert.version,
|
||
)
|
||
.await
|
||
.expect("确认应成功");
|
||
assert_eq!(acked.status, "acknowledged");
|
||
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("解决应成功");
|
||
assert_eq!(resolved.status, "resolved");
|
||
assert!(resolved.resolved_at.is_some());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 7: 告警状态流转 — pending → dismissed
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_alert_dismiss_from_pending() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("忽略患者").await;
|
||
|
||
seed_threshold_rule(&app, "heart_rate", 140.0).await;
|
||
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",
|
||
)
|
||
.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,
|
||
)
|
||
.await
|
||
.expect("忽略应成功");
|
||
assert_eq!(dismissed.status, "dismissed");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 8: 告警列表按患者过滤 + 租户隔离
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_alert_list_filter_and_tenant_isolation() {
|
||
let app = TestApp::new().await;
|
||
let patient_a = app.create_patient("过滤患者A").await;
|
||
let patient_b = app.create_patient("过滤患者B").await;
|
||
|
||
seed_threshold_rule(&app, "heart_rate", 140.0).await;
|
||
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();
|
||
|
||
// 按患者 A 过滤
|
||
let (alerts_a, total_a) = alert_service::list_alerts(
|
||
app.health_state(),
|
||
app.tenant_id(),
|
||
Some(patient_a),
|
||
None,
|
||
None,
|
||
1,
|
||
20,
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(total_a, 1);
|
||
assert_eq!(alerts_a[0].patient_id, patient_a);
|
||
|
||
// 租户隔离
|
||
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,
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(total_other, 0, "不同租户不应看到告警");
|
||
}
|