use axum::Extension; use axum::extract::{FromRef, Json, Path, Query, State}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use serde::Deserialize; use utoipa::IntoParams; use uuid::Uuid; use erp_core::error::AppError; use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext}; use crate::dto::consultation_dto::*; use crate::service::consultation_service; use crate::state::HealthState; #[derive(Debug, Deserialize, IntoParams)] pub struct SessionListParams { pub page: Option, pub page_size: Option, pub status: Option, pub patient_id: Option, pub doctor_id: Option, } #[derive(Debug, Deserialize, IntoParams)] pub struct MessageListParams { pub page: Option, pub page_size: Option, pub after_id: Option, } #[derive(Debug, Deserialize, IntoParams)] pub struct PollMessagesParams { pub after_id: Option, /// 超时秒数,默认 25,最大 30 pub timeout: Option, } #[derive(Debug, serde::Deserialize, utoipa::ToSchema)] pub struct CloseSessionReq { pub version: i32, } #[derive(Debug, serde::Deserialize, utoipa::ToSchema)] pub struct CreateConsultationMessageReq { pub session_id: Uuid, pub content_type: Option, pub content: String, } #[derive(Debug, Deserialize, IntoParams)] pub struct ExportSessionsParams { pub status: Option, pub patient_id: Option, pub doctor_id: Option, pub page: Option, pub page_size: Option, } pub async fn create_session( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> Result>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.manage")?; let result = consultation_service::create_session(&state, ctx.tenant_id, Some(ctx.user_id), req).await?; Ok(Json(ApiResponse::ok(result))) } pub async fn list_sessions( State(state): State, Extension(ctx): Extension, Query(params): Query, ) -> Result>>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.list")?; let page = params.page.unwrap_or(1); let page_size = params.page_size.unwrap_or(20); let result = consultation_service::list_sessions( &state, ctx.tenant_id, page, page_size, params.status, params.patient_id, params.doctor_id, ) .await?; Ok(Json(ApiResponse::ok(result))) } pub async fn get_session( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.list")?; let result = consultation_service::get_session(&state, ctx.tenant_id, id).await?; Ok(Json(ApiResponse::ok(result))) } pub async fn list_messages( State(state): State, Extension(ctx): Extension, Path(session_id): Path, Query(params): Query, ) -> Result>>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.list")?; let page = params.page.unwrap_or(1); let page_size = params.page_size.unwrap_or(20); let result = consultation_service::list_messages( &state, ctx.tenant_id, session_id, page, page_size, params.after_id, ) .await?; Ok(Json(ApiResponse::ok(result))) } /// 长轮询咨询消息 — 有新消息立即返回,否则挂起等待(最多 timeout 秒)。 pub async fn poll_messages( State(state): State, Extension(ctx): Extension, Path(session_id): Path, Query(params): Query, ) -> Result>>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.list")?; let timeout_secs = params.timeout.unwrap_or(25).min(30); let result = consultation_service::poll_new_messages( &state, ctx.tenant_id, session_id, params.after_id, timeout_secs, ) .await?; Ok(Json(ApiResponse::ok(result))) } pub async fn close_session( State(state): State, Extension(ctx): Extension, Path(id): Path, Json(req): Json, ) -> Result>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.manage")?; let result = consultation_service::close_session( &state, ctx.tenant_id, id, Some(ctx.user_id), req.version, ) .await?; Ok(Json(ApiResponse::ok(result))) } pub async fn create_message( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> Result>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.manage")?; // 从 JWT 身份推导 sender_role,不信任客户端输入 let is_doctor = crate::entity::doctor_profile::Entity::find() .filter(crate::entity::doctor_profile::Column::UserId.eq(ctx.user_id)) .filter(crate::entity::doctor_profile::Column::TenantId.eq(ctx.tenant_id)) .filter(crate::entity::doctor_profile::Column::DeletedAt.is_null()) .one(&state.db) .await .map_err(|e| AppError::Internal(e.to_string()))? .is_some(); let sender_role = if is_doctor { "doctor" } else { "patient" }.to_string(); let mut msg_req = CreateMessageReq { session_id: req.session_id, content_type: req.content_type, content: req.content, }; msg_req.sanitize(); let result = consultation_service::create_message( &state, ctx.tenant_id, Some(ctx.user_id), ctx.user_id, sender_role, msg_req, ) .await?; Ok(Json(ApiResponse::ok(result))) } pub async fn export_sessions( State(state): State, Extension(ctx): Extension, Query(params): Query, ) -> Result>>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.list")?; let result = consultation_service::export_sessions( &state, ctx.tenant_id, params.status, params.patient_id, params.doctor_id, params.page, params.page_size, ) .await?; Ok(Json(ApiResponse::ok(result))) } /// 标记会话消息为已读。 #[utoipa::path( put, path = "/consultation-sessions/{id}/read", responses( (status = 200, description = "标记成功"), (status = 404, description = "会话不存在"), ), tag = "咨询管理", )] pub async fn mark_session_read( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.manage")?; let is_doctor = crate::entity::doctor_profile::Entity::find() .filter(crate::entity::doctor_profile::Column::UserId.eq(ctx.user_id)) .filter(crate::entity::doctor_profile::Column::TenantId.eq(ctx.tenant_id)) .filter(crate::entity::doctor_profile::Column::DeletedAt.is_null()) .one(&state.db) .await .map_err(|e| AppError::Internal(e.to_string()))? .is_some(); let role = if is_doctor { "doctor" } else { "patient" }; consultation_service::mark_session_read(&state, ctx.tenant_id, id, ctx.user_id, role).await?; Ok(Json(ApiResponse::ok(()))) } /// 获取当前医生的仪表盘数据。 #[utoipa::path( get, path = "/doctor/dashboard", responses( (status = 200, description = "仪表盘数据"), ), tag = "医护端", )] pub async fn get_doctor_dashboard( State(state): State, Extension(ctx): Extension, ) -> Result>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.list")?; let mut result = consultation_service::get_doctor_dashboard(&state, ctx.tenant_id, ctx.user_id).await?; consultation_service::enrich_doctor_dashboard_health( &state, ctx.tenant_id, ctx.user_id, &mut result, ) .await?; Ok(Json(ApiResponse::ok(result))) }