Files
hms/crates/erp-ai/src/service/comparison.rs
iven 5d2402a1e7 feat(ai+health): 闭环核心 — 随访完成→再分析触发 + 前后对比报告
- follow_up.completed 消费者:通过 action_result 反查 AI 建议,触发再分析
- ai.reanalysis.requested 消费者:加载原始建议 baseline
- comparison.rs:对比报告生成引擎(指标变化百分比+趋势判断)
- GET /ai/suggestions/{id}/comparison:前后对比报告 API
- find_by_followup_task:通过随访任务反查关联建议ID
2026-05-01 09:14:13 +08:00

129 lines
4.3 KiB
Rust

use serde::{Deserialize, Serialize};
/// 趋势方向
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrendDirection {
Improving,
Stable,
Worsening,
}
/// 单项指标变化
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricChange {
pub metric: String,
pub baseline_value: f64,
pub current_value: f64,
pub change_percent: f64,
pub trend: TrendDirection,
}
/// 前后对比报告
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComparisonReport {
pub baseline: serde_json::Value,
pub current: serde_json::Value,
pub changes: Vec<MetricChange>,
pub overall_trend: TrendDirection,
}
/// 对比 baseline 和当前数据生成变化报告。
pub fn generate_comparison(
baseline: &serde_json::Value,
current: &serde_json::Value,
) -> ComparisonReport {
let mut changes = Vec::new();
// 提取可比较的数值指标
if let (Some(b_obj), Some(c_obj)) = (baseline.as_object(), current.as_object()) {
for key in b_obj.keys() {
if let (Some(b_val), Some(c_val)) = (b_obj.get(key), c_obj.get(key)) {
if let (Some(b_num), Some(c_num)) = (b_val.as_f64(), c_val.as_f64()) {
let change_pct = if b_num.abs() > 0.0001 {
((c_num - b_num) / b_num.abs()) * 100.0
} else {
0.0
};
let trend = if change_pct.abs() > 5.0 {
TrendDirection::Worsening
} else {
TrendDirection::Stable
};
changes.push(MetricChange {
metric: key.clone(),
baseline_value: b_num,
current_value: c_num,
change_percent: change_pct,
trend,
});
}
}
}
}
// 综合趋势判断
let changed = changes.iter().filter(|c| c.trend == TrendDirection::Worsening).count();
let overall = if changed > 0 {
TrendDirection::Worsening
} else {
TrendDirection::Stable
};
ComparisonReport {
baseline: baseline.clone(),
current: current.clone(),
changes,
overall_trend: overall,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_comparison_detects_significant_change() {
let baseline = serde_json::json!({"systolic_bp": 160.0, "heart_rate": 95.0});
let current = serde_json::json!({"systolic_bp": 130.0, "heart_rate": 80.0});
let report = generate_comparison(&baseline, &current);
assert_eq!(report.overall_trend, TrendDirection::Worsening);
assert_eq!(report.changes.len(), 2);
}
#[test]
fn generate_comparison_detects_single_change() {
let baseline = serde_json::json!({"systolic_bp": 130.0});
let current = serde_json::json!({"systolic_bp": 160.0});
let report = generate_comparison(&baseline, &current);
assert_eq!(report.overall_trend, TrendDirection::Worsening);
}
#[test]
fn generate_comparison_detects_stable() {
let baseline = serde_json::json!({"heart_rate": 75.0});
let current = serde_json::json!({"heart_rate": 76.0});
let report = generate_comparison(&baseline, &current);
assert_eq!(report.overall_trend, TrendDirection::Stable);
}
#[test]
fn generate_comparison_empty_data() {
let baseline = serde_json::json!({});
let current = serde_json::json!({});
let report = generate_comparison(&baseline, &current);
assert_eq!(report.overall_trend, TrendDirection::Stable);
assert!(report.changes.is_empty());
}
#[test]
fn generate_comparison_mixed_metrics() {
let baseline = serde_json::json!({"systolic_bp": 150.0, "heart_rate": 80.0, "spo2": 96.0});
let current = serde_json::json!({"systolic_bp": 140.0, "heart_rate": 95.0, "spo2": 90.0});
let report = generate_comparison(&baseline, &current);
// bp: -6.7% changed, hr: +18.75% changed, spo2: -6.25% changed → has changes
assert_eq!(report.overall_trend, TrendDirection::Worsening);
assert_eq!(report.changes.len(), 3);
}
}