From 82ab190e0d6ef1f1fe4317a2fc797c11b9f8b834 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 18 Jun 2026 13:31:37 +0800 Subject: [PATCH] fix(comments): make duplicate-check atomic with transaction; index content_hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 查重 SELECT 与 INSERT 包进同一事务,串行化并发请求缩小重复窗口(M4); 重复时 rollback 空事务。content_hash 加索引(非唯一,避免误杀不同作者 发相同短内容的合法场景)加速 5 分钟窗口查重,原先全表扫。 --- migrations/013_comment_content_hash_index.sql | 5 +++++ src/api/comments/create.rs | 13 ++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 migrations/013_comment_content_hash_index.sql diff --git a/migrations/013_comment_content_hash_index.sql b/migrations/013_comment_content_hash_index.sql new file mode 100644 index 0000000..7f32392 --- /dev/null +++ b/migrations/013_comment_content_hash_index.sql @@ -0,0 +1,5 @@ +-- 评论内容哈希索引,加速 5 分钟窗口内的重复检测查询。 +-- 注意不加 UNIQUE 约束:content_hash 基于 parent_id+author+content, +-- 不同作者发相同内容(如"顶"、"+1")是合法的,唯一约束会误杀。 +CREATE INDEX IF NOT EXISTS idx_comments_content_hash + ON comments(content_hash); diff --git a/src/api/comments/create.rs b/src/api/comments/create.rs index df51a18..a6f0a5d 100644 --- a/src/api/comments/create.rs +++ b/src/api/comments/create.rs @@ -92,7 +92,7 @@ pub async fn create_comment( }); } - let client = get_conn().await.map_err(AppError::db_conn)?; + let mut client = get_conn().await.map_err(AppError::db_conn)?; // 确认目标文章存在且处于已发布状态。 let post_row = client @@ -196,7 +196,10 @@ pub async fn create_comment( // 基于文章、父评论、作者与内容计算哈希,防止短时间重复提交。 let content_hash = compute_content_hash(post_id, parent_id, &author_name, &content_md); - let dup: Option = client + // 查重与插入必须在同一事务内,避免并发请求双双通过查重窗口(M4)。 + let tx = client.transaction().await.map_err(AppError::query)?; + + let dup: Option = tx .query_opt( "SELECT id FROM comments WHERE post_id = $1 AND content_hash = $2 AND created_at > NOW() - INTERVAL '5 minutes'", &[&post_id, &content_hash], @@ -206,6 +209,8 @@ pub async fn create_comment( .map(|r| r.get(0)); if dup.is_some() { + // 重复:回滚空事务后返回。 + tx.rollback().await.ok(); return Ok(CommentResponse { success: false, message: "请勿重复提交".to_string(), @@ -246,7 +251,7 @@ pub async fn create_comment( }; // 插入评论,默认状态为 pending,等待管理员审核。 - let row = client + let row = tx .query_one( "INSERT INTO comments \ (post_id, parent_id, depth, author_name, author_email, author_url, \ @@ -270,6 +275,8 @@ pub async fn create_comment( .await .map_err(AppError::query)?; + tx.commit().await.map_err(AppError::query)?; + let comment_id: i64 = row.get(0); // 根据邮箱生成 Gravatar 头像链接。