refactor(dialysis+health): 透析统计从 erp-health 迁移到 erp-dialysis,消除跨 crate 残留
- erp-dialysis: 新建 dialysis_stats_dto/handler/service,注册 /health/admin/statistics/dialysis 路由 - erp-health: 删除 get_dialysis_statistics 及 helper、DialysisStatisticsResp、 DialysisRecordNotFound/DialysisPrescriptionNotFound、validate_dialysis_status* 及 9 个测试、 DoctorDashboard.pending_dialysis_review、module 路由 - Web: HealthDataStats 移除 dialysis 字段,新增 getDialysisStats() 独立 API, useStatsData 并行 fetch,HealthDataCenter 接受独立 dialysisData prop - 小程序: DoctorDashboard 移除 pending_dialysis_review,医护工作台移除"待审透析"卡片
This commit is contained in:
24
crates/erp-dialysis/src/dto/dialysis_stats_dto.rs
Normal file
24
crates/erp-dialysis/src/dto/dialysis_stats_dto.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct DialysisStatisticsResp {
|
||||
pub total_records: i64,
|
||||
pub this_month: i64,
|
||||
/// 本月各透析类型分布
|
||||
pub type_distribution: Vec<NameValue>,
|
||||
/// 本月并发症发生率 (%)
|
||||
pub complication_rate: f64,
|
||||
/// 平均超滤量 (ml)
|
||||
pub avg_ultrafiltration: Option<f64>,
|
||||
/// 平均透析时长 (分钟)
|
||||
pub avg_duration: Option<f64>,
|
||||
/// 待审核数量
|
||||
pub pending_review: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct NameValue {
|
||||
pub name: String,
|
||||
pub value: i64,
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod dialysis_dto;
|
||||
pub mod dialysis_prescription_dto;
|
||||
pub mod dialysis_stats_dto;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct DeleteWithVersion {
|
||||
|
||||
23
crates/erp-dialysis/src/handler/dialysis_stats_handler.rs
Normal file
23
crates/erp-dialysis/src/handler/dialysis_stats_handler.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use axum::extract::{Extension, FromRef, State};
|
||||
use axum::Json;
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
|
||||
use crate::dto::dialysis_stats_dto::DialysisStatisticsResp;
|
||||
use crate::service::dialysis_stats_service;
|
||||
use crate::state::DialysisState;
|
||||
|
||||
pub async fn get_dialysis_stats<S>(
|
||||
State(state): State<S>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
) -> Result<Json<ApiResponse<DialysisStatisticsResp>>, AppError>
|
||||
where
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.dialysis.list")?;
|
||||
let dialysis_state = DialysisState::from_ref(&state);
|
||||
let stats = dialysis_stats_service::get_dialysis_statistics(&dialysis_state, ctx.tenant_id).await?;
|
||||
Ok(Json(ApiResponse::ok(stats)))
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod dialysis_handler;
|
||||
pub mod dialysis_prescription_handler;
|
||||
pub mod dialysis_stats_handler;
|
||||
|
||||
@@ -3,7 +3,7 @@ use axum::Router;
|
||||
use erp_core::error::AppResult;
|
||||
use erp_core::module::{ErpModule, ModuleContext, ModuleType, PermissionDescriptor};
|
||||
|
||||
use crate::handler::{dialysis_handler, dialysis_prescription_handler};
|
||||
use crate::handler::{dialysis_handler, dialysis_prescription_handler, dialysis_stats_handler};
|
||||
use crate::state::DialysisState;
|
||||
|
||||
pub struct DialysisModule;
|
||||
@@ -54,6 +54,11 @@ impl DialysisModule {
|
||||
.put(dialysis_prescription_handler::update_prescription)
|
||||
.delete(dialysis_prescription_handler::delete_prescription),
|
||||
)
|
||||
// 透析统计
|
||||
.route(
|
||||
"/health/admin/statistics/dialysis",
|
||||
axum::routing::get(dialysis_stats_handler::get_dialysis_stats),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
134
crates/erp-dialysis/src/service/dialysis_stats_service.rs
Normal file
134
crates/erp-dialysis/src/service/dialysis_stats_service.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use sea_orm::{DatabaseBackend, FromQueryResult, Statement};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dto::dialysis_stats_dto::{DialysisStatisticsResp, NameValue};
|
||||
use crate::error::{DialysisResult, DialysisError};
|
||||
use crate::state::DialysisState;
|
||||
|
||||
pub async fn get_dialysis_statistics(
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
) -> DialysisResult<DialysisStatisticsResp> {
|
||||
let db = &state.db;
|
||||
|
||||
#[derive(FromQueryResult)]
|
||||
struct CountRow { count: i64 }
|
||||
|
||||
let total_records = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL",
|
||||
[tenant_id.into()],
|
||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||
|
||||
let this_month = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND created_at >= date_trunc('month', NOW())",
|
||||
[tenant_id.into()],
|
||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||
|
||||
let pending_review = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND status = 'draft'",
|
||||
[tenant_id.into()],
|
||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||
|
||||
let type_distribution = count_by_field(
|
||||
db, tenant_id,
|
||||
"SELECT dialysis_type AS name, COUNT(*) AS value FROM dialysis_record \
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL \
|
||||
AND created_at >= date_trunc('month', NOW()) \
|
||||
GROUP BY dialysis_type ORDER BY value DESC",
|
||||
).await?;
|
||||
|
||||
let complication_rate = compute_complication_rate(db, tenant_id).await?;
|
||||
let avg_ultrafiltration = compute_avg_field(db, tenant_id, "ultrafiltration_volume").await?;
|
||||
let avg_duration = compute_avg_field(db, tenant_id, "dialysis_duration").await?;
|
||||
|
||||
Ok(DialysisStatisticsResp {
|
||||
total_records,
|
||||
this_month,
|
||||
type_distribution,
|
||||
complication_rate,
|
||||
avg_ultrafiltration,
|
||||
avg_duration,
|
||||
pending_review,
|
||||
})
|
||||
}
|
||||
|
||||
async fn count_by_field(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
sql: &str,
|
||||
) -> DialysisResult<Vec<NameValue>> {
|
||||
#[derive(FromQueryResult)]
|
||||
struct NameValueRow { name: String, value: i64 }
|
||||
|
||||
let rows: Vec<NameValueRow> = FromQueryResult::find_by_statement(
|
||||
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
||||
)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(|r| NameValue { name: r.name, value: r.value }).collect())
|
||||
}
|
||||
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct AvgFieldResult { 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(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
field: &str,
|
||||
) -> DialysisResult<Option<f64>> {
|
||||
let sql = match field {
|
||||
"ultrafiltration_volume" => avg_field_sql!("ultrafiltration_volume"),
|
||||
"dialysis_duration" => avg_field_sql!("dialysis_duration"),
|
||||
"blood_flow_rate" => avg_field_sql!("blood_flow_rate"),
|
||||
_ => return Err(DialysisError::Validation(format!("不允许的字段名: {field}"))),
|
||||
};
|
||||
let result: Option<AvgFieldResult> = FromQueryResult::find_by_statement(
|
||||
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
Ok(result.and_then(|r| r.avg_val))
|
||||
}
|
||||
|
||||
async fn compute_complication_rate(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
) -> DialysisResult<f64> {
|
||||
let sql = r#"
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE complication_notes IS NOT NULL AND complication_notes != '') AS with_comp,
|
||||
COUNT(*) AS total
|
||||
FROM dialysis_record
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
AND created_at >= date_trunc('month', NOW())
|
||||
"#;
|
||||
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct CompResult { with_comp: i64, total: i64 }
|
||||
|
||||
let result: Option<CompResult> = FromQueryResult::find_by_statement(
|
||||
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
Ok(match result {
|
||||
Some(r) if r.total > 0 => (r.with_comp as f64 / r.total as f64) * 100.0,
|
||||
_ => 0.0,
|
||||
})
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod dialysis_service;
|
||||
pub mod dialysis_prescription_service;
|
||||
pub mod dialysis_stats_service;
|
||||
|
||||
Reference in New Issue
Block a user