perf(health): stats_service 合并 COUNT 为 GROUP BY + 宏化 compute_avg_field
get_follow_up_statistics: 4次独立COUNT合并为1次GROUP BY GROUPING SETS。 compute_avg_field: format! 动态拼接改为宏生成静态SQL,利用PG prepared statement缓存。
This commit is contained in:
@@ -96,32 +96,44 @@ pub async fn get_follow_up_statistics(
|
|||||||
) -> AppResult<FollowUpStatisticsResp> {
|
) -> AppResult<FollowUpStatisticsResp> {
|
||||||
let db = &state.db;
|
let db = &state.db;
|
||||||
|
|
||||||
let total_tasks = follow_up_task::Entity::find()
|
// 单次 GROUP BY 查询替代 4 次独立 COUNT
|
||||||
.filter(follow_up_task::Column::TenantId.eq(tenant_id))
|
let sql = r#"
|
||||||
.filter(follow_up_task::Column::DeletedAt.is_null())
|
SELECT COALESCE(status, '__total') AS status, COUNT(*) AS cnt
|
||||||
.count(db)
|
FROM follow_up_task
|
||||||
.await?;
|
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||||
|
GROUP BY GROUPING SETS ((status), ())
|
||||||
|
"#;
|
||||||
|
|
||||||
let completed = follow_up_task::Entity::find()
|
#[derive(Debug, sea_orm::FromQueryResult)]
|
||||||
.filter(follow_up_task::Column::TenantId.eq(tenant_id))
|
struct StatusCount {
|
||||||
.filter(follow_up_task::Column::DeletedAt.is_null())
|
status: String,
|
||||||
.filter(follow_up_task::Column::Status.eq("completed"))
|
cnt: i64,
|
||||||
.count(db)
|
}
|
||||||
.await?;
|
|
||||||
|
|
||||||
let pending = follow_up_task::Entity::find()
|
let rows: Vec<StatusCount> = sea_orm::FromQueryResult::find_by_statement(
|
||||||
.filter(follow_up_task::Column::TenantId.eq(tenant_id))
|
sea_orm::Statement::from_sql_and_values(
|
||||||
.filter(follow_up_task::Column::DeletedAt.is_null())
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
.filter(follow_up_task::Column::Status.eq("pending"))
|
sql,
|
||||||
.count(db)
|
[tenant_id.into()],
|
||||||
.await?;
|
),
|
||||||
|
)
|
||||||
|
.all(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let overdue = follow_up_task::Entity::find()
|
let mut total_tasks: i64 = 0;
|
||||||
.filter(follow_up_task::Column::TenantId.eq(tenant_id))
|
let mut completed: i64 = 0;
|
||||||
.filter(follow_up_task::Column::DeletedAt.is_null())
|
let mut pending: i64 = 0;
|
||||||
.filter(follow_up_task::Column::Status.eq("overdue"))
|
let mut overdue: i64 = 0;
|
||||||
.count(db)
|
|
||||||
.await?;
|
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 {
|
let completion_rate = if completed + pending + overdue > 0 {
|
||||||
(completed as f64 / (completed + pending + overdue) as f64) * 100.0
|
(completed as f64 / (completed + pending + overdue) as f64) * 100.0
|
||||||
@@ -130,10 +142,10 @@ pub async fn get_follow_up_statistics(
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(FollowUpStatisticsResp {
|
Ok(FollowUpStatisticsResp {
|
||||||
total_tasks: total_tasks as i64,
|
total_tasks,
|
||||||
completed: completed as i64,
|
completed,
|
||||||
pending: pending as i64,
|
pending,
|
||||||
overdue: overdue as i64,
|
overdue,
|
||||||
completion_rate,
|
completion_rate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -420,36 +432,36 @@ struct AvgFieldResult {
|
|||||||
avg_val: Option<f64>,
|
avg_val: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
async fn compute_avg_field(
|
||||||
db: &sea_orm::DatabaseConnection,
|
db: &sea_orm::DatabaseConnection,
|
||||||
tenant_id: uuid::Uuid,
|
tenant_id: uuid::Uuid,
|
||||||
field: &str,
|
field: &str,
|
||||||
) -> AppResult<Option<f64>> {
|
) -> AppResult<Option<f64>> {
|
||||||
const ALLOWED_FIELDS: &[&str] = &[
|
let sql = match field {
|
||||||
"ultrafiltration_volume",
|
"ultrafiltration_volume" => avg_field_sql!("ultrafiltration_volume"),
|
||||||
"dialysis_duration",
|
"dialysis_duration" => avg_field_sql!("dialysis_duration"),
|
||||||
"uf_volume",
|
"uf_volume" => avg_field_sql!("uf_volume"),
|
||||||
"uf_rate",
|
"uf_rate" => avg_field_sql!("uf_rate"),
|
||||||
"blood_flow_rate",
|
"blood_flow_rate" => avg_field_sql!("blood_flow_rate"),
|
||||||
"dialysate_flow_rate",
|
"dialysate_flow_rate" => avg_field_sql!("dialysate_flow_rate"),
|
||||||
"pre_weight",
|
"pre_weight" => avg_field_sql!("pre_weight"),
|
||||||
"post_weight",
|
"post_weight" => avg_field_sql!("post_weight"),
|
||||||
"pre_bp_systolic",
|
"pre_bp_systolic" => avg_field_sql!("pre_bp_systolic"),
|
||||||
"pre_bp_diastolic",
|
"pre_bp_diastolic" => avg_field_sql!("pre_bp_diastolic"),
|
||||||
"post_bp_systolic",
|
"post_bp_systolic" => avg_field_sql!("post_bp_systolic"),
|
||||||
"post_bp_diastolic",
|
"post_bp_diastolic" => avg_field_sql!("post_bp_diastolic"),
|
||||||
];
|
_ => return Err(erp_core::error::AppError::Validation(format!("不允许的字段名: {field}"))),
|
||||||
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 result: Option<AvgFieldResult> = sea_orm::FromQueryResult::find_by_statement(
|
let result: Option<AvgFieldResult> = sea_orm::FromQueryResult::find_by_statement(
|
||||||
sea_orm::Statement::from_sql_and_values(
|
sea_orm::Statement::from_sql_and_values(
|
||||||
sea_orm::DatabaseBackend::Postgres,
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
|
|||||||
Reference in New Issue
Block a user