Files
hms/crates/erp-health/src/handler/consultation_handler.rs
iven 931edc3025 fix(security): 补全 XSS sanitize + 修复 sender_id 身份伪造
安全审计修复:
- 补全 6 个 DTO 的 sanitize 方法(diagnosis/consent/alert/medication_record/medication_reminder/follow_up_template)
- 4 个 handler 添加 .sanitize() 调用(diagnosis/consent/alert_rule/medication_record)
- 修复咨询消息 sender_id/sender_role 从客户端提交改为服务端从 JWT 提取
- 修复小程序 AI 报告 markdownToHtml XSS(添加 sanitizeHtml 过滤)
2026-04-30 10:21:52 +08:00

257 lines
8.1 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 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, 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)))
}
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)))
}