- erp-health: article/banner/consultation/media 服务层优化 - erp-ai: analysis/insight/prompt 服务增强 - erp-auth: auth/role/token 服务改进 - erp-workflow: executor 执行引擎修复 - erp-plugin: 服务层改进 - 新增媒体上传文件样例
308 lines
9.2 KiB
Rust
308 lines
9.2 KiB
Rust
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<u64>,
|
||
pub page_size: Option<u64>,
|
||
pub status: Option<String>,
|
||
pub patient_id: Option<Uuid>,
|
||
pub doctor_id: Option<Uuid>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize, IntoParams)]
|
||
pub struct MessageListParams {
|
||
pub page: Option<u64>,
|
||
pub page_size: Option<u64>,
|
||
pub after_id: Option<Uuid>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize, IntoParams)]
|
||
pub struct PollMessagesParams {
|
||
pub after_id: Option<Uuid>,
|
||
/// 超时秒数,默认 25,最大 30
|
||
pub timeout: Option<u64>,
|
||
}
|
||
|
||
#[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<String>,
|
||
pub content: String,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize, IntoParams)]
|
||
pub struct ExportSessionsParams {
|
||
pub status: Option<String>,
|
||
pub patient_id: Option<Uuid>,
|
||
pub doctor_id: Option<Uuid>,
|
||
pub page: Option<u64>,
|
||
pub page_size: Option<u64>,
|
||
}
|
||
|
||
pub async fn create_session<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Json(req): Json<CreateSessionReq>,
|
||
) -> Result<Json<ApiResponse<SessionResp>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Query(params): Query<SessionListParams>,
|
||
) -> Result<Json<ApiResponse<PaginatedResponse<SessionResp>>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Path(id): Path<Uuid>,
|
||
) -> Result<Json<ApiResponse<SessionResp>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Path(session_id): Path<Uuid>,
|
||
Query(params): Query<MessageListParams>,
|
||
) -> Result<Json<ApiResponse<PaginatedResponse<MessageResp>>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Path(session_id): Path<Uuid>,
|
||
Query(params): Query<PollMessagesParams>,
|
||
) -> Result<Json<ApiResponse<Vec<MessageResp>>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Path(id): Path<Uuid>,
|
||
Json(req): Json<CloseSessionReq>,
|
||
) -> Result<Json<ApiResponse<SessionResp>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Json(req): Json<CreateConsultationMessageReq>,
|
||
) -> Result<Json<ApiResponse<MessageResp>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Query(params): Query<ExportSessionsParams>,
|
||
) -> Result<Json<ApiResponse<PaginatedResponse<SessionResp>>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Path(id): Path<Uuid>,
|
||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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<S>(
|
||
State(state): State<HealthState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
) -> Result<Json<ApiResponse<consultation_service::DoctorDashboard>>, AppError>
|
||
where
|
||
HealthState: FromRef<S>,
|
||
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)))
|
||
}
|