P0 blockers: - Fix migration numbering conflict and duplicate indexes - Change comments.post_id FK to ON DELETE CASCADE - Restrict public post detail endpoint to published posts only - Fix rate-limiting IP extraction and fallback to ConnectInfo - Harden HTML sanitizer: deny unknown URL schemes, restrict data URIs - Remove session token from login response body - Enforce image pixel/dimension limits on upload and serving P1 high-risk: - Validate uploads by magic bytes and decode GIF/WebP - Add pagination/rate-limiting to search, tag posts, and comments - Make first-admin registration and slug uniqueness check atomic - HTML-escape comment author fields - Improve HTML minify cache key and skip admin/error responses - Add mobile navigation menu P2 accessibility/quality: - Associate form labels with inputs - Key PostDetail article by slug to re-init scripts on navigation - Improve image viewer keyboard accessibility - Make theme toggle SSR-friendly and add aria-label - Invalidate slug 404 cache on create and pending count on new comment - Deduplicate tags case-insensitively P3 cleanup: - Remove unused tower-http dependency, expand make clean - Configure DB pool timeouts and verified recycling - Run background cleanup tasks immediately on startup - Use SHA-256 for stable disk cache keys - Log DB errors with Display instead of Debug - Update README migration instructions All tests pass (321), clippy clean, dx check clean.
88 lines
3.0 KiB
Rust
88 lines
3.0 KiB
Rust
//! 前端评论读取接口:已审核评论列表与评论计数。
|
||
//!
|
||
//! 结果按文章 id 缓存,Dioxus server function 注册在 `/api` 路径下。
|
||
//! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。
|
||
|
||
use crate::api::comments::types::*;
|
||
use dioxus::prelude::*;
|
||
|
||
/// 获取指定文章的已审核评论列表。
|
||
///
|
||
/// 优先命中缓存;按 id 升序返回,便于前端构建嵌套树。
|
||
#[server(GetComments, "/api")]
|
||
pub async fn get_comments(post_id: i32) -> Result<CommentTreeResponse, ServerFnError> {
|
||
#[cfg(feature = "server")]
|
||
{
|
||
use crate::api::comments::helpers::row_to_public_comment;
|
||
use crate::api::error::AppError;
|
||
use crate::cache;
|
||
use crate::db::pool::get_conn;
|
||
|
||
if let Some(cached) = cache::get_comments_by_post(post_id).await {
|
||
let count = cached.len() as i64;
|
||
return Ok(CommentTreeResponse {
|
||
comments: cached,
|
||
count,
|
||
});
|
||
}
|
||
|
||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||
|
||
let rows = client
|
||
.query(
|
||
"SELECT id, parent_id, depth, author_name, author_email, author_url, content_html, created_at \
|
||
FROM comments \
|
||
WHERE post_id = $1 AND status = 'approved' AND deleted_at IS NULL \
|
||
AND EXISTS (SELECT 1 FROM posts p WHERE p.id = $1 AND p.status = 'published' AND p.deleted_at IS NULL) \
|
||
ORDER BY id ASC \
|
||
LIMIT 200",
|
||
&[&post_id],
|
||
)
|
||
.await
|
||
.map_err(AppError::query)?;
|
||
|
||
let comments: Vec<_> = rows.iter().map(row_to_public_comment).collect();
|
||
let count = comments.len() as i64;
|
||
|
||
cache::set_comments_by_post(post_id, comments.clone()).await;
|
||
|
||
Ok(CommentTreeResponse { comments, count })
|
||
}
|
||
#[cfg(not(feature = "server"))]
|
||
unreachable!()
|
||
}
|
||
|
||
/// 获取指定文章的已审核评论数量。
|
||
///
|
||
/// 优先命中缓存;未命中时执行 COUNT 查询并写入缓存。
|
||
#[server(GetCommentCount, "/api")]
|
||
pub async fn get_comment_count(post_id: i32) -> Result<CommentCountResponse, ServerFnError> {
|
||
#[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!()
|
||
}
|