From 4b40d47b71787485992272e9ebd6f3c781c27423 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 21 May 2026 22:37:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(health):=20DTO=20=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E8=A1=A5=E5=85=A8=20+=20handler=20.validate(?= =?UTF-8?q?)=20=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - daily_monitoring_dto: Create/Update 添加 Validate derive + 血压/体重/血糖/入液量范围校验 - health_data_dto: LabReport/HealthRecord Create/Update/Review 添加 Validate derive - consultation_dto: CreateSessionReq/CreateMessageReq 添加 Validate + content length - article_dto: title max=500→200 匹配 DB VARCHAR(200) - health_data_handler: 7 个 create/update handler 添加 .validate() 调用 - consultation_handler: create_session/create_message 添加 .validate() 调用 - daily_monitoring_handler: create/update 添加 .validate() 调用 --- crates/erp-health/src/dto/article_dto.rs | 4 +- crates/erp-health/src/dto/consultation_dto.rs | 11 +++-- .../src/dto/daily_monitoring_dto.rs | 23 +++++++++- crates/erp-health/src/dto/health_data_dto.rs | 42 +++++++++++++++---- .../erp-health/src/handler/article_handler.rs | 6 +-- .../src/handler/consultation_handler.rs | 9 +++- .../src/handler/daily_monitoring_handler.rs | 7 +++- .../src/handler/health_data_handler.rs | 24 +++++++++-- 8 files changed, 102 insertions(+), 24 deletions(-) diff --git a/crates/erp-health/src/dto/article_dto.rs b/crates/erp-health/src/dto/article_dto.rs index cec66ed..69962fe 100644 --- a/crates/erp-health/src/dto/article_dto.rs +++ b/crates/erp-health/src/dto/article_dto.rs @@ -75,7 +75,7 @@ pub struct ArticleListParams { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct CreateArticleReq { - #[validate(length(min = 1, max = 500, message = "文章标题长度须在1-500之间"))] + #[validate(length(min = 1, max = 200, message = "文章标题长度须在1-200之间"))] pub title: String, #[validate(length(max = 2000, message = "摘要最多2000字"))] pub summary: Option, @@ -113,7 +113,7 @@ impl CreateArticleReq { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct UpdateArticleReq { - #[validate(length(min = 1, max = 500, message = "文章标题长度须在1-500之间"))] + #[validate(length(min = 1, max = 200, message = "文章标题长度须在1-200之间"))] pub title: Option, #[validate(length(max = 2000, message = "摘要最多2000字"))] pub summary: Option, diff --git a/crates/erp-health/src/dto/consultation_dto.rs b/crates/erp-health/src/dto/consultation_dto.rs index cafe407..9e137fb 100644 --- a/crates/erp-health/src/dto/consultation_dto.rs +++ b/crates/erp-health/src/dto/consultation_dto.rs @@ -37,10 +37,12 @@ pub struct MessageResp { } /// 发送消息请求体 — 不含 sender_id/sender_role,由服务端从 JWT 注入。 -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct CreateMessageReq { pub session_id: Uuid, + #[validate(length(max = 50, message = "内容类型最多 50 字"))] pub content_type: Option, + #[validate(length(min = 1, max = 5000, message = "消息内容长度须在 1-5000 之间"))] pub content: String, /// 关联的媒体文件 ID(当 content_type 为 image/file/voice 时必填) pub media_id: Option, @@ -52,10 +54,11 @@ impl CreateMessageReq { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct CreateSessionReq { pub patient_id: Uuid, pub doctor_id: Option, + #[validate(length(max = 50, message = "咨询类型最多 50 字"))] pub consultation_type: Option, } @@ -70,11 +73,13 @@ pub struct SessionQuery { /// 从咨询会话创建随访任务请求体 — 仅需填写随访类型和计划日期, /// patient_id / source_type / source_id 由服务端自动填充。 -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct CreateFollowUpFromConsultationReq { + #[validate(length(min = 1, max = 50, message = "随访类型长度须在 1-50 之间"))] pub follow_up_type: String, pub planned_date: chrono::NaiveDate, pub assigned_to: Option, + #[validate(length(max = 2000, message = "内容模板最多 2000 字"))] pub content_template: Option, } diff --git a/crates/erp-health/src/dto/daily_monitoring_dto.rs b/crates/erp-health/src/dto/daily_monitoring_dto.rs index 16af755..f059555 100644 --- a/crates/erp-health/src/dto/daily_monitoring_dto.rs +++ b/crates/erp-health/src/dto/daily_monitoring_dto.rs @@ -3,6 +3,7 @@ use erp_core::sanitize::sanitize_option; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; +use validator::Validate; type Decimal = f64; @@ -10,18 +11,27 @@ type Decimal = f64; // 日常监测 // --------------------------------------------------------------------------- -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct CreateDailyMonitoringReq { pub patient_id: Uuid, pub record_date: NaiveDate, + #[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))] pub morning_bp_systolic: Option, + #[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))] pub morning_bp_diastolic: Option, + #[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))] pub evening_bp_systolic: Option, + #[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))] pub evening_bp_diastolic: Option, + #[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))] pub weight: Option, + #[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))] pub blood_sugar: Option, + #[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))] pub fluid_intake: Option, + #[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))] pub urine_output: Option, + #[validate(length(max = 2000, message = "备注最多 2000 字"))] #[serde(skip_serializing_if = "Option::is_none")] pub notes: Option, } @@ -32,17 +42,26 @@ impl CreateDailyMonitoringReq { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct UpdateDailyMonitoringReq { pub record_date: Option, + #[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))] pub morning_bp_systolic: Option, + #[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))] pub morning_bp_diastolic: Option, + #[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))] pub evening_bp_systolic: Option, + #[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))] pub evening_bp_diastolic: Option, + #[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))] pub weight: Option, + #[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))] pub blood_sugar: Option, + #[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))] pub fluid_intake: Option, + #[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))] pub urine_output: Option, + #[validate(length(max = 2000, message = "备注最多 2000 字"))] pub notes: Option, } diff --git a/crates/erp-health/src/dto/health_data_dto.rs b/crates/erp-health/src/dto/health_data_dto.rs index dfafa49..0be869f 100644 --- a/crates/erp-health/src/dto/health_data_dto.rs +++ b/crates/erp-health/src/dto/health_data_dto.rs @@ -3,6 +3,7 @@ use erp_core::sanitize::sanitize_option; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; +use validator::Validate; /// 用 f64 表示 Decimal 值以满足 utoipa ToSchema 要求。 /// 对于健康数值(血压 60-200mmHg、血糖 3.9-11.1mmol/L、体重 30-300kg), @@ -10,23 +11,37 @@ use uuid::Uuid; /// 数据库层仍使用 SeaORM Decimal 类型,转换仅在 DTO 边界进行。 type Decimal = f64; -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct CreateVitalSignsReq { pub record_date: NaiveDate, + #[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))] pub systolic_bp_morning: Option, + #[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))] pub diastolic_bp_morning: Option, + #[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))] pub systolic_bp_evening: Option, + #[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))] pub diastolic_bp_evening: Option, + #[validate(range(min = 20, max = 250, message = "心率范围 20-250 bpm"))] pub heart_rate: Option, + #[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))] pub weight: Option, + #[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))] pub blood_sugar: Option, + #[validate(range(min = 30.0, max = 45.0, message = "体温范围 30-45 °C"))] pub body_temperature: Option, + #[validate(range(min = 50, max = 100, message = "血氧范围 50-100%"))] pub spo2: Option, /// fasting / postprandial / random / ogtt + #[validate(length(max = 50, message = "血糖类型最多 50 字"))] pub blood_sugar_type: Option, + #[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))] pub water_intake_ml: Option, + #[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))] pub urine_output_ml: Option, + #[validate(length(max = 2000, message = "备注最多 2000 字"))] pub notes: Option, + #[validate(length(max = 50, message = "来源最多 50 字"))] pub source: Option, } @@ -36,21 +51,34 @@ impl CreateVitalSignsReq { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct UpdateVitalSignsReq { pub record_date: Option, + #[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))] pub systolic_bp_morning: Option, + #[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))] pub diastolic_bp_morning: Option, + #[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))] pub systolic_bp_evening: Option, + #[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))] pub diastolic_bp_evening: Option, + #[validate(range(min = 20, max = 250, message = "心率范围 20-250 bpm"))] pub heart_rate: Option, + #[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))] pub weight: Option, + #[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))] pub blood_sugar: Option, + #[validate(range(min = 30.0, max = 45.0, message = "体温范围 30-45 °C"))] pub body_temperature: Option, + #[validate(range(min = 50, max = 100, message = "血氧范围 50-100%"))] pub spo2: Option, + #[validate(length(max = 50, message = "血糖类型最多 50 字"))] pub blood_sugar_type: Option, + #[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))] pub water_intake_ml: Option, + #[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))] pub urine_output_ml: Option, + #[validate(length(max = 2000, message = "备注最多 2000 字"))] pub notes: Option, } @@ -84,7 +112,7 @@ pub struct VitalSignsResp { pub version: i32, } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct CreateLabReportReq { pub report_date: NaiveDate, pub report_type: String, @@ -102,7 +130,7 @@ impl CreateLabReportReq { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct UpdateLabReportReq { pub report_date: Option, pub report_type: Option, @@ -136,7 +164,7 @@ pub struct LabReportResp { pub version: i32, } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct CreateHealthRecordReq { pub record_type: Option, pub record_date: NaiveDate, @@ -154,7 +182,7 @@ impl CreateHealthRecordReq { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)] pub struct UpdateHealthRecordReq { pub record_type: Option, pub record_date: Option, @@ -264,7 +292,7 @@ pub struct MiniTodayResp { pub weight: Option, } -#[derive(Debug, Clone, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, serde::Deserialize, ToSchema, Validate)] pub struct ReviewLabReportReq { pub doctor_notes: Option, pub items: Option, diff --git a/crates/erp-health/src/handler/article_handler.rs b/crates/erp-health/src/handler/article_handler.rs index ae285a1..2f9782d 100644 --- a/crates/erp-health/src/handler/article_handler.rs +++ b/crates/erp-health/src/handler/article_handler.rs @@ -25,7 +25,7 @@ where { require_permission(&ctx, "health.articles.list")?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); // 非管理权限用户只能查看已发布文章,防止草稿泄露 let status = if require_any_permission(&ctx, &["health.articles.manage", "health.articles.review"]) @@ -58,7 +58,7 @@ pub async fn list_public_articles( .tenant_id .ok_or_else(|| AppError::Validation("tenant_id is required".into()))?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); let result = article_service::list_articles( &state, tenant_id, @@ -307,7 +307,7 @@ where { require_permission(&ctx, "health.articles.list")?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); let result = article_service::list_revisions(&state, ctx.tenant_id, id, page, page_size).await?; Ok(Json(ApiResponse::ok(result))) diff --git a/crates/erp-health/src/handler/consultation_handler.rs b/crates/erp-health/src/handler/consultation_handler.rs index 5dfd5b6..8e63f97 100644 --- a/crates/erp-health/src/handler/consultation_handler.rs +++ b/crates/erp-health/src/handler/consultation_handler.rs @@ -68,6 +68,8 @@ where S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.consultation.manage")?; + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; let result = consultation_service::create_session(&state, ctx.tenant_id, Some(ctx.user_id), req).await?; Ok(Json(ApiResponse::ok(result))) @@ -84,7 +86,7 @@ where { require_permission(&ctx, "health.consultation.list")?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); let result = consultation_service::list_sessions( &state, ctx.tenant_id, @@ -124,7 +126,7 @@ where { require_permission(&ctx, "health.consultation.list")?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); let result = consultation_service::list_messages( &state, ctx.tenant_id, @@ -209,6 +211,9 @@ where content: req.content, media_id: None, }; + msg_req + .validate() + .map_err(|e| AppError::Validation(e.to_string()))?; msg_req.sanitize(); let result = consultation_service::create_message( &state, diff --git a/crates/erp-health/src/handler/daily_monitoring_handler.rs b/crates/erp-health/src/handler/daily_monitoring_handler.rs index 73baf67..efe697c 100644 --- a/crates/erp-health/src/handler/daily_monitoring_handler.rs +++ b/crates/erp-health/src/handler/daily_monitoring_handler.rs @@ -3,6 +3,7 @@ use axum::extract::{FromRef, Json, Path, Query, State}; use serde::Deserialize; use utoipa::IntoParams; use uuid::Uuid; +use validator::Validate; use erp_core::error::AppError; use erp_core::rbac::require_permission; @@ -38,7 +39,7 @@ where { require_permission(&ctx, "health.daily-monitoring.list")?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); let result = daily_monitoring_service::list_daily_monitoring( &state, ctx.tenant_id, @@ -76,6 +77,8 @@ where { require_permission(&ctx, "health.daily-monitoring.manage")?; let mut req = req; + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; req.sanitize(); let result = daily_monitoring_service::create_daily_monitoring( &state, @@ -99,6 +102,8 @@ where { require_permission(&ctx, "health.daily-monitoring.manage")?; let mut data = req.data; + data.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; data.sanitize(); let result = daily_monitoring_service::update_daily_monitoring( &state, diff --git a/crates/erp-health/src/handler/health_data_handler.rs b/crates/erp-health/src/handler/health_data_handler.rs index 548aebc..b1616b7 100644 --- a/crates/erp-health/src/handler/health_data_handler.rs +++ b/crates/erp-health/src/handler/health_data_handler.rs @@ -8,6 +8,8 @@ use erp_core::error::AppError; use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext}; +use validator::Validate; + use crate::dto::DeleteWithVersion; use crate::dto::health_data_dto::*; use crate::service::health_data_service; @@ -58,7 +60,7 @@ where { require_permission(&ctx, "health.health-data.list")?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); let result = health_data_service::list_vital_signs(&state, ctx.tenant_id, patient_id, page, page_size) .await?; @@ -77,6 +79,8 @@ where { require_permission(&ctx, "health.health-data.manage")?; let mut req = req; + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; req.sanitize(); let result = health_data_service::create_vital_signs( &state, @@ -101,6 +105,8 @@ where { require_permission(&ctx, "health.health-data.manage")?; let mut data = req.data; + data.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; data.sanitize(); let result = health_data_service::update_vital_signs( &state, @@ -153,7 +159,7 @@ where { require_permission(&ctx, "health.health-data.list")?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); let result = health_data_service::list_lab_reports(&state, ctx.tenant_id, patient_id, page, page_size) .await?; @@ -172,6 +178,8 @@ where { require_permission(&ctx, "health.health-data.manage")?; let mut req = req; + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; req.sanitize(); let result = health_data_service::create_lab_report( &state, @@ -196,6 +204,8 @@ where { require_permission(&ctx, "health.health-data.manage")?; let mut data = req.data; + data.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; data.sanitize(); let result = health_data_service::update_lab_report( &state, @@ -244,6 +254,8 @@ where { require_permission(&ctx, "health.health-data.manage")?; let mut data = req.data; + data.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; data.sanitize(); let result = health_data_service::review_lab_report( &state, @@ -274,7 +286,7 @@ where { require_permission(&ctx, "health.health-data.list")?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); let result = health_data_service::list_health_records( &state, ctx.tenant_id, @@ -298,6 +310,8 @@ where { require_permission(&ctx, "health.health-data.manage")?; let mut req = req; + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; req.sanitize(); let result = health_data_service::create_health_record( &state, @@ -322,6 +336,8 @@ where { require_permission(&ctx, "health.health-data.manage")?; let mut data = req.data; + data.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; data.sanitize(); let result = health_data_service::update_health_record( &state, @@ -374,7 +390,7 @@ where { require_permission(&ctx, "health.health-data.list")?; let page = params.page.unwrap_or(1); - let page_size = params.page_size.unwrap_or(20); + let page_size = params.page_size.unwrap_or(20).min(100); let result = trend_service::list_trends(&state, ctx.tenant_id, patient_id, page, page_size).await?; Ok(Json(ApiResponse::ok(result)))