Files
hms/crates/erp-health/src/service/stats_service.rs
iven 2f42ebff1d
Some checks failed
CI / security-audit (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
feat: 仪表盘角色自适应重构 — 4角色视图 + 后端个人工作量API
后端:
- 新增 GET /health/admin/statistics/personal-stats 接口
- PersonalStatsResp: 13个个人维度统计字段
- 按医生/护士/管理员/运营角色聚合工作量数据

前端:
- useDashboardRole hook: 按优先级 doctor>nurse>admin>operator 匹配角色
- DoctorDashboard: 今日工作台(日程/审核/消息/统计卡)
- NurseDashboard: 随访监控台(异常提醒/队列/上报率)
- AdminDashboard: 管理中心(5KPI + 健康数据Tab)
- OperatorDashboard: 运营中心(积分/文章/活动)
- StatisticsDashboard.tsx 重写为角色路由组件
- 删除旧区块:快捷入口/积分排行Top10/最近活动
2026-04-28 07:54:08 +08:00

875 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, sea_query::Expr, FromQueryResult};
use erp_core::error::AppResult;
use crate::dto::stats_dto::*;
use crate::entity::{
patient, consultation_session, follow_up_task,
points_transaction, dialysis_record, lab_report,
appointment, vital_signs, patient_doctor_relation, doctor_profile,
};
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// 基础运营统计
// ---------------------------------------------------------------------------
pub async fn get_patient_statistics(
state: &HealthState,
tenant_id: uuid::Uuid,
) -> AppResult<PatientStatisticsResp> {
let db = &state.db;
let total = patient::Entity::find()
.filter(patient::Column::TenantId.eq(tenant_id))
.filter(patient::Column::DeletedAt.is_null())
.count(db)
.await?;
let new_this_month = patient::Entity::find()
.filter(patient::Column::TenantId.eq(tenant_id))
.filter(patient::Column::DeletedAt.is_null())
.filter(Expr::col(patient::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
.count(db)
.await?;
let new_this_week = patient::Entity::find()
.filter(patient::Column::TenantId.eq(tenant_id))
.filter(patient::Column::DeletedAt.is_null())
.filter(Expr::col(patient::Column::CreatedAt).gte(Expr::cust("date_trunc('week', NOW())")))
.count(db)
.await?;
let active_this_month = points_transaction::Entity::find()
.filter(points_transaction::Column::TenantId.eq(tenant_id))
.filter(Expr::col(points_transaction::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
.count(db)
.await?;
Ok(PatientStatisticsResp {
total_patients: total as i64,
new_this_month: new_this_month as i64,
new_this_week: new_this_week as i64,
active_this_month: active_this_month as i64,
})
}
pub async fn get_consultation_statistics(
state: &HealthState,
tenant_id: uuid::Uuid,
) -> AppResult<ConsultationStatisticsResp> {
let db = &state.db;
let total_sessions = consultation_session::Entity::find()
.filter(consultation_session::Column::TenantId.eq(tenant_id))
.filter(consultation_session::Column::DeletedAt.is_null())
.count(db)
.await?;
let pending_reply = consultation_session::Entity::find()
.filter(consultation_session::Column::TenantId.eq(tenant_id))
.filter(consultation_session::Column::DeletedAt.is_null())
.filter(consultation_session::Column::Status.eq("waiting"))
.count(db)
.await?;
let this_month = consultation_session::Entity::find()
.filter(consultation_session::Column::TenantId.eq(tenant_id))
.filter(consultation_session::Column::DeletedAt.is_null())
.filter(Expr::col(consultation_session::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
.count(db)
.await?;
let avg_response_time_minutes = compute_avg_response_time(db, tenant_id).await?;
Ok(ConsultationStatisticsResp {
total_sessions: total_sessions as i64,
pending_reply: pending_reply as i64,
avg_response_time_minutes,
this_month: this_month as i64,
})
}
pub async fn get_follow_up_statistics(
state: &HealthState,
tenant_id: uuid::Uuid,
) -> AppResult<FollowUpStatisticsResp> {
let db = &state.db;
// 单次 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), ())
"#;
#[derive(Debug, sea_orm::FromQueryResult)]
struct StatusCount {
status: String,
cnt: i64,
}
let rows: Vec<StatusCount> = 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 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
} else {
0.0
};
Ok(FollowUpStatisticsResp {
total_tasks,
completed,
pending,
overdue,
completion_rate,
})
}
#[derive(Debug, FromQueryResult)]
struct AvgResponseTime {
avg_minutes: Option<f64>,
}
async fn compute_avg_response_time(
db: &sea_orm::DatabaseConnection,
tenant_id: uuid::Uuid,
) -> AppResult<Option<f64>> {
let sql = r#"
SELECT AVG(EXTRACT(EPOCH FROM (m.created_at - s.created_at)) / 60) AS avg_minutes
FROM consultation_session s
INNER JOIN consultation_message m ON m.session_id = s.id AND m.tenant_id = $1 AND m.deleted_at IS NULL
WHERE s.tenant_id = $1 AND s.deleted_at IS NULL
AND m.sender_role = 'doctor'
"#;
let result: Option<AvgResponseTime> = sea_orm::FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
[tenant_id.into()],
),
)
.one(db)
.await?;
Ok(result.and_then(|r| r.avg_minutes))
}
// ---------------------------------------------------------------------------
// 健康数据统计
// ---------------------------------------------------------------------------
pub async fn get_dialysis_statistics(
state: &HealthState,
tenant_id: uuid::Uuid,
) -> AppResult<DialysisStatisticsResp> {
let db = &state.db;
let total_records = dialysis_record::Entity::find()
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
.filter(dialysis_record::Column::DeletedAt.is_null())
.count(db)
.await?;
let this_month = dialysis_record::Entity::find()
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
.filter(dialysis_record::Column::DeletedAt.is_null())
.filter(Expr::col(dialysis_record::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
.count(db)
.await?;
let pending_review = dialysis_record::Entity::find()
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
.filter(dialysis_record::Column::DeletedAt.is_null())
.filter(dialysis_record::Column::Status.eq("draft"))
.count(db)
.await?;
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: total_records as i64,
this_month: this_month as i64,
type_distribution,
complication_rate,
avg_ultrafiltration,
avg_duration,
pending_review: pending_review as i64,
})
}
pub async fn get_lab_report_statistics(
state: &HealthState,
tenant_id: uuid::Uuid,
) -> AppResult<LabReportStatisticsResp> {
let db = &state.db;
let total_reports = lab_report::Entity::find()
.filter(lab_report::Column::TenantId.eq(tenant_id))
.filter(lab_report::Column::DeletedAt.is_null())
.count(db)
.await?;
let this_month = lab_report::Entity::find()
.filter(lab_report::Column::TenantId.eq(tenant_id))
.filter(lab_report::Column::DeletedAt.is_null())
.filter(Expr::col(lab_report::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
.count(db)
.await?;
let pending_review = lab_report::Entity::find()
.filter(lab_report::Column::TenantId.eq(tenant_id))
.filter(lab_report::Column::DeletedAt.is_null())
.filter(lab_report::Column::Status.eq("pending"))
.count(db)
.await?;
let reviewed = lab_report::Entity::find()
.filter(lab_report::Column::TenantId.eq(tenant_id))
.filter(lab_report::Column::DeletedAt.is_null())
.filter(lab_report::Column::Status.eq("reviewed"))
.count(db)
.await?;
let type_distribution = count_by_field(
db, tenant_id,
"SELECT report_type AS name, COUNT(*) AS value FROM lab_report \
WHERE tenant_id = $1 AND deleted_at IS NULL \
AND created_at >= date_trunc('month', NOW()) \
GROUP BY report_type ORDER BY value DESC",
).await?;
let abnormal_items = count_abnormal_lab_items(db, tenant_id).await?;
Ok(LabReportStatisticsResp {
total_reports: total_reports as i64,
this_month: this_month as i64,
type_distribution,
abnormal_items,
pending_review: pending_review as i64,
reviewed: reviewed as i64,
})
}
pub async fn get_appointment_statistics(
state: &HealthState,
tenant_id: uuid::Uuid,
) -> AppResult<AppointmentStatisticsResp> {
let db = &state.db;
let total_appointments = appointment::Entity::find()
.filter(appointment::Column::TenantId.eq(tenant_id))
.filter(appointment::Column::DeletedAt.is_null())
.count(db)
.await?;
let this_month = appointment::Entity::find()
.filter(appointment::Column::TenantId.eq(tenant_id))
.filter(appointment::Column::DeletedAt.is_null())
.filter(Expr::col(appointment::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
.count(db)
.await?;
let status_distribution = count_by_field(
db, tenant_id,
"SELECT status AS name, COUNT(*) AS value FROM appointment \
WHERE tenant_id = $1 AND deleted_at IS NULL \
AND created_at >= date_trunc('month', NOW()) \
GROUP BY status ORDER BY value DESC",
).await?;
let type_distribution = count_by_field(
db, tenant_id,
"SELECT appointment_type AS name, COUNT(*) AS value FROM appointment \
WHERE tenant_id = $1 AND deleted_at IS NULL \
AND created_at >= date_trunc('month', NOW()) \
GROUP BY appointment_type ORDER BY value DESC",
).await?;
let cancelled = appointment::Entity::find()
.filter(appointment::Column::TenantId.eq(tenant_id))
.filter(appointment::Column::DeletedAt.is_null())
.filter(Expr::col(appointment::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
.filter(appointment::Column::Status.eq("cancelled"))
.count(db)
.await?;
let cancel_rate = if this_month > 0 {
(cancelled as f64 / this_month as f64) * 100.0
} else {
0.0
};
Ok(AppointmentStatisticsResp {
total_appointments: total_appointments as i64,
this_month: this_month as i64,
status_distribution,
type_distribution,
cancel_rate,
})
}
pub async fn get_vital_signs_report_rate(
state: &HealthState,
tenant_id: uuid::Uuid,
) -> AppResult<VitalSignsReportRateResp> {
let db = &state.db;
let total_patients = patient::Entity::find()
.filter(patient::Column::TenantId.eq(tenant_id))
.filter(patient::Column::DeletedAt.is_null())
.count(db)
.await?;
let total_records = vital_signs::Entity::find()
.filter(vital_signs::Column::TenantId.eq(tenant_id))
.filter(vital_signs::Column::DeletedAt.is_null())
.filter(Expr::col(vital_signs::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
.count(db)
.await?;
let reported_patients = count_distinct_patients_vital_signs(db, tenant_id).await?;
let report_rate = if total_patients > 0 {
(reported_patients as f64 / total_patients as f64) * 100.0
} else {
0.0
};
let daily_trend = compute_daily_report_rate(db, tenant_id).await?;
Ok(VitalSignsReportRateResp {
total_patients: total_patients as i64,
reported_patients: reported_patients as i64,
report_rate,
total_records: total_records as i64,
daily_trend,
})
}
pub async fn get_health_data_stats(
state: &HealthState,
tenant_id: uuid::Uuid,
) -> AppResult<HealthDataStatsResp> {
let dialysis = get_dialysis_statistics(state, tenant_id).await?;
let lab_reports = get_lab_report_statistics(state, tenant_id).await?;
let appointments = get_appointment_statistics(state, tenant_id).await?;
let vital_signs_report_rate = get_vital_signs_report_rate(state, tenant_id).await?;
Ok(HealthDataStatsResp {
dialysis,
lab_reports,
appointments,
vital_signs_report_rate,
})
}
// ---------------------------------------------------------------------------
// 辅助查询
// ---------------------------------------------------------------------------
#[derive(Debug, FromQueryResult)]
struct NameValueRow {
name: String,
value: i64,
}
async fn count_by_field(
db: &sea_orm::DatabaseConnection,
tenant_id: uuid::Uuid,
sql: &str,
) -> AppResult<Vec<NameValue>> {
let rows: Vec<NameValueRow> = sea_orm::FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::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::Uuid,
field: &str,
) -> AppResult<Option<f64>> {
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<AvgFieldResult> = sea_orm::FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::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::Uuid,
) -> AppResult<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> = sea_orm::FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::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,
})
}
async fn count_abnormal_lab_items(
db: &sea_orm::DatabaseConnection,
tenant_id: uuid::Uuid,
) -> AppResult<i64> {
let sql = r#"
SELECT COALESCE(SUM(jsonb_array_length(
COALESCE(
(SELECT jsonb_agg(elem) FROM jsonb_array_elements(items) elem WHERE elem->>'is_abnormal' = 'true'),
'[]'::jsonb
)
)), 0) AS total
FROM lab_report
WHERE tenant_id = $1 AND deleted_at IS NULL AND items IS NOT NULL
AND created_at >= date_trunc('month', NOW())
"#;
#[derive(Debug, FromQueryResult)]
struct AbnormalCount {
total: Option<i64>,
}
let result: Option<AbnormalCount> = sea_orm::FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
[tenant_id.into()],
),
)
.one(db)
.await?;
Ok(result.and_then(|r| r.total).unwrap_or(0))
}
async fn count_distinct_patients_vital_signs(
db: &sea_orm::DatabaseConnection,
tenant_id: uuid::Uuid,
) -> AppResult<u64> {
let sql = r#"
SELECT COUNT(DISTINCT patient_id) AS cnt
FROM vital_signs
WHERE tenant_id = $1 AND deleted_at IS NULL
AND created_at >= date_trunc('month', NOW())
"#;
#[derive(Debug, FromQueryResult)]
struct DistinctCount {
cnt: i64,
}
let result: Option<DistinctCount> = sea_orm::FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
[tenant_id.into()],
),
)
.one(db)
.await?;
Ok(result.map(|r| r.cnt as u64).unwrap_or(0))
}
async fn compute_daily_report_rate(
db: &sea_orm::DatabaseConnection,
tenant_id: uuid::Uuid,
) -> AppResult<Vec<DailyReportRate>> {
let sql = r#"
SELECT d::date::text AS date,
COUNT(DISTINCT vs.patient_id) AS reported,
0 AS total
FROM generate_series(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE,
INTERVAL '1 day'
) d
LEFT JOIN vital_signs vs ON vs.record_date = d::date
AND vs.tenant_id = $1 AND vs.deleted_at IS NULL
GROUP BY d::date
ORDER BY d::date
"#;
#[derive(Debug, FromQueryResult)]
struct DailyRow {
date: String,
reported: i64,
total: i64,
}
let rows: Vec<DailyRow> = 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 total_patients = patient::Entity::find()
.filter(patient::Column::TenantId.eq(tenant_id))
.filter(patient::Column::DeletedAt.is_null())
.count(db)
.await?;
Ok(rows.into_iter().map(|r| {
let total = total_patients as i64;
let rate = if total > 0 { (r.reported as f64 / total as f64) * 100.0 } else { 0.0 };
DailyReportRate { date: r.date, reported: r.reported, total, rate }
}).collect())
}
// ---------------------------------------------------------------------------
// 个人维度统计
// ---------------------------------------------------------------------------
pub async fn get_personal_stats(
state: &HealthState,
user_id: uuid::Uuid,
tenant_id: uuid::Uuid,
) -> AppResult<PersonalStatsResp> {
let db = &state.db;
// 通过 user_id 查找 doctor_profile 以获得 doctor_id
let doctor_profile = doctor_profile::Entity::find()
.filter(doctor_profile::Column::TenantId.eq(tenant_id))
.filter(doctor_profile::Column::DeletedAt.is_null())
.filter(doctor_profile::Column::UserId.eq(user_id))
.one(db)
.await?;
let doctor_id = doctor_profile.map(|p| p.id);
// my_patients: 通过 patient_doctor_relation 统计
let my_patients = if let Some(did) = doctor_id {
patient_doctor_relation::Entity::find()
.filter(patient_doctor_relation::Column::TenantId.eq(tenant_id))
.filter(patient_doctor_relation::Column::DeletedAt.is_null())
.filter(patient_doctor_relation::Column::DoctorId.eq(did))
.count(db)
.await? as i64
} else {
0
};
// new_patients_this_month: 本月新增关联患者
let new_patients_this_month = if let Some(did) = doctor_id {
let sql = r#"
SELECT COUNT(*) AS cnt
FROM patient_doctor_relation pdr
INNER JOIN patient p ON p.id = pdr.patient_id AND p.deleted_at IS NULL AND p.tenant_id = $1
WHERE pdr.tenant_id = $1 AND pdr.deleted_at IS NULL
AND pdr.doctor_id = $2
AND p.created_at >= date_trunc('month', NOW())
"#;
#[derive(Debug, FromQueryResult)]
struct Cnt {
cnt: i64,
}
let result: Option<Cnt> = FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
[tenant_id.into(), did.into()],
),
)
.one(db)
.await?;
result.map(|r| r.cnt).unwrap_or(0)
} else {
0
};
// follow_up_rate: 分配给当前用户的随访完成率
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 AND assigned_to = $2
GROUP BY GROUPING SETS ((status), ())
"#;
#[derive(Debug, FromQueryResult)]
struct StatusCount {
status: String,
cnt: i64,
}
let fu_rows: Vec<StatusCount> = FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
[tenant_id.into(), user_id.into()],
),
)
.all(db)
.await?;
let mut fu_total: i64 = 0;
let mut fu_completed: i64 = 0;
let mut overdue_follow_ups: i64 = 0;
for row in &fu_rows {
match row.status.as_str() {
"__total" => fu_total = row.cnt,
"completed" => fu_completed = row.cnt,
"overdue" => overdue_follow_ups = row.cnt,
_ => {}
}
}
let follow_up_rate = if fu_total > 0 {
(fu_completed as f64 / fu_total as f64) * 100.0
} else {
0.0
};
// consultations_this_month / pending_consultations: 咨询统计
let consultations_this_month = if let Some(did) = doctor_id {
consultation_session::Entity::find()
.filter(consultation_session::Column::TenantId.eq(tenant_id))
.filter(consultation_session::Column::DeletedAt.is_null())
.filter(consultation_session::Column::DoctorId.eq(did))
.filter(Expr::col(consultation_session::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
.count(db)
.await? as i64
} else {
0
};
let pending_consultations = if let Some(did) = doctor_id {
consultation_session::Entity::find()
.filter(consultation_session::Column::TenantId.eq(tenant_id))
.filter(consultation_session::Column::DeletedAt.is_null())
.filter(consultation_session::Column::DoctorId.eq(did))
.filter(consultation_session::Column::Status.eq("active"))
.count(db)
.await? as i64
} else {
0
};
// today_appointments: 今日预约
let today_appointments = if let Some(did) = doctor_id {
appointment::Entity::find()
.filter(appointment::Column::TenantId.eq(tenant_id))
.filter(appointment::Column::DeletedAt.is_null())
.filter(appointment::Column::DoctorId.eq(did))
.filter(Expr::col(appointment::Column::AppointmentDate).eq(Expr::cust("CURRENT_DATE")))
.count(db)
.await? as i64
} else {
0
};
// today_follow_ups: 今日随访任务
let today_follow_ups = 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::AssignedTo.eq(user_id))
.filter(Expr::col(follow_up_task::Column::PlannedDate).eq(Expr::cust("CURRENT_DATE")))
.count(db)
.await? as i64;
// vital_signs_report_rate: 当前医生的患者体征上报率
let (vital_signs_reported, vital_signs_total, vital_signs_report_rate) = if my_patients > 0 {
let vs_sql = r#"
SELECT
COUNT(DISTINCT vs.patient_id) AS reported,
$3::bigint AS total
FROM vital_signs vs
WHERE vs.tenant_id = $1 AND vs.deleted_at IS NULL
AND vs.created_at >= date_trunc('month', NOW())
AND vs.patient_id IN (
SELECT patient_id FROM patient_doctor_relation
WHERE doctor_id = $2 AND tenant_id = $1 AND deleted_at IS NULL
)
"#;
#[derive(Debug, FromQueryResult)]
struct VsCount {
reported: i64,
total: i64,
}
let result: Option<VsCount> = FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
vs_sql,
[tenant_id.into(), doctor_id.unwrap_or_default().into(), my_patients.into()],
),
)
.one(db)
.await?;
match result {
Some(r) => {
let rate = if r.total > 0 {
(r.reported as f64 / r.total as f64) * 100.0
} else {
0.0
};
(r.reported, r.total, rate)
}
None => (0, my_patients, 0.0),
}
} else {
(0, 0, 0.0)
};
// pending_lab_reviews: 待审核化验报告(与当前医生的患者关联)
let pending_lab_reviews = if doctor_id.is_some() {
let lr_sql = r#"
SELECT COUNT(*) AS cnt
FROM lab_report lr
WHERE lr.tenant_id = $1 AND lr.deleted_at IS NULL
AND lr.status = 'pending'
AND lr.patient_id IN (
SELECT patient_id FROM patient_doctor_relation
WHERE doctor_id = $2 AND tenant_id = $1 AND deleted_at IS NULL
)
"#;
#[derive(Debug, FromQueryResult)]
struct LrCnt {
cnt: i64,
}
let result: Option<LrCnt> = FromQueryResult::find_by_statement(
sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
lr_sql,
[tenant_id.into(), doctor_id.unwrap_or_default().into()],
),
)
.one(db)
.await?;
result.map(|r| r.cnt).unwrap_or(0)
} else {
0
};
// abnormal_vital_signs: 简化实现,返回 0完整实现需要关联危急值阈值配置
let abnormal_vital_signs: i64 = 0;
Ok(PersonalStatsResp {
my_patients,
new_patients_this_month,
follow_up_rate,
consultations_this_month,
pending_consultations,
vital_signs_report_rate,
today_appointments,
overdue_follow_ups,
today_follow_ups,
abnormal_vital_signs,
vital_signs_reported,
vital_signs_total,
pending_lab_reviews,
})
}