feat(health): consultation/follow_up 列表 API 内联 patient_name/doctor_name

consultation session list 添加 patient_name/doctor_name,
follow_up task list 添加 patient_name,批量查询消除 N+1。
DTO 新增 Option 字段,向后兼容。
This commit is contained in:
iven
2026-04-27 09:39:46 +08:00
parent 432f6e3554
commit 4a5dbaeaeb
4 changed files with 62 additions and 2 deletions

View File

@@ -8,6 +8,8 @@ pub struct SessionResp {
pub id: Uuid,
pub patient_id: Uuid,
pub doctor_id: Option<Uuid>,
pub patient_name: Option<String>,
pub doctor_name: Option<String>,
pub consultation_type: String,
pub status: String,
pub last_message_at: Option<chrono::DateTime<chrono::Utc>>,

View File

@@ -49,6 +49,7 @@ pub struct FollowUpTaskResp {
pub id: Uuid,
pub patient_id: Uuid,
pub assigned_to: Option<Uuid>,
pub patient_name: Option<String>,
pub follow_up_type: String,
pub planned_date: NaiveDate,
pub status: String,

View File

@@ -12,7 +12,7 @@ use erp_core::error::check_version;
use erp_core::types::PaginatedResponse;
use crate::dto::consultation_dto::*;
use crate::entity::{consultation_message, consultation_session, patient};
use crate::entity::{consultation_message, consultation_session, doctor_profile, patient};
use crate::error::{HealthError, HealthResult};
use crate::service::validation::{validate_sender_role, validate_content_type, validate_consultation_type};
use crate::state::HealthState;
@@ -25,6 +25,7 @@ use erp_core::crypto as pii;
fn model_to_session_resp(m: consultation_session::Model) -> SessionResp {
SessionResp {
id: m.id, patient_id: m.patient_id, doctor_id: m.doctor_id,
patient_name: None, doctor_name: None,
consultation_type: m.consultation_type, status: m.status,
last_message_at: m.last_message_at,
unread_count_patient: m.unread_count_patient,
@@ -216,8 +217,43 @@ pub async fn export_sessions(
.all(&state.db)
.await?;
// 批量查询 patient_name 和 doctor_name
let patient_ids: std::collections::HashSet<Uuid> = models.iter().map(|m| m.patient_id).collect();
let doctor_ids: std::collections::HashSet<Uuid> = models.iter().filter_map(|m| m.doctor_id).collect();
let patient_names: std::collections::HashMap<Uuid, String> = if !patient_ids.is_empty() {
patient::Entity::find()
.filter(patient::Column::Id.is_in(patient_ids))
.filter(patient::Column::TenantId.eq(tenant_id))
.all(&state.db)
.await?
.into_iter()
.map(|p| (p.id, p.name))
.collect()
} else {
std::collections::HashMap::new()
};
let doctor_names: std::collections::HashMap<Uuid, String> = if !doctor_ids.is_empty() {
doctor_profile::Entity::find()
.filter(doctor_profile::Column::Id.is_in(doctor_ids))
.filter(doctor_profile::Column::TenantId.eq(tenant_id))
.all(&state.db)
.await?
.into_iter()
.map(|d| (d.id, d.name))
.collect()
} else {
std::collections::HashMap::new()
};
let total_pages = total.div_ceil(limit.max(1));
let data = models.into_iter().map(model_to_session_resp).collect();
let data = models.into_iter().map(|m| {
let mut resp = model_to_session_resp(m.clone());
resp.patient_name = patient_names.get(&m.patient_id).cloned();
resp.doctor_name = m.doctor_id.and_then(|did| doctor_names.get(&did).cloned());
resp
}).collect();
Ok(PaginatedResponse { data, total, page: page_num, page_size: limit, total_pages })
}

View File

@@ -13,6 +13,7 @@ use erp_core::types::PaginatedResponse;
use crate::dto::follow_up_dto::*;
use crate::entity::{follow_up_record, follow_up_task, patient};
use std::collections::{HashMap, HashSet};
use crate::error::{HealthError, HealthResult};
use crate::service::validation::validate_follow_up_type;
use crate::state::HealthState;
@@ -51,8 +52,25 @@ pub async fn list_tasks(
.await?;
let total_pages = total.div_ceil(limit.max(1));
// 批量查询 patient_name
let patient_ids: HashSet<Uuid> = models.iter().map(|m| m.patient_id).collect();
let patient_names: HashMap<Uuid, String> = if !patient_ids.is_empty() {
patient::Entity::find()
.filter(patient::Column::Id.is_in(patient_ids))
.filter(patient::Column::TenantId.eq(tenant_id))
.all(&state.db)
.await?
.into_iter()
.map(|p| (p.id, p.name))
.collect()
} else {
HashMap::new()
};
let data = models.into_iter().map(|m| FollowUpTaskResp {
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
patient_name: patient_names.get(&m.patient_id).cloned(),
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
status: m.status, content_template: m.content_template,
related_appointment_id: m.related_appointment_id,
@@ -77,6 +95,7 @@ pub async fn get_task(
Ok(FollowUpTaskResp {
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
patient_name: None,
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
status: m.status, content_template: m.content_template,
related_appointment_id: m.related_appointment_id,
@@ -137,6 +156,7 @@ pub async fn create_task(
Ok(FollowUpTaskResp {
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
patient_name: None,
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
status: m.status, content_template: m.content_template,
related_appointment_id: m.related_appointment_id,
@@ -207,6 +227,7 @@ pub async fn update_task(
Ok(FollowUpTaskResp {
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
patient_name: None,
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
status: m.status, content_template: m.content_template,
related_appointment_id: m.related_appointment_id,