diff --git a/crates/erp-health/src/service/stats_service.rs b/crates/erp-health/src/service/stats_service.rs index f6460a8..74332cb 100644 --- a/crates/erp-health/src/service/stats_service.rs +++ b/crates/erp-health/src/service/stats_service.rs @@ -96,32 +96,44 @@ pub async fn get_follow_up_statistics( ) -> AppResult { let db = &state.db; - let total_tasks = follow_up_task::Entity::find() - .filter(follow_up_task::Column::TenantId.eq(tenant_id)) - .filter(follow_up_task::Column::DeletedAt.is_null()) - .count(db) - .await?; + // 单次 GROUP BY 查询替代 4 次独立 COUNT + let sql = r#" + SELECT COALESCE(status, '__total') AS status, COUNT(*) AS cnt + FROM follow_up_task + WHERE tenant_id = $1 AND deleted_at IS NULL + GROUP BY GROUPING SETS ((status), ()) + "#; - let completed = follow_up_task::Entity::find() - .filter(follow_up_task::Column::TenantId.eq(tenant_id)) - .filter(follow_up_task::Column::DeletedAt.is_null()) - .filter(follow_up_task::Column::Status.eq("completed")) - .count(db) - .await?; + #[derive(Debug, sea_orm::FromQueryResult)] + struct StatusCount { + status: String, + cnt: i64, + } - let pending = follow_up_task::Entity::find() - .filter(follow_up_task::Column::TenantId.eq(tenant_id)) - .filter(follow_up_task::Column::DeletedAt.is_null()) - .filter(follow_up_task::Column::Status.eq("pending")) - .count(db) - .await?; + let rows: Vec = sea_orm::FromQueryResult::find_by_statement( + sea_orm::Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + sql, + [tenant_id.into()], + ), + ) + .all(db) + .await?; - let overdue = follow_up_task::Entity::find() - .filter(follow_up_task::Column::TenantId.eq(tenant_id)) - .filter(follow_up_task::Column::DeletedAt.is_null()) - .filter(follow_up_task::Column::Status.eq("overdue")) - .count(db) - .await?; + let mut total_tasks: i64 = 0; + let mut completed: i64 = 0; + let mut pending: i64 = 0; + let mut overdue: i64 = 0; + + for row in &rows { + match row.status.as_str() { + "__total" => total_tasks = row.cnt, + "completed" => completed = row.cnt, + "pending" => pending = row.cnt, + "overdue" => overdue = row.cnt, + _ => {} + } + } let completion_rate = if completed + pending + overdue > 0 { (completed as f64 / (completed + pending + overdue) as f64) * 100.0 @@ -130,10 +142,10 @@ pub async fn get_follow_up_statistics( }; Ok(FollowUpStatisticsResp { - total_tasks: total_tasks as i64, - completed: completed as i64, - pending: pending as i64, - overdue: overdue as i64, + total_tasks, + completed, + pending, + overdue, completion_rate, }) } @@ -420,36 +432,36 @@ struct AvgFieldResult { avg_val: Option, } +macro_rules! avg_field_sql { + ($field:literal) => { + concat!( + "SELECT AVG(", $field, ")::FLOAT8 AS avg_val FROM dialysis_record ", + "WHERE tenant_id = $1 AND deleted_at IS NULL AND ", $field, " IS NOT NULL ", + "AND created_at >= date_trunc('month', NOW())" + ) + }; +} + async fn compute_avg_field( db: &sea_orm::DatabaseConnection, tenant_id: uuid::Uuid, field: &str, ) -> AppResult> { - const ALLOWED_FIELDS: &[&str] = &[ - "ultrafiltration_volume", - "dialysis_duration", - "uf_volume", - "uf_rate", - "blood_flow_rate", - "dialysate_flow_rate", - "pre_weight", - "post_weight", - "pre_bp_systolic", - "pre_bp_diastolic", - "post_bp_systolic", - "post_bp_diastolic", - ]; - if !ALLOWED_FIELDS.contains(&field) { - return Err(erp_core::error::AppError::Validation(format!( - "不允许的字段名: {field}" - ))); - } - // field is whitelist-validated, safe to interpolate - let sql = format!( - "SELECT AVG({field})::FLOAT8 AS avg_val FROM dialysis_record \ - WHERE tenant_id = $1 AND deleted_at IS NULL AND {field} IS NOT NULL \ - AND created_at >= date_trunc('month', NOW())" - ); + let sql = match field { + "ultrafiltration_volume" => avg_field_sql!("ultrafiltration_volume"), + "dialysis_duration" => avg_field_sql!("dialysis_duration"), + "uf_volume" => avg_field_sql!("uf_volume"), + "uf_rate" => avg_field_sql!("uf_rate"), + "blood_flow_rate" => avg_field_sql!("blood_flow_rate"), + "dialysate_flow_rate" => avg_field_sql!("dialysate_flow_rate"), + "pre_weight" => avg_field_sql!("pre_weight"), + "post_weight" => avg_field_sql!("post_weight"), + "pre_bp_systolic" => avg_field_sql!("pre_bp_systolic"), + "pre_bp_diastolic" => avg_field_sql!("pre_bp_diastolic"), + "post_bp_systolic" => avg_field_sql!("post_bp_systolic"), + "post_bp_diastolic" => avg_field_sql!("post_bp_diastolic"), + _ => return Err(erp_core::error::AppError::Validation(format!("不允许的字段名: {field}"))), + }; let result: Option = sea_orm::FromQueryResult::find_by_statement( sea_orm::Statement::from_sql_and_values( sea_orm::DatabaseBackend::Postgres,