fix(health): DTO 输入校验补全 + handler .validate() 调用

- 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() 调用
This commit is contained in:
iven
2026-05-21 22:37:26 +08:00
parent 21481dbd88
commit 4b40d47b71
8 changed files with 102 additions and 24 deletions

View File

@@ -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<String>,
@@ -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<String>,
#[validate(length(max = 2000, message = "摘要最多2000字"))]
pub summary: Option<String>,

View File

@@ -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<String>,
#[validate(length(min = 1, max = 5000, message = "消息内容长度须在 1-5000 之间"))]
pub content: String,
/// 关联的媒体文件 ID当 content_type 为 image/file/voice 时必填)
pub media_id: Option<Uuid>,
@@ -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<Uuid>,
#[validate(length(max = 50, message = "咨询类型最多 50 字"))]
pub consultation_type: Option<String>,
}
@@ -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<Uuid>,
#[validate(length(max = 2000, message = "内容模板最多 2000 字"))]
pub content_template: Option<String>,
}

View File

@@ -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<i32>,
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
pub morning_bp_diastolic: Option<i32>,
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
pub evening_bp_systolic: Option<i32>,
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
pub evening_bp_diastolic: Option<i32>,
#[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))]
pub weight: Option<Decimal>,
#[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))]
pub blood_sugar: Option<Decimal>,
#[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))]
pub fluid_intake: Option<i32>,
#[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))]
pub urine_output: Option<i32>,
#[validate(length(max = 2000, message = "备注最多 2000 字"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
@@ -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<NaiveDate>,
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
pub morning_bp_systolic: Option<i32>,
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
pub morning_bp_diastolic: Option<i32>,
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
pub evening_bp_systolic: Option<i32>,
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
pub evening_bp_diastolic: Option<i32>,
#[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))]
pub weight: Option<Decimal>,
#[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))]
pub blood_sugar: Option<Decimal>,
#[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))]
pub fluid_intake: Option<i32>,
#[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))]
pub urine_output: Option<i32>,
#[validate(length(max = 2000, message = "备注最多 2000 字"))]
pub notes: Option<String>,
}

View File

@@ -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<i32>,
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
pub diastolic_bp_morning: Option<i32>,
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
pub systolic_bp_evening: Option<i32>,
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
pub diastolic_bp_evening: Option<i32>,
#[validate(range(min = 20, max = 250, message = "心率范围 20-250 bpm"))]
pub heart_rate: Option<i32>,
#[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))]
pub weight: Option<Decimal>,
#[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))]
pub blood_sugar: Option<Decimal>,
#[validate(range(min = 30.0, max = 45.0, message = "体温范围 30-45 °C"))]
pub body_temperature: Option<Decimal>,
#[validate(range(min = 50, max = 100, message = "血氧范围 50-100%"))]
pub spo2: Option<i32>,
/// fasting / postprandial / random / ogtt
#[validate(length(max = 50, message = "血糖类型最多 50 字"))]
pub blood_sugar_type: Option<String>,
#[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))]
pub water_intake_ml: Option<i32>,
#[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))]
pub urine_output_ml: Option<i32>,
#[validate(length(max = 2000, message = "备注最多 2000 字"))]
pub notes: Option<String>,
#[validate(length(max = 50, message = "来源最多 50 字"))]
pub source: Option<String>,
}
@@ -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<NaiveDate>,
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
pub systolic_bp_morning: Option<i32>,
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
pub diastolic_bp_morning: Option<i32>,
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
pub systolic_bp_evening: Option<i32>,
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
pub diastolic_bp_evening: Option<i32>,
#[validate(range(min = 20, max = 250, message = "心率范围 20-250 bpm"))]
pub heart_rate: Option<i32>,
#[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))]
pub weight: Option<Decimal>,
#[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))]
pub blood_sugar: Option<Decimal>,
#[validate(range(min = 30.0, max = 45.0, message = "体温范围 30-45 °C"))]
pub body_temperature: Option<Decimal>,
#[validate(range(min = 50, max = 100, message = "血氧范围 50-100%"))]
pub spo2: Option<i32>,
#[validate(length(max = 50, message = "血糖类型最多 50 字"))]
pub blood_sugar_type: Option<String>,
#[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))]
pub water_intake_ml: Option<i32>,
#[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))]
pub urine_output_ml: Option<i32>,
#[validate(length(max = 2000, message = "备注最多 2000 字"))]
pub notes: Option<String>,
}
@@ -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<NaiveDate>,
pub report_type: Option<String>,
@@ -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<String>,
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<String>,
pub record_date: Option<NaiveDate>,
@@ -264,7 +292,7 @@ pub struct MiniTodayResp {
pub weight: Option<IndicatorSummary>,
}
#[derive(Debug, Clone, serde::Deserialize, ToSchema)]
#[derive(Debug, Clone, serde::Deserialize, ToSchema, Validate)]
pub struct ReviewLabReportReq {
pub doctor_notes: Option<String>,
pub items: Option<serde_json::Value>,

View File

@@ -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)))

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)))