fix(posts): lock rows and read metadata in transaction for delete/purge/restore

This commit is contained in:
xfy 2026-06-17 16:58:54 +08:00
parent 1092fbb3ce
commit ca212a2aab
2 changed files with 22 additions and 16 deletions

View File

@ -25,12 +25,13 @@ pub async fn delete_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErr
#[cfg(feature = "server")] #[cfg(feature = "server")]
{ {
let client = get_conn().await.map_err(AppError::db_conn)?; let mut client = get_conn().await.map_err(AppError::db_conn)?;
let tx = client.transaction().await.map_err(AppError::tx)?;
// 在软删除前查询 slug 与标签,用于后续精准失效缓存 // 在事务内锁定行并读取 slug避免并发更新导致缓存失效目标过期
let slug_row = client let slug_row = tx
.query_opt( .query_opt(
"SELECT slug FROM posts WHERE id = $1 AND deleted_at IS NULL", "SELECT slug FROM posts WHERE id = $1 AND deleted_at IS NULL FOR UPDATE",
&[&post_id], &[&post_id],
) )
.await .await
@ -46,7 +47,7 @@ pub async fn delete_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErr
}; };
let slug: String = slug_row.get(0); let slug: String = slug_row.get(0);
let tag_rows = client let tag_rows = tx
.query( .query(
"SELECT t.name FROM tags t JOIN post_tags pt ON t.id = pt.tag_id WHERE pt.post_id = $1", "SELECT t.name FROM tags t JOIN post_tags pt ON t.id = pt.tag_id WHERE pt.post_id = $1",
&[&post_id], &[&post_id],
@ -56,13 +57,13 @@ pub async fn delete_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErr
let tags: Vec<String> = tag_rows.iter().map(|r| r.get(0)).collect(); let tags: Vec<String> = tag_rows.iter().map(|r| r.get(0)).collect();
// 软删除:仅影响未被删除的文章。 // 软删除:仅影响未被删除的文章。
let result = client let result = tx
.execute( .execute(
"UPDATE posts SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL", "UPDATE posts SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL",
&[&post_id], &[&post_id],
) )
.await .await
.map_err(AppError::query)?; .map_err(AppError::tx)?;
if result == 0 { if result == 0 {
return Ok(CreatePostResponse { return Ok(CreatePostResponse {
@ -73,6 +74,8 @@ pub async fn delete_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErr
}); });
} }
tx.commit().await.map_err(AppError::tx)?;
// 删除后按影响范围精准失效缓存。 // 删除后按影响范围精准失效缓存。
crate::cache::invalidate_post_lists(); crate::cache::invalidate_post_lists();
crate::cache::invalidate_all_tags(); crate::cache::invalidate_all_tags();

View File

@ -34,10 +34,10 @@ pub async fn restore_post(post_id: i32) -> Result<CreatePostResponse, ServerFnEr
let mut client = get_conn().await.map_err(AppError::db_conn)?; let mut client = get_conn().await.map_err(AppError::db_conn)?;
let tx = client.transaction().await.map_err(AppError::tx)?; let tx = client.transaction().await.map_err(AppError::tx)?;
// 读取待恢复文章的当前 slug、标签与是否确已删除。 // 在事务内锁定行并读取当前 slug、标签与是否确已删除。
let row = tx let row = tx
.query_opt( .query_opt(
"SELECT slug FROM posts WHERE id = $1 AND deleted_at IS NOT NULL", "SELECT slug FROM posts WHERE id = $1 AND deleted_at IS NOT NULL FOR UPDATE",
&[&post_id], &[&post_id],
) )
.await .await
@ -123,12 +123,13 @@ pub async fn purge_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErro
#[cfg(feature = "server")] #[cfg(feature = "server")]
{ {
let client = get_conn().await.map_err(AppError::db_conn)?; let mut client = get_conn().await.map_err(AppError::db_conn)?;
let tx = client.transaction().await.map_err(AppError::tx)?;
// 物理删除前查询 slug 与标签,用于精准失效缓存 // 在事务内锁定行并读取 slug 与标签,避免并发更新导致缓存失效目标过期
let slug_row = client let slug_row = tx
.query_opt( .query_opt(
"SELECT slug FROM posts WHERE id = $1 AND deleted_at IS NOT NULL", "SELECT slug FROM posts WHERE id = $1 AND deleted_at IS NOT NULL FOR UPDATE",
&[&post_id], &[&post_id],
) )
.await .await
@ -144,7 +145,7 @@ pub async fn purge_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErro
}; };
let slug: String = slug_row.get(0); let slug: String = slug_row.get(0);
let tag_rows = client let tag_rows = tx
.query( .query(
"SELECT t.name FROM tags t JOIN post_tags pt ON t.id = pt.tag_id WHERE pt.post_id = $1", "SELECT t.name FROM tags t JOIN post_tags pt ON t.id = pt.tag_id WHERE pt.post_id = $1",
&[&post_id], &[&post_id],
@ -153,13 +154,13 @@ pub async fn purge_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErro
.map_err(AppError::query)?; .map_err(AppError::query)?;
let tags: Vec<String> = tag_rows.iter().map(|r| r.get(0)).collect(); let tags: Vec<String> = tag_rows.iter().map(|r| r.get(0)).collect();
let result = client let result = tx
.execute( .execute(
"DELETE FROM posts WHERE id = $1 AND deleted_at IS NOT NULL", "DELETE FROM posts WHERE id = $1 AND deleted_at IS NOT NULL",
&[&post_id], &[&post_id],
) )
.await .await
.map_err(AppError::query)?; .map_err(AppError::tx)?;
if result == 0 { if result == 0 {
return Ok(CreatePostResponse { return Ok(CreatePostResponse {
@ -170,6 +171,8 @@ pub async fn purge_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErro
}); });
} }
tx.commit().await.map_err(AppError::tx)?;
// 精准失效相关缓存。 // 精准失效相关缓存。
crate::cache::invalidate_post_lists(); crate::cache::invalidate_post_lists();
crate::cache::invalidate_all_tags(); crate::cache::invalidate_all_tags();