diff --git a/src/api/posts/trash.rs b/src/api/posts/trash.rs index 38fdd3e..16f7ca2 100644 --- a/src/api/posts/trash.rs +++ b/src/api/posts/trash.rs @@ -1,6 +1,7 @@ //! 回收站操作接口:恢复、彻底删除、批量操作与一键清空。 //! -//! 所有接口需要 admin 权限,操作后清空全部文章相关缓存。 +//! 所有接口需要 admin 权限,操作后按影响范围精准失效缓存; +//! 仅在影响集很大(如批量清空)时才回退到全量缓存失效。 //! Dioxus server function,注册在 `/api` 路径下。 //! 仅在 `feature = "server"` 启用的服务端构建中执行数据库操作。 @@ -16,6 +17,11 @@ use crate::api::slug::ensure_unique_slug; #[cfg(feature = "server")] use crate::db::pool::get_conn; +/// 批量/清空操作使用精准失效的最大记录数阈值。 +/// 超过该阈值时回退到 `invalidate_all_post_caches()`,避免大量串行缓存操作。 +#[cfg(feature = "server")] +const PRECISE_INVALIDATION_LIMIT: usize = 50; + /// 恢复一篇已删除的文章(将 deleted_at 置空)。 /// /// 若该文章原始 slug 已被其他未删除文章占用,自动追加数字后缀。 @@ -28,7 +34,7 @@ pub async fn restore_post(post_id: i32) -> Result Result = tag_rows.iter().map(|r| r.get(0)).collect(); + // 置空 deleted_at,并更新 slug(可能已加后缀)。 let result = tx .execute( @@ -71,7 +86,13 @@ pub async fn restore_post(post_id: i32) -> Result Result = tag_rows.iter().map(|r| r.get(0)).collect(); + let result = client .execute( "DELETE FROM posts WHERE id = $1 AND deleted_at IS NOT NULL", @@ -121,13 +170,18 @@ pub async fn purge_post(post_id: i32) -> Result) -> Result = Vec::with_capacity(post_ids.len() * 2); + let mut affected_tags: std::collections::HashSet = + std::collections::HashSet::new(); + for id in &post_ids { let row = tx .query_opt( @@ -174,6 +232,18 @@ pub async fn batch_restore_posts(post_ids: Vec) -> Result) -> Result = + affected_slugs.into_iter().collect(); + crate::cache::invalidate_post_lists(); + crate::cache::invalidate_all_tags(); + crate::cache::invalidate_post_stats(); + for slug in &unique_slugs { + crate::cache::invalidate_post_by_slug(slug).await; + } + crate::cache::invalidate_tag_posts_for(&affected_tags.into_iter().collect::>()).await; Ok(CreatePostResponse { success: true, @@ -225,8 +307,34 @@ pub async fn batch_purge_posts(post_ids: Vec) -> Result = slug_rows.iter().map(|r| r.get(0)).collect(); + + let tag_rows = client + .query( + "SELECT t.name FROM tags t JOIN post_tags pt ON t.id = pt.tag_id WHERE pt.post_id = ANY($1)", + &[&post_ids], + ) + .await + .map_err(AppError::query)?; + let tags: Vec = tag_rows.iter().map(|r| r.get(0)).collect(); + + (slugs, tags) + } else { + (Vec::new(), Vec::new()) + }; + let result = client .execute( "DELETE FROM posts WHERE id = ANY($1) AND deleted_at IS NOT NULL", @@ -235,7 +343,18 @@ pub async fn batch_purge_posts(post_ids: Vec) -> Result Result { { let client = get_conn().await.map_err(AppError::db_conn)?; + // 记录数较少时查询 slug 与标签,使用精准失效;否则回退到全量失效。 + let count_row = client + .query_one( + "SELECT COUNT(*) FROM posts WHERE deleted_at IS NOT NULL", + &[], + ) + .await + .map_err(AppError::query)?; + let count: i64 = count_row.get(0); + let use_precise = count > 0 && (count as usize) <= PRECISE_INVALIDATION_LIMIT; + + let (slugs, tags) = if use_precise { + let slug_rows = client + .query( + "SELECT slug FROM posts WHERE deleted_at IS NOT NULL", + &[], + ) + .await + .map_err(AppError::query)?; + let slugs: Vec = slug_rows.iter().map(|r| r.get(0)).collect(); + + let tag_rows = client + .query( + "SELECT t.name FROM tags t JOIN post_tags pt ON t.id = pt.tag_id JOIN posts p ON p.id = pt.post_id WHERE p.deleted_at IS NOT NULL", + &[], + ) + .await + .map_err(AppError::query)?; + let tags: Vec = tag_rows.iter().map(|r| r.get(0)).collect(); + + (slugs, tags) + } else { + (Vec::new(), Vec::new()) + }; + let result = client .execute("DELETE FROM posts WHERE deleted_at IS NOT NULL", &[]) .await .map_err(AppError::query)?; - crate::cache::invalidate_all_post_caches(); + crate::cache::invalidate_post_lists(); + crate::cache::invalidate_all_tags(); + crate::cache::invalidate_post_stats(); + if use_precise { + for slug in &slugs { + crate::cache::invalidate_post_by_slug(slug).await; + } + crate::cache::invalidate_tag_posts_for(&tags).await; + } else { + // 影响集过大时回退到全量失效,避免大量串行缓存操作。 + crate::cache::invalidate_all_post_caches(); + } Ok(CreatePostResponse { success: true,