perf(posts): add optional pagination to get_posts_by_tag, fix total count
- get_posts_by_tag 现接受 page/per_page 可选参数;两者均 None 时返回全部 (上限 200,用于无翻页 UI 的标签详情页),均提供时走标准分页。 - 修正 total:不再用 posts.len(),改为真实 COUNT(*),即使被 LIMIT 截断 也返回完整计数。 - 新增 CacheKey::PostsByTagPage 分页缓存键,与不分页键 PostsByTag 共存。 - 前端 tags.rs 传 (None, None) 保持原全部展示行为。
This commit is contained in:
parent
2dda168b19
commit
79cb809010
@ -235,12 +235,37 @@ pub async fn list_deleted_posts(
|
|||||||
|
|
||||||
/// 获取指定标签下的已发布文章列表。
|
/// 获取指定标签下的已发布文章列表。
|
||||||
///
|
///
|
||||||
/// 优先命中缓存;当前实现返回全部匹配文章,因此 total 用 posts.len() 计算。
|
/// 分页参数为可选:
|
||||||
|
/// - `page` 与 `per_page` 均为 `None` 时返回该标签下全部已发布文章(上限 200),
|
||||||
|
/// 用于无分页 UI 的标签详情页。
|
||||||
|
/// - 两者均提供时走标准分页(经 `clamp_pagination` 钳制)。
|
||||||
|
/// 结果缓存于按标签的分页键空间。
|
||||||
#[server(GetPostsByTag, "/api")]
|
#[server(GetPostsByTag, "/api")]
|
||||||
pub async fn get_posts_by_tag(tag_name: String) -> Result<PostListResponse, ServerFnError> {
|
pub async fn get_posts_by_tag(
|
||||||
|
tag_name: String,
|
||||||
|
page: Option<i32>,
|
||||||
|
per_page: Option<i32>,
|
||||||
|
) -> Result<PostListResponse, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
if let Some((cached_posts, cached_total)) = crate::cache::get_posts_by_tag(&tag_name).await
|
// 仅当两个分页参数都提供时才走分页路径;任一为 None 视为不分页。
|
||||||
|
let (page, per_page) = match (page, per_page) {
|
||||||
|
(Some(p), Some(pp)) => (Some(p), Some(pp)),
|
||||||
|
_ => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||||||
|
|
||||||
|
if let (Some(page), Some(per_page)) = (page, per_page) {
|
||||||
|
// 分页路径:钳制参数,走分页缓存键。
|
||||||
|
let (page, per_page) = clamp_pagination(page, per_page);
|
||||||
|
let cache_key = crate::cache::CacheKey::PostsByTagPage {
|
||||||
|
tag: tag_name.clone(),
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
};
|
||||||
|
if let Some((cached_posts, cached_total)) =
|
||||||
|
crate::cache::get_posts_by_tag_paged(&cache_key).await
|
||||||
{
|
{
|
||||||
return Ok(PostListResponse {
|
return Ok(PostListResponse {
|
||||||
posts: cached_posts,
|
posts: cached_posts,
|
||||||
@ -248,9 +273,73 @@ pub async fn get_posts_by_tag(tag_name: String) -> Result<PostListResponse, Serv
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
// 标签下已发布文章总数。
|
||||||
|
let total: i64 = client
|
||||||
|
.query_one(
|
||||||
|
"SELECT COUNT(*) FROM posts p
|
||||||
|
JOIN post_tags pt ON p.id = pt.post_id
|
||||||
|
JOIN tags t ON pt.tag_id = t.id
|
||||||
|
WHERE t.name = $1 AND p.status = 'published' AND p.deleted_at IS NULL",
|
||||||
|
&[&tag_name],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(AppError::query)?
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
let offset = ((page - 1).max(0) as i64) * (per_page as i64);
|
||||||
|
let limit = per_page as i64;
|
||||||
|
let rows = client
|
||||||
|
.query(
|
||||||
|
"SELECT
|
||||||
|
p.id, p.author_id, p.title, p.slug, p.summary, p.status,
|
||||||
|
p.published_at, p.created_at, p.updated_at, p.cover_image,
|
||||||
|
p.word_count, p.reading_time,
|
||||||
|
COALESCE(array_agg(t2.name) FILTER (WHERE t2.name IS NOT NULL), '{}') as tags
|
||||||
|
FROM posts p
|
||||||
|
JOIN post_tags pt ON p.id = pt.post_id
|
||||||
|
JOIN tags t ON pt.tag_id = t.id
|
||||||
|
LEFT JOIN post_tags pt2 ON p.id = pt2.post_id
|
||||||
|
LEFT JOIN tags t2 ON pt2.tag_id = t2.id
|
||||||
|
WHERE t.name = $1 AND p.status = 'published' AND p.deleted_at IS NULL
|
||||||
|
GROUP BY p.id
|
||||||
|
ORDER BY p.published_at DESC
|
||||||
|
LIMIT $2 OFFSET $3",
|
||||||
|
&[&tag_name, &limit, &offset],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(AppError::query)?;
|
||||||
|
|
||||||
|
let mut posts = Vec::new();
|
||||||
|
for row in &rows {
|
||||||
|
posts.push(row_to_post_list_item(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::cache::set_posts_by_tag_paged(&cache_key, posts.clone(), total).await;
|
||||||
|
Ok(PostListResponse { posts, total })
|
||||||
|
} else {
|
||||||
|
// 不分页路径:返回全部(上限 200),用于无翻页 UI 的标签详情页。
|
||||||
|
if let Some((cached_posts, cached_total)) =
|
||||||
|
crate::cache::get_posts_by_tag(&tag_name).await
|
||||||
|
{
|
||||||
|
return Ok(PostListResponse {
|
||||||
|
posts: cached_posts,
|
||||||
|
total: cached_total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 真实总数(即使被 LIMIT 截断也返回完整计数)。
|
||||||
|
let total: i64 = client
|
||||||
|
.query_one(
|
||||||
|
"SELECT COUNT(*) FROM posts p
|
||||||
|
JOIN post_tags pt ON p.id = pt.post_id
|
||||||
|
JOIN tags t ON pt.tag_id = t.id
|
||||||
|
WHERE t.name = $1 AND p.status = 'published' AND p.deleted_at IS NULL",
|
||||||
|
&[&tag_name],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(AppError::query)?
|
||||||
|
.get(0);
|
||||||
|
|
||||||
// 通过 JOIN 筛选含目标标签的已发布文章,并聚合该文章的所有标签。
|
|
||||||
let rows = client
|
let rows = client
|
||||||
.query(
|
.query(
|
||||||
"SELECT
|
"SELECT
|
||||||
@ -277,12 +366,11 @@ pub async fn get_posts_by_tag(tag_name: String) -> Result<PostListResponse, Serv
|
|||||||
posts.push(row_to_post_list_item(row));
|
posts.push(row_to_post_list_item(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前查询未分页,返回全部匹配文章,因此 total 等于结果长度。
|
// total 为真实 COUNT(*),不再用 posts.len()。
|
||||||
// 若后续增加分页,应改为 COUNT(*) 查询。
|
|
||||||
let total = posts.len() as i64;
|
|
||||||
crate::cache::set_posts_by_tag(&tag_name, posts.clone(), total).await;
|
crate::cache::set_posts_by_tag(&tag_name, posts.clone(), total).await;
|
||||||
Ok(PostListResponse { posts, total })
|
Ok(PostListResponse { posts, total })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "server"))]
|
#[cfg(not(feature = "server"))]
|
||||||
{
|
{
|
||||||
|
|||||||
16
src/cache.rs
16
src/cache.rs
@ -74,8 +74,10 @@ pub enum CacheKey {
|
|||||||
AllTags,
|
AllTags,
|
||||||
/// 按 slug 查询的单篇文章。
|
/// 按 slug 查询的单篇文章。
|
||||||
PostBySlug(String),
|
PostBySlug(String),
|
||||||
/// 按标签查询的文章列表。
|
/// 按标签查询的文章列表(不分页,返回全部)。
|
||||||
PostsByTag(String),
|
PostsByTag(String),
|
||||||
|
/// 按标签查询的分页文章列表。
|
||||||
|
PostsByTagPage { tag: String, page: i32, per_page: i32 },
|
||||||
/// 文章统计信息。
|
/// 文章统计信息。
|
||||||
PostStats,
|
PostStats,
|
||||||
/// 某篇文章下的评论列表。
|
/// 某篇文章下的评论列表。
|
||||||
@ -274,6 +276,18 @@ pub async fn set_posts_by_tag(tag: &str, posts: Vec<PostListItem>, total: i64) {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 按标签+分页读取文章列表缓存。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub async fn get_posts_by_tag_paged(key: &CacheKey) -> Option<(Vec<PostListItem>, i64)> {
|
||||||
|
TAG_POSTS_CACHE.get(key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按标签+分页写入文章列表缓存。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub async fn set_posts_by_tag_paged(key: &CacheKey, posts: Vec<PostListItem>, total: i64) {
|
||||||
|
let _ = TAG_POSTS_CACHE.insert(key.clone(), (posts, total)).await;
|
||||||
|
}
|
||||||
|
|
||||||
/// 读取文章统计缓存。
|
/// 读取文章统计缓存。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub async fn get_post_stats() -> Option<PostStats> {
|
pub async fn get_post_stats() -> Option<PostStats> {
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
//!
|
//!
|
||||||
//! 数据获取:
|
//! 数据获取:
|
||||||
//! - 标签云通过 `use_server_future(list_tags)` 获取全部标签信息。
|
//! - 标签云通过 `use_server_future(list_tags)` 获取全部标签信息。
|
||||||
//! - 标签详情通过 `use_server_future` 调用 `get_posts_by_tag(tag)` 获取该标签下的文章列表。
|
//! - 标签详情通过 `use_server_future` 调用 `get_posts_by_tag(tag, None, None)`
|
||||||
|
//! 获取该标签下的全部已发布文章(不分页)。
|
||||||
//! 在 `wasm32` 目标下,这些 server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根;
|
//! 在 `wasm32` 目标下,这些 server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根;
|
||||||
//! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。
|
//! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。
|
||||||
|
|
||||||
@ -109,7 +110,7 @@ pub fn TagDetail(tag: String) -> Element {
|
|||||||
/// 成功时渲染文章总数与文章卡片。
|
/// 成功时渲染文章总数与文章卡片。
|
||||||
#[component]
|
#[component]
|
||||||
fn TagDetailContent(tag: String) -> Element {
|
fn TagDetailContent(tag: String) -> Element {
|
||||||
let posts_res = use_server_future(move || get_posts_by_tag(tag.clone()))?;
|
let posts_res = use_server_future(move || get_posts_by_tag(tag.clone(), None, None))?;
|
||||||
|
|
||||||
// 将结果映射为 (posts, total) 形式以便渲染。
|
// 将结果映射为 (posts, total) 形式以便渲染。
|
||||||
let posts_data = posts_res.read().as_ref().map(|r| match r {
|
let posts_data = posts_res.read().as_ref().map(|r| match r {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user