diff --git a/src/api/comments/create.rs b/src/api/comments/create.rs index f836896..75fc06e 100644 --- a/src/api/comments/create.rs +++ b/src/api/comments/create.rs @@ -38,71 +38,29 @@ pub async fn create_comment( let parts = ctx.parts_mut(); let ip = crate::api::rate_limit::get_client_ip(&parts.headers); if let Err(msg) = crate::api::rate_limit::check_comment_limit(&ip) { - return Ok(CommentResponse { - success: false, - message: msg, - error_code: Some("rate_limited".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("rate_limited", msg)); } } // 蜜罐字段二次校验:禁用 JS 的机器人可能绕过前端拦截,这里作为服务端防线。 if let Err(e) = validate_comment_honeypot(&honeypot) { - return Ok(CommentResponse { - success: false, - message: e, - error_code: Some("spam_detected".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("spam_detected", e)); } // 依次校验昵称、邮箱、网址与评论内容。 if let Err(e) = validate_comment_name(&author_name) { - return Ok(CommentResponse { - success: false, - message: e, - error_code: Some("invalid_input".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("invalid_input", e)); } if let Err(e) = validate_comment_email(&author_email) { - return Ok(CommentResponse { - success: false, - message: e, - error_code: Some("invalid_input".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("invalid_input", e)); } if let Some(ref url) = author_url { if let Err(e) = validate_comment_url(url) { - return Ok(CommentResponse { - success: false, - message: e, - error_code: Some("invalid_input".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("invalid_input", e)); } } if let Err(e) = validate_comment_content(&content_md) { - return Ok(CommentResponse { - success: false, - message: e, - error_code: Some("invalid_input".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("invalid_input", e)); } let mut client = get_conn().await.map_err(AppError::db_conn)?; @@ -118,27 +76,13 @@ pub async fn create_comment( match post_row { None => { - return Ok(CommentResponse { - success: false, - message: "文章不存在".to_string(), - error_code: Some("post_not_found".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("post_not_found", "文章不存在".to_string())); } Some(row) => { let status: String = row.get("status"); let deleted_at: Option> = row.get("deleted_at"); if status != "published" || deleted_at.is_some() { - return Ok(CommentResponse { - success: false, - message: "文章不存在".to_string(), - error_code: Some("post_not_found".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("post_not_found", "文章不存在".to_string())); } } } @@ -156,14 +100,7 @@ pub async fn create_comment( match parent_row { None => { - return Ok(CommentResponse { - success: false, - message: "父评论不存在".to_string(), - error_code: Some("parent_not_found".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("parent_not_found", "父评论不存在".to_string())); } Some(row) => { let parent_post_id: i32 = row.get("post_id"); @@ -171,36 +108,15 @@ pub async fn create_comment( let parent_depth: i32 = row.get("depth"); if parent_post_id != post_id { - return Ok(CommentResponse { - success: false, - message: "父评论不存在".to_string(), - error_code: Some("parent_not_found".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("parent_not_found", "父评论不存在".to_string())); } if parent_status != "approved" { - return Ok(CommentResponse { - success: false, - message: "父评论未通过审核".to_string(), - error_code: Some("parent_not_approved".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("parent_not_approved", "父评论未通过审核".to_string())); } depth = parent_depth + 1; if depth > 20 { - return Ok(CommentResponse { - success: false, - message: "评论嵌套层级过深".to_string(), - error_code: Some("too_deep".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("too_deep", "评论嵌套层级过深".to_string())); } } } @@ -259,14 +175,7 @@ pub async fn create_comment( if dup.is_some() { // 重复:回滚(释放 advisory 锁)后返回。 tx.rollback().await.ok(); - return Ok(CommentResponse { - success: false, - message: "请勿重复提交".to_string(), - error_code: Some("duplicate".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("duplicate", "请勿重复提交".to_string())); } // 插入评论,默认状态为 pending,等待管理员审核。 @@ -305,14 +214,7 @@ pub async fn create_comment( cache::invalidate_comments_by_post(post_id).await; cache::invalidate_pending_count().await; - Ok(CommentResponse { - success: true, - message: "评论已提交,等待审核".to_string(), - error_code: None, - comment_id: Some(comment_id), - avatar_url: Some(avatar_url), - depth: Some(depth), - }) + Ok(CommentResponse::created("评论已提交,等待审核".to_string(), comment_id, avatar_url, depth)) } #[cfg(not(feature = "server"))] unreachable!() diff --git a/src/api/comments/types.rs b/src/api/comments/types.rs index da0d677..2eb1cfa 100644 --- a/src/api/comments/types.rs +++ b/src/api/comments/types.rs @@ -23,6 +23,52 @@ pub struct CommentResponse { pub depth: Option, } +/// 这些构造器只在 server function body 内调用,WASM 端因 cfg gate 剥离了调用点, +/// 故对非 server 构建允许 dead_code。 +#[cfg_attr(not(feature = "server"), allow(dead_code))] +impl CommentResponse { + /// 构造失败响应,携带错误码(comment_id / avatar_url / depth 均为 None)。 + pub fn error(error_code: &str, message: impl Into) -> Self { + Self { + success: false, + message: message.into(), + error_code: Some(error_code.into()), + comment_id: None, + avatar_url: None, + depth: None, + } + } + + /// 构造成功响应(无关联数据,用于审核/删除等操作)。 + pub fn ok(message: impl Into) -> Self { + Self { + success: true, + message: message.into(), + error_code: None, + comment_id: None, + avatar_url: None, + depth: None, + } + } + + /// 构造成功响应,携带新评论 id / 头像 / 深度(用于创建评论)。 + pub fn created( + message: impl Into, + comment_id: i64, + avatar_url: impl Into, + depth: i32, + ) -> Self { + Self { + success: true, + message: message.into(), + error_code: None, + comment_id: Some(comment_id), + avatar_url: Some(avatar_url.into()), + depth: Some(depth), + } + } +} + /// 评论树响应:包含文章下的全部已审核评论。 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CommentTreeResponse { diff --git a/src/api/comments/update.rs b/src/api/comments/update.rs index ed6a3b8..f14ec63 100644 --- a/src/api/comments/update.rs +++ b/src/api/comments/update.rs @@ -34,14 +34,7 @@ pub async fn approve_comment(id: i64) -> Result let post_id: i32 = match row { Some(r) => r.get("post_id"), None => { - return Ok(CommentResponse { - success: false, - message: "评论不存在".to_string(), - error_code: Some("not_found".into()), - comment_id: None, - avatar_url: None, - depth: None, - }); + return Ok(CommentResponse::error("not_found", "评论不存在".to_string())); } }; @@ -72,14 +65,7 @@ pub async fn approve_comment(id: i64) -> Result cache::invalidate_comments_by_post(post_id).await; cache::invalidate_pending_count().await; - Ok(CommentResponse { - success: true, - message: "已通过".to_string(), - error_code: None, - comment_id: None, - avatar_url: None, - depth: None, - }) + Ok(CommentResponse::ok("已通过".to_string())) } #[cfg(not(feature = "server"))] unreachable!() @@ -127,14 +113,7 @@ pub async fn spam_comment(id: i64) -> Result { cache::invalidate_pending_count().await; } - Ok(CommentResponse { - success: true, - message: "已标记为垃圾".to_string(), - error_code: None, - comment_id: None, - avatar_url: None, - depth: None, - }) + Ok(CommentResponse::ok("已标记为垃圾".to_string())) } #[cfg(not(feature = "server"))] unreachable!() @@ -179,14 +158,7 @@ pub async fn trash_comment(id: i64) -> Result { cache::invalidate_pending_count().await; } - Ok(CommentResponse { - success: true, - message: "已删除".to_string(), - error_code: None, - comment_id: None, - avatar_url: None, - depth: None, - }) + Ok(CommentResponse::ok("已删除".to_string())) } #[cfg(not(feature = "server"))] unreachable!() diff --git a/src/api/posts/create.rs b/src/api/posts/create.rs index 784040c..fbf841f 100644 --- a/src/api/posts/create.rs +++ b/src/api/posts/create.rs @@ -37,22 +37,12 @@ pub async fn create_post( // 标题不能为空。 if title.trim().is_empty() { - return Ok(CreatePostResponse { - success: false, - message: "标题不能为空".to_string(), - post_id: None, - slug: None, - }); + return Ok(CreatePostResponse::err("标题不能为空".to_string())); } // 内容不能为空。 if content_md.trim().is_empty() { - return Ok(CreatePostResponse { - success: false, - message: "内容不能为空".to_string(), - post_id: None, - slug: None, - }); + return Ok(CreatePostResponse::err("内容不能为空".to_string())); } // 确定基础 slug:用户传入时校验格式,否则由标题生成。 @@ -60,12 +50,7 @@ pub async fn create_post( Some(ref s) if !s.trim().is_empty() => { let s = s.trim(); if !crate::api::slug::is_valid_slug(s) { - return Ok(CreatePostResponse { - success: false, - message: "slug 格式无效,只能包含字母、数字、连字符和下划线".to_string(), - post_id: None, - slug: None, - }); + return Ok(CreatePostResponse::err("slug 格式无效,只能包含字母、数字、连字符和下划线".to_string())); } s.to_string() } @@ -157,21 +142,11 @@ pub async fn create_post( // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 crate::ssr_cache::bump_global_generation(); - Ok(CreatePostResponse { - success: true, - message: "创建成功".to_string(), - post_id: Some(post_id), - slug: Some(final_slug), - }) + Ok(CreatePostResponse::ok("创建成功".to_string(), post_id, final_slug)) } #[cfg(not(feature = "server"))] { - Ok(CreatePostResponse { - success: false, - message: "server only".to_string(), - post_id: None, - slug: None, - }) + Ok(CreatePostResponse::err("server only".to_string())) } } diff --git a/src/api/posts/delete.rs b/src/api/posts/delete.rs index 4c6084d..8db2c04 100644 --- a/src/api/posts/delete.rs +++ b/src/api/posts/delete.rs @@ -38,12 +38,7 @@ pub async fn delete_post(post_id: i32) -> Result Result Result Result Result Result Result Result Result) -> Result) -> Result) -> Result) -> Result Result { crate::ssr_cache::bump_global_generation(); } - Ok(CreatePostResponse { - success: true, - message: format!("已清空回收站({result} 篇)"), - post_id: None, - slug: None, - }) + Ok(CreatePostResponse::ok_msg(format!("已清空回收站({result} 篇)"))) } #[cfg(not(feature = "server"))] { - Ok(CreatePostResponse { - success: false, - message: "server only".to_string(), - post_id: None, - slug: None, - }) + Ok(CreatePostResponse::err("server only".to_string())) } } diff --git a/src/api/posts/types.rs b/src/api/posts/types.rs index 5a8d913..3dd53e3 100644 --- a/src/api/posts/types.rs +++ b/src/api/posts/types.rs @@ -15,6 +15,41 @@ pub struct CreatePostResponse { pub slug: Option, } +/// 这些构造器只在 server function body 内调用,WASM 端因 cfg gate 剥离了调用点, +/// 故对非 server 构建允许 dead_code。 +#[cfg_attr(not(feature = "server"), allow(dead_code))] +impl CreatePostResponse { + /// 构造失败响应(post_id / slug 均为 None)。 + pub fn err(message: impl Into) -> Self { + Self { + success: false, + message: message.into(), + post_id: None, + slug: None, + } + } + + /// 构造成功响应,携带新文章 id 与 slug。 + pub fn ok(message: impl Into, post_id: i32, slug: impl Into) -> Self { + Self { + success: true, + message: message.into(), + post_id: Some(post_id), + slug: Some(slug.into()), + } + } + + /// 构造成功响应(无关联 id/slug,用于批量操作)。 + pub fn ok_msg(message: impl Into) -> Self { + Self { + success: true, + message: message.into(), + post_id: None, + slug: None, + } + } +} + /// 文章列表响应。 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PostListResponse { diff --git a/src/api/posts/update.rs b/src/api/posts/update.rs index bbb6867..45f5099 100644 --- a/src/api/posts/update.rs +++ b/src/api/posts/update.rs @@ -84,12 +84,7 @@ pub async fn update_post( .is_some(); if !exists { - return Ok(CreatePostResponse { - success: false, - message: "文章不存在或无权限".to_string(), - post_id: None, - slug: None, - }); + return Ok(CreatePostResponse::err("文章不存在或无权限".to_string())); } // 确定基础 slug:用户传入时校验格式,否则由标题生成。 @@ -97,12 +92,7 @@ pub async fn update_post( Some(ref s) if !s.trim().is_empty() => { let s = s.trim(); if !crate::api::slug::is_valid_slug(s) { - return Ok(CreatePostResponse { - success: false, - message: "slug 格式无效".to_string(), - post_id: None, - slug: None, - }); + return Ok(CreatePostResponse::err("slug 格式无效".to_string())); } s.to_string() } @@ -180,12 +170,7 @@ pub async fn update_post( .map_err(AppError::tx)?; if updated == 0 { - return Ok(CreatePostResponse { - success: false, - message: "文章不存在或无权限".to_string(), - post_id: None, - slug: None, - }); + return Ok(CreatePostResponse::err("文章不存在或无权限".to_string())); } let tags_cleaned = clean_tags(&tags); @@ -226,21 +211,11 @@ pub async fn update_post( // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 crate::ssr_cache::bump_global_generation(); - Ok(CreatePostResponse { - success: true, - message: "更新成功".to_string(), - post_id: Some(post_id), - slug: Some(final_slug), - }) + Ok(CreatePostResponse::ok("更新成功".to_string(), post_id, final_slug)) } #[cfg(not(feature = "server"))] { - Ok(CreatePostResponse { - success: false, - message: "server only".to_string(), - post_id: None, - slug: None, - }) + Ok(CreatePostResponse::err("server only".to_string())) } }