//! 文章详情查询接口。 //! //! 提供按 id(管理员)与按 slug(公开)两种方式获取文章, //! 其中按 slug 查询包含上下篇导航并启用缓存。 //! Dioxus server function,注册在 `/api` 路径下。 //! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。 use dioxus::prelude::*; #[cfg(feature = "server")] use super::helpers::{get_current_admin_user, row_to_post_full, row_to_post_list}; use super::types::SinglePostResponse; #[cfg(feature = "server")] use crate::api::error::AppError; use crate::db::pool::get_conn; /// 根据文章 id 获取详情。 /// /// 需要 admin 权限;不缓存,用于管理后台编辑等场景。 #[server(GetPostById, "/api")] pub async fn get_post_by_id(post_id: i32) -> Result { let _user = get_current_admin_user().await?; #[cfg(feature = "server")] { let client = get_conn().await.map_err(AppError::db_conn)?; let row = client .query_opt( "SELECT p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html, p.status, p.published_at, p.created_at, p.updated_at, p.cover_image, COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags FROM posts p LEFT JOIN post_tags pt ON p.id = pt.post_id LEFT JOIN tags t ON pt.tag_id = t.id WHERE p.id = $1 AND p.deleted_at IS NULL GROUP BY p.id", &[&post_id], ) .await .map_err(AppError::query)?; let post = match row { Some(row) => Some(row_to_post_list(&client, &row).await), None => None, }; Ok(SinglePostResponse { post }) } #[cfg(not(feature = "server"))] { Ok(SinglePostResponse { post: None }) } } /// 根据 slug 获取公开文章详情。 /// /// 优先命中缓存;未命中时查询数据库,并附带基于 published_at 的上一篇/下一篇导航。 #[server(GetPostBySlug, "/api")] pub async fn get_post_by_slug(slug: String) -> Result { #[cfg(feature = "server")] { if let Some(cached) = crate::cache::get_post_by_slug(&slug).await { return Ok(SinglePostResponse { post: cached }); } let client = get_conn().await.map_err(AppError::db_conn)?; // 使用 LATERAL JOIN 查询按 published_at 排序的相邻文章。 let row = client .query_opt( "SELECT p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html, p.status, p.published_at, p.created_at, p.updated_at, p.cover_image, COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags, prev.title as prev_title, prev.slug as prev_slug, next.title as next_title, next.slug as next_slug FROM posts p LEFT JOIN post_tags pt ON p.id = pt.post_id LEFT JOIN tags t ON pt.tag_id = t.id LEFT JOIN LATERAL ( SELECT title, slug FROM posts WHERE published_at < p.published_at AND status = 'published' AND deleted_at IS NULL ORDER BY published_at DESC LIMIT 1 ) prev ON true LEFT JOIN LATERAL ( SELECT title, slug FROM posts WHERE published_at > p.published_at AND status = 'published' AND deleted_at IS NULL ORDER BY published_at ASC LIMIT 1 ) next ON true WHERE p.slug = $1 AND p.deleted_at IS NULL GROUP BY p.id, prev.title, prev.slug, next.title, next.slug", &[&slug], ) .await .map_err(AppError::query)?; let post = match row { Some(row) => Some(row_to_post_full(&client, &row).await), None => None, }; if post.is_some() { crate::cache::set_post_by_slug(&slug, post.clone()).await; } Ok(SinglePostResponse { post }) } #[cfg(not(feature = "server"))] { Ok(SinglePostResponse { post: None }) } }