From b81a97224587a195697aefdd45b74bd5095742a2 Mon Sep 17 00:00:00 2001 From: iven Date: Wed, 3 Jun 2026 01:14:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(diary):=20=E4=B8=BA=E6=89=80=E6=9C=89=20DTO?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=20Validate=20derive=20+=20handler=20?= =?UTF-8?q?=E8=B0=83=E7=94=A8=20validate()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DTO 验证规则: - CreateJournalReq: title 1-200, tags ≤20 - UpdateJournalReq: title 1-200, tags ≤20 - CreateClassReq: name 1-50, school_name ≤100 - JoinClassReq: class_code = 6位 - UpdateClassReq: name 1-50, school_name ≤100 - SyncReq: changes ≤100 条 - CreateTopicReq: title 1-200, description ≤2000 - UpdateTopicReq: title 1-200, description ≤2000 - CreateCommentReq: content 1-1000 - CreateStickerPackReq: name 1-50, description ≤500 - UpdateStickerPackReq: name 1-50, description ≤500 - CreateStickerReq: name 1-30, image_url 1-500 - BindChildReq/DeleteChildDataReq: Validate derive (Uuid 已由 serde 验证) Handler 调用: validate() 放在 require_permission() 之前(先验证输入再检查权限) 审计 ID: 5a-C01, 5a-C02, 5a-C03 --- crates/erp-diary/src/dto.rs | 49 ++++++++++++++----- crates/erp-diary/src/handler/class_handler.rs | 4 ++ .../erp-diary/src/handler/comment_handler.rs | 2 + .../erp-diary/src/handler/journal_handler.rs | 3 ++ .../erp-diary/src/handler/parent_handler.rs | 5 +- .../erp-diary/src/handler/sticker_handler.rs | 4 ++ crates/erp-diary/src/handler/sync_handler.rs | 2 + crates/erp-diary/src/handler/topic_handler.rs | 3 ++ 8 files changed, 58 insertions(+), 14 deletions(-) diff --git a/crates/erp-diary/src/dto.rs b/crates/erp-diary/src/dto.rs index 9131076..274630e 100644 --- a/crates/erp-diary/src/dto.rs +++ b/crates/erp-diary/src/dto.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use validator::Validate; /// 日记心情枚举 #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -26,12 +27,14 @@ pub enum Weather { } /// 创建日记请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateJournalReq { + #[validate(length(min = 1, max = 200, message = "标题长度 1-200 字符"))] pub title: String, pub date: chrono::NaiveDate, pub mood: Mood, pub weather: Weather, + #[validate(length(max = 20, message = "标签最多 20 个"))] pub tags: Vec, pub is_private: bool, pub class_id: Option, @@ -39,11 +42,13 @@ pub struct CreateJournalReq { } /// 更新日记请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateJournalReq { + #[validate(length(min = 1, max = 200, message = "标题长度 1-200 字符"))] pub title: Option, pub mood: Option, pub weather: Option, + #[validate(length(max = 20, message = "标签最多 20 个"))] pub tags: Option>, pub is_private: Option, pub shared_to_class: Option, @@ -69,24 +74,29 @@ pub struct JournalResp { } /// 创建班级请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateClassReq { + #[validate(length(min = 1, max = 50, message = "班级名称长度 1-50 字符"))] pub name: String, + #[validate(length(max = 100, message = "学校名称最长 100 字符"))] pub school_name: Option, } /// 加入班级请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct JoinClassReq { + #[validate(length(min = 6, max = 6, message = "班级码必须为 6 位"))] pub class_code: String, } /// 更新班级请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateClassReq { /// 班级名称 + #[validate(length(min = 1, max = 50, message = "班级名称长度 1-50 字符"))] pub name: Option, /// 学校名称 + #[validate(length(max = 100, message = "学校名称最长 100 字符"))] pub school_name: Option, /// 乐观锁版本号 pub version: i32, @@ -114,9 +124,10 @@ pub struct ClassResp { } /// 同步请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct SyncReq { pub last_sync_time: Option>, + #[validate(length(max = 100, message = "单次同步最多 100 条变更"))] pub changes: Vec, } @@ -158,11 +169,13 @@ pub struct ClassMemberResp { // ========== 主题布置 ========== /// 创建主题请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateTopicReq { /// 主题标题 + #[validate(length(min = 1, max = 200, message = "主题标题长度 1-200 字符"))] pub title: String, /// 主题描述/要求 + #[validate(length(max = 2000, message = "主题描述最长 2000 字符"))] pub description: Option, /// 截止日期 pub due_date: Option, @@ -183,18 +196,21 @@ pub struct TopicResp { // ========== 评语 ========== /// 创建评语请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateCommentReq { /// 评语内容 + #[validate(length(min = 1, max = 1000, message = "评语长度 1-1000 字符"))] pub content: String, } /// 更新主题请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateTopicReq { /// 主题标题 + #[validate(length(min = 1, max = 200, message = "主题标题长度 1-200 字符"))] pub title: Option, /// 主题描述 + #[validate(length(max = 2000, message = "主题描述最长 2000 字符"))] pub description: Option, /// 截止日期 pub due_date: Option, @@ -203,11 +219,13 @@ pub struct UpdateTopicReq { } /// 创建贴纸包请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateStickerPackReq { /// 贴纸包名称 + #[validate(length(min = 1, max = 50, message = "贴纸包名称长度 1-50 字符"))] pub name: String, /// 描述 + #[validate(length(max = 500, message = "描述最长 500 字符"))] pub description: Option, /// 缩略图 URL pub thumbnail_url: Option, @@ -218,17 +236,20 @@ pub struct CreateStickerPackReq { #[serde(default)] pub price: i32, /// 分类 + #[validate(length(max = 30, message = "分类最长 30 字符"))] pub category: Option, } fn default_true() -> bool { true } /// 更新贴纸包请求 — 所有字段可选,仅更新传入的字段 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateStickerPackReq { /// 贴纸包名称 + #[validate(length(min = 1, max = 50, message = "贴纸包名称长度 1-50 字符"))] pub name: Option, /// 描述 + #[validate(length(max = 500, message = "描述最长 500 字符"))] pub description: Option, /// 缩略图 URL pub thumbnail_url: Option, @@ -237,17 +258,21 @@ pub struct UpdateStickerPackReq { /// 价格(积分) pub price: Option, /// 分类 + #[validate(length(max = 30, message = "分类最长 30 字符"))] pub category: Option, } /// 创建贴纸请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateStickerReq { /// 贴纸名称 + #[validate(length(min = 1, max = 30, message = "贴纸名称长度 1-30 字符"))] pub name: String, /// 图片 URL + #[validate(length(min = 1, max = 500, message = "图片 URL 长度 1-500 字符"))] pub image_url: String, /// 分类 + #[validate(length(max = 30, message = "分类最长 30 字符"))] pub category: Option, } diff --git a/crates/erp-diary/src/handler/class_handler.rs b/crates/erp-diary/src/handler/class_handler.rs index 17c5b50..6ccab71 100644 --- a/crates/erp-diary/src/handler/class_handler.rs +++ b/crates/erp-diary/src/handler/class_handler.rs @@ -3,6 +3,7 @@ use axum::extract::{Extension, FromRef, Path, State}; use axum::response::Json; use uuid::Uuid; +use validator::Validate; use erp_core::error::AppError; use erp_core::rbac::require_permission; @@ -37,6 +38,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.class.manage")?; if req.name.trim().is_empty() { @@ -81,6 +83,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.journal.create")?; if req.class_code.trim().is_empty() { @@ -248,6 +251,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.class.manage")?; if let Some(ref name) = req.name { diff --git a/crates/erp-diary/src/handler/comment_handler.rs b/crates/erp-diary/src/handler/comment_handler.rs index 8ee453e..467de04 100644 --- a/crates/erp-diary/src/handler/comment_handler.rs +++ b/crates/erp-diary/src/handler/comment_handler.rs @@ -3,6 +3,7 @@ use axum::extract::{Extension, FromRef, Path, State}; use axum::response::Json; use uuid::Uuid; +use validator::Validate; use erp_core::error::AppError; use erp_core::rbac::require_permission; @@ -41,6 +42,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.comment.write")?; if req.content.trim().is_empty() { diff --git a/crates/erp-diary/src/handler/journal_handler.rs b/crates/erp-diary/src/handler/journal_handler.rs index 278fa5a..bef74d6 100644 --- a/crates/erp-diary/src/handler/journal_handler.rs +++ b/crates/erp-diary/src/handler/journal_handler.rs @@ -5,6 +5,7 @@ use axum::response::Json; use serde::Deserialize; use utoipa::IntoParams; use uuid::Uuid; +use validator::Validate; use erp_core::error::AppError; use erp_core::rbac::require_permission; @@ -58,6 +59,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.journal.create")?; // 基础验证 @@ -145,6 +147,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.journal.update")?; let resp = JournalService::update( diff --git a/crates/erp-diary/src/handler/parent_handler.rs b/crates/erp-diary/src/handler/parent_handler.rs index cb9ef39..951ae38 100644 --- a/crates/erp-diary/src/handler/parent_handler.rs +++ b/crates/erp-diary/src/handler/parent_handler.rs @@ -5,6 +5,7 @@ use axum::response::Json; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; +use validator::Validate; use erp_core::error::AppError; use erp_core::rbac::require_permission; @@ -17,7 +18,7 @@ use crate::state::DiaryState; // ---- 请求/响应 DTO ---- /// 绑定孩子请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct BindChildReq { /// 孩子的用户 ID pub child_id: Uuid, @@ -42,7 +43,7 @@ pub struct ExportQuery { } /// 删除孩子数据请求 -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct DeleteChildDataReq { /// 孩子的用户 ID pub child_id: Uuid, diff --git a/crates/erp-diary/src/handler/sticker_handler.rs b/crates/erp-diary/src/handler/sticker_handler.rs index e53c562..ef33c51 100644 --- a/crates/erp-diary/src/handler/sticker_handler.rs +++ b/crates/erp-diary/src/handler/sticker_handler.rs @@ -5,6 +5,7 @@ use axum::response::Json; use serde::Deserialize; use utoipa::IntoParams; use uuid::Uuid; +use validator::Validate; use erp_core::error::AppError; use erp_core::rbac::require_permission; @@ -110,6 +111,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.class.manage")?; if req.name.trim().is_empty() { @@ -155,6 +157,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.class.manage")?; if let Some(ref name) = req.name { @@ -235,6 +238,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.class.manage")?; if req.name.trim().is_empty() { diff --git a/crates/erp-diary/src/handler/sync_handler.rs b/crates/erp-diary/src/handler/sync_handler.rs index 1d57509..e71662f 100644 --- a/crates/erp-diary/src/handler/sync_handler.rs +++ b/crates/erp-diary/src/handler/sync_handler.rs @@ -2,6 +2,7 @@ use axum::extract::{Extension, FromRef, State}; use axum::response::Json; +use validator::Validate; use erp_core::error::AppError; use erp_core::rbac::require_permission; @@ -38,6 +39,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.journal.read")?; let resp = SyncService::sync( diff --git a/crates/erp-diary/src/handler/topic_handler.rs b/crates/erp-diary/src/handler/topic_handler.rs index 90122ba..62bf177 100644 --- a/crates/erp-diary/src/handler/topic_handler.rs +++ b/crates/erp-diary/src/handler/topic_handler.rs @@ -3,6 +3,7 @@ use axum::extract::{Extension, FromRef, Path, State}; use axum::response::Json; use uuid::Uuid; +use validator::Validate; use erp_core::error::AppError; use erp_core::rbac::require_permission; @@ -40,6 +41,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.topic.assign")?; if req.title.trim().is_empty() { @@ -118,6 +120,7 @@ where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { + req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.topic.assign")?; if let Some(ref title) = req.title {