fix(comments): make duplicate-check atomic with transaction; index content_hash

查重 SELECT 与 INSERT 包进同一事务,串行化并发请求缩小重复窗口(M4);
重复时 rollback 空事务。content_hash 加索引(非唯一,避免误杀不同作者
发相同短内容的合法场景)加速 5 分钟窗口查重,原先全表扫。
This commit is contained in:
xfy 2026-06-18 13:31:37 +08:00
parent 1c6974ca68
commit 82ab190e0d
2 changed files with 15 additions and 3 deletions

View File

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

View File

@ -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<i64> = client
// 查重与插入必须在同一事务内避免并发请求双双通过查重窗口M4
let tx = client.transaction().await.map_err(AppError::query)?;
let dup: Option<i64> = 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 头像链接。