diff --git a/Cargo.lock b/Cargo.lock index 70e0b16..5044243 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5400,7 +5400,6 @@ dependencies = [ "governor", "hex", "http", - "http-body-util", "image", "js-sys", "lol_html", diff --git a/Cargo.toml b/Cargo.toml index 60be984..f86de3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ rand = { version = "0.8", features = ["getrandom"], optional = true } http = { version = "1", optional = true } axum = { version = "0.8", optional = true, features = ["multipart"] } tower-http = { version = "0.6", optional = true, features = ["compression-full", "timeout", "trace"] } -http-body-util = { version = "0.1", optional = true } + serde_json = "1.0" sha2 = { version = "0.10", optional = true } hex = { version = "0.4", optional = true } @@ -73,7 +73,6 @@ server = [ "dep:dotenvy", "dep:tracing", "dep:tracing-subscriber", - "dep:http-body-util", "dep:lol_html", "dep:syntect", "dep:axum", diff --git a/docs/superpowers/plans/2026-06-11-comment-localstorage.md b/docs/superpowers/plans/2026-06-11-comment-localstorage.md index a2ba428..1bf462b 100644 --- a/docs/superpowers/plans/2026-06-11-comment-localstorage.md +++ b/docs/superpowers/plans/2026-06-11-comment-localstorage.md @@ -73,7 +73,6 @@ Replace `create.rs:190-221` with: let avatar_url = crate::api::comments::helpers::gravatar_url(&author_email); cache::invalidate_comments_by_post(post_id).await; - cache::invalidate_comment_count(post_id).await; Ok(CommentResponse { success: true, @@ -181,9 +180,9 @@ mod check; pub use types::*; pub use create::create_comment; -pub use read::{get_comments, get_comment_count}; +pub use read::get_comments; pub use update::{approve_comment, spam_comment, trash_comment, batch_update_comment_status}; -pub use list::{get_pending_comments, get_pending_count, get_all_comments}; +pub use list::{get_pending_count, get_all_comments}; pub use check::check_pending_status; #[cfg(feature = "server")] diff --git a/migrations/002_posts.sql b/migrations/002_posts.sql index 865bb6b..111a0bb 100644 --- a/migrations/002_posts.sql +++ b/migrations/002_posts.sql @@ -21,7 +21,6 @@ CREATE TABLE IF NOT EXISTS posts ( ); CREATE INDEX idx_posts_status_published ON posts(status, published_at DESC) WHERE deleted_at IS NULL; -CREATE INDEX idx_posts_slug ON posts(slug) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX idx_posts_slug_unique ON posts(slug) WHERE deleted_at IS NULL; CREATE TABLE IF NOT EXISTS tags ( diff --git a/migrations/003_indexes.sql b/migrations/003_indexes.sql index 359943a..c818fa4 100644 --- a/migrations/003_indexes.sql +++ b/migrations/003_indexes.sql @@ -1,10 +1,4 @@ -- 补充索引(已在 002_posts.sql 中创建的索引不再重复定义) --- 标签名称查询 -CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name); - --- 文章标签关联查询(tag 方向) -CREATE INDEX IF NOT EXISTS idx_post_tags_tag_id ON post_tags(tag_id); - -- 用户会话查询 CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); diff --git a/migrations/009_cleanup_duplicate_indexes.sql b/migrations/009_cleanup_duplicate_indexes.sql new file mode 100644 index 0000000..de727a0 --- /dev/null +++ b/migrations/009_cleanup_duplicate_indexes.sql @@ -0,0 +1,6 @@ +-- 清理早期迁移重复创建的索引 +-- 这些索引在新数据库中已不会再被创建 + +DROP INDEX IF EXISTS idx_posts_slug; +DROP INDEX IF EXISTS idx_tags_name; +DROP INDEX IF EXISTS idx_post_tags_tag_id; diff --git a/src/api/comments/create.rs b/src/api/comments/create.rs index 2105d6e..df51a18 100644 --- a/src/api/comments/create.rs +++ b/src/api/comments/create.rs @@ -275,9 +275,8 @@ pub async fn create_comment( // 根据邮箱生成 Gravatar 头像链接。 let avatar_url = crate::api::comments::helpers::gravatar_url(&author_email); - // 新评论可能影响文章评论列表、评论计数与待审核计数,清空相关缓存。 + // 新评论可能影响文章评论列表与待审核计数,清空相关缓存。 cache::invalidate_comments_by_post(post_id).await; - cache::invalidate_comment_count(post_id).await; cache::invalidate_pending_count().await; Ok(CommentResponse { diff --git a/src/api/comments/list.rs b/src/api/comments/list.rs index fb5f354..e2d47cc 100644 --- a/src/api/comments/list.rs +++ b/src/api/comments/list.rs @@ -1,4 +1,4 @@ -//! 评论列表查询接口:后台管理用的待审核列表、全部评论列表与待审核计数。 +//! 评论列表查询接口:后台管理用的全部评论列表与待审核计数。 //! //! 所有接口均需管理员身份,Dioxus server function 注册在 `/api` 路径下。 //! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。 @@ -6,56 +6,6 @@ use crate::api::comments::types::*; use dioxus::prelude::*; -/// 获取待审核评论分页列表。 -/// -/// 每页 20 条,按创建时间倒序排列,并返回总数用于分页。 -#[server(GetPendingComments, "/api")] -pub async fn get_pending_comments(page: i32) -> Result { - #[cfg(feature = "server")] - { - use crate::api::auth::get_current_admin_user; - use crate::api::comments::helpers::row_to_admin_comment; - use crate::api::error::AppError; - use crate::db::pool::get_conn; - - let _admin = get_current_admin_user().await?; - - let page = page.max(1); - let per_page: i64 = 20; - let offset: i64 = (page as i64 - 1) * per_page; - - let client = get_conn().await.map_err(AppError::db_conn)?; - - let total: i64 = client - .query_one( - "SELECT COUNT(*) FROM comments WHERE status = 'pending' AND deleted_at IS NULL", - &[], - ) - .await - .map_err(AppError::query)? - .get(0); - - let rows = client - .query( - "SELECT c.id, c.post_id, c.parent_id, c.depth, c.author_name, c.author_email, \ - c.author_url, c.content_md, c.status, c.created_at, \ - p.title as post_title, p.slug as post_slug \ - FROM comments c JOIN posts p ON c.post_id = p.id \ - WHERE c.status = 'pending' AND c.deleted_at IS NULL \ - ORDER BY c.created_at DESC LIMIT $1 OFFSET $2", - &[&per_page, &offset], - ) - .await - .map_err(AppError::query)?; - - let comments = rows.iter().map(row_to_admin_comment).collect(); - - Ok(PendingCommentsResponse { comments, total }) - } - #[cfg(not(feature = "server"))] - unreachable!() -} - /// 获取待审核评论总数。 /// /// 优先从缓存读取,未命中时查询数据库并写入缓存。 diff --git a/src/api/comments/mod.rs b/src/api/comments/mod.rs index 09bc7ad..1a2d93f 100644 --- a/src/api/comments/mod.rs +++ b/src/api/comments/mod.rs @@ -21,15 +21,9 @@ pub use create::create_comment; /// 获取全部评论分页列表。 #[allow(unused_imports)] pub use list::get_all_comments; -/// 获取待审核评论分页列表。 -#[allow(unused_imports)] -pub use list::get_pending_comments; /// 获取待审核评论总数。 #[allow(unused_imports)] pub use list::get_pending_count; -/// 获取指定文章的已审核评论数量。 -#[allow(unused_imports)] -pub use read::get_comment_count; /// 获取指定文章的已审核评论列表。 pub use read::get_comments; /// 评论 API 的请求与响应数据结构。 diff --git a/src/api/comments/read.rs b/src/api/comments/read.rs index 34dab9e..25198bc 100644 --- a/src/api/comments/read.rs +++ b/src/api/comments/read.rs @@ -1,4 +1,4 @@ -//! 前端评论读取接口:已审核评论列表与评论计数。 +//! 前端评论读取接口:已审核评论列表。 //! //! 结果按文章 id 缓存,Dioxus server function 注册在 `/api` 路径下。 //! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。 @@ -52,36 +52,3 @@ pub async fn get_comments(post_id: i32) -> Result Result { - #[cfg(feature = "server")] - { - use crate::api::error::AppError; - use crate::cache; - use crate::db::pool::get_conn; - - if let Some(cached) = cache::get_comment_count(post_id).await { - return Ok(CommentCountResponse { count: cached }); - } - - let client = get_conn().await.map_err(AppError::db_conn)?; - - let count: i64 = client - .query_one( - "SELECT COUNT(*) FROM comments WHERE post_id = $1 AND status = 'approved' AND deleted_at IS NULL", - &[&post_id], - ) - .await - .map_err(AppError::query)? - .get(0); - - cache::set_comment_count(post_id, count).await; - - Ok(CommentCountResponse { count }) - } - #[cfg(not(feature = "server"))] - unreachable!() -} diff --git a/src/api/comments/types.rs b/src/api/comments/types.rs index 8cf4c7c..da0d677 100644 --- a/src/api/comments/types.rs +++ b/src/api/comments/types.rs @@ -32,30 +32,6 @@ pub struct CommentTreeResponse { pub count: i64, } -/// 评论计数响应。 -/// -/// 此类型仅在服务端函数体中构造;保留 `#[allow(dead_code)]` 以避免 WASM 构建中 -/// 因函数体被剥离而产生的未使用警告。 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[allow(dead_code)] -pub struct CommentCountResponse { - /// 评论数量。 - pub count: i64, -} - -/// 待审核评论列表响应。 -/// -/// 当前前端未直接调用 `get_pending_comments`,此类型仅在服务端函数体中构造; -/// 保留 `#[allow(dead_code)]` 以避免 WASM 构建中因函数体被剥离而产生的未使用警告。 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[allow(dead_code)] -pub struct PendingCommentsResponse { - /// 待审核评论列表。 - pub comments: Vec, - /// 总数。 - pub total: i64, -} - /// 全部评论列表响应。 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AllCommentsResponse { diff --git a/src/api/comments/update.rs b/src/api/comments/update.rs index 6ce6119..ed6a3b8 100644 --- a/src/api/comments/update.rs +++ b/src/api/comments/update.rs @@ -70,7 +70,6 @@ pub async fn approve_comment(id: i64) -> Result .map_err(AppError::query)?; cache::invalidate_comments_by_post(post_id).await; - cache::invalidate_comment_count(post_id).await; cache::invalidate_pending_count().await; Ok(CommentResponse { @@ -124,7 +123,6 @@ pub async fn spam_comment(id: i64) -> Result { if old_status == "approved" { cache::invalidate_comments_by_post(post_id).await; - cache::invalidate_comment_count(post_id).await; } cache::invalidate_pending_count().await; } @@ -178,7 +176,6 @@ pub async fn trash_comment(id: i64) -> Result { .map_err(AppError::query)?; cache::invalidate_comments_by_post(post_id).await; - cache::invalidate_comment_count(post_id).await; cache::invalidate_pending_count().await; } @@ -266,7 +263,6 @@ pub async fn batch_update_comment_status( cache::invalidate_pending_count().await; for pid in post_ids { cache::invalidate_comments_by_post(pid).await; - cache::invalidate_comment_count(pid).await; } Ok(BatchStatusResponse { diff --git a/src/cache.rs b/src/cache.rs index f393e41..dd0bea7 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -44,10 +44,6 @@ const TTL_TAG_POSTS: Duration = Duration::from_secs(120); #[cfg(feature = "server")] const TTL_COMMENTS: Duration = Duration::from_secs(60); -/// 评论数量缓存 TTL:60 秒。 -#[cfg(feature = "server")] -const TTL_COMMENT_COUNT: Duration = Duration::from_secs(60); - /// 待审核评论数量缓存 TTL:10 秒,因管理后台需要较实时数据。 #[cfg(feature = "server")] const TTL_PENDING_COUNT: Duration = Duration::from_secs(10); @@ -74,8 +70,6 @@ pub enum CacheKey { PostStats, /// 某篇文章下的评论列表。 CommentsByPost { post_id: i32 }, - /// 某篇文章的评论数量。 - CommentCount { post_id: i32 }, /// 待审核评论总数。 PendingCommentCount, } @@ -149,10 +143,6 @@ static TAG_POSTS_CACHE: LazyLock = LazyLock::new(|| { #[cfg(feature = "server")] pub type CommentListCache = Cache>; -/// 评论数量缓存类型。 -#[cfg(feature = "server")] -pub type CommentCountCache = Cache; - /// 全局评论列表缓存实例,最大容量 200。 #[cfg(feature = "server")] static COMMENT_CACHE: LazyLock = LazyLock::new(|| { @@ -162,18 +152,9 @@ static COMMENT_CACHE: LazyLock = LazyLock::new(|| { .build() }); -/// 全局评论数量缓存实例,最大容量 200。 -#[cfg(feature = "server")] -static COMMENT_COUNT_CACHE: LazyLock = LazyLock::new(|| { - Cache::builder() - .max_capacity(200) - .time_to_live(TTL_COMMENT_COUNT) - .build() -}); - /// 全局待审核评论数量缓存实例,最大容量 10。 #[cfg(feature = "server")] -static PENDING_COUNT_CACHE: LazyLock = LazyLock::new(|| { +static PENDING_COUNT_CACHE: LazyLock> = LazyLock::new(|| { Cache::builder() .max_capacity(10) .time_to_live(TTL_PENDING_COUNT) @@ -333,22 +314,6 @@ pub async fn set_comments_by_post(post_id: i32, comments: Vec) { .await; } -/// 按文章主键读取评论数量缓存。 -#[cfg(feature = "server")] -pub async fn get_comment_count(post_id: i32) -> Option { - COMMENT_COUNT_CACHE - .get(&CacheKey::CommentCount { post_id }) - .await -} - -/// 按文章主键写入评论数量缓存。 -#[cfg(feature = "server")] -pub async fn set_comment_count(post_id: i32, count: i64) { - let _ = COMMENT_COUNT_CACHE - .insert(CacheKey::CommentCount { post_id }, count) - .await; -} - /// 读取待审核评论总数缓存。 #[cfg(feature = "server")] pub async fn get_pending_count() -> Option { @@ -373,14 +338,6 @@ pub async fn invalidate_comments_by_post(post_id: i32) { .await; } -/// 按文章主键失效评论数量缓存。 -#[cfg(feature = "server")] -pub async fn invalidate_comment_count(post_id: i32) { - COMMENT_COUNT_CACHE - .invalidate(&CacheKey::CommentCount { post_id }) - .await; -} - /// 失效待审核评论总数缓存。 #[cfg(feature = "server")] pub async fn invalidate_pending_count() { @@ -554,16 +511,6 @@ mod tests { assert_eq!(cached.unwrap().len(), 1); } - #[tokio::test] - #[serial] - async fn comment_count_cache_roundtrip() { - set_comment_count(42, 15).await; - let cached = get_comment_count(42).await; - - assert!(cached.is_some()); - assert_eq!(cached.unwrap(), 15); - } - #[tokio::test] #[serial] async fn pending_count_cache_roundtrip() { @@ -584,16 +531,6 @@ mod tests { assert!(get_comments_by_post(99).await.is_none()); } - #[tokio::test] - #[serial] - async fn comment_count_invalidation() { - set_comment_count(99, 5).await; - assert!(get_comment_count(99).await.is_some()); - - invalidate_comment_count(99).await; - assert!(get_comment_count(99).await.is_none()); - } - #[tokio::test] #[serial] async fn pending_count_invalidation() {