使用axum构建博客系统 - 文章详情

发布时间 2023-10-21 20:30:12作者: CrossPython

本章将实现博客文章的详情显示功能。

数据库视图

CREATE VIEW v_topic_cat_detail AS
  SELECT t.id, title, html, hit, dateline,category_id,t.is_del,
         c.name AS category_name
    FROM
      topics AS t
      INNER JOIN categories AS c
          ON t.category_id=c.id
   WHERE c.is_del = false
         ;

该视图和v_topic_cat_list唯一不同的地方在于,它要的是topicshtml字段。

数据模型

// src/model.rs
#[derive(PostgresMapper, Serialize)]
#[pg_mapper(table="v_topic_cat_detail")]
pub struct TopicDetail{
    pub id:i64,
    pub title: String,
    pub category_id:i32,
    pub html:String,
    pub hit:i32,
    pub dateline:time::SystemTime,
    pub is_del:bool,
    pub category_name:String,
}
impl TopicDetail {
    pub fn dateline(&self) ->String {
        dateline(self.dateline.clone())
    }
}

fn dateline(dt:time::SystemTime) -> String {
    let ts = dt.duration_since(time::UNIX_EPOCH).unwrap_or(time::Duration::from_secs(0)).as_secs() as i64;
    Local.timestamp(ts, 0).format("%Y/%m/%d %H:%M:%S").to_string()
}

这里新增了 dateline()函数,用于将时间以字符串的形式显示。为了代码重用,TopicList::dateline()也重构为调用该函数

// src/model.rs
impl TopicList {
    pub fn dateline(&self) ->String {
        dateline(self.dateline.clone())
    }
}

模板

文章详情的模板位于templates/frontend/topic_detail.html,请自行查看。

视图类

文章详情的视图类位于src/view/frontend/topic.rs,请自行查看。

handler

// src/handler/frontend/topic.rs

pub async fn detail(
    Extension(state): Extension<Arc<AppState>>,
    Path(id): Path<i64>,
) -> Result<HtmlView> {
    let handler_name = "frontend/topic/list";
    let client = get_client(&state).await.map_err(log_error(handler_name))?;
    let cats = category::list(&client)
        .await
        .map_err(log_error(handler_name))?;
    let archives = topic::archive_list(&client)
        .await
        .map_err(log_error(handler_name))?;
    let state = state.clone();
    let item = topic::detail(&client, id).await.map_err(log_error(handler_name))?;
    let tmpl = Detail {
        cats,
        archives,
        item,
    };
    render(tmpl).map_err(log_error(handler_name))
}

topic::detail()用于通过id获取文章详情。

数据库操作

pub async fn detail(client: &Client, id: i64) -> Result<TopicDetail> {
    super::execute(client, "UPDATE topics SET hit=hit+1 WHERE id=$1", &[&id]).await ?;
    let sql = "SELECT id,title,category_id,html,hit,dateline,is_del,category_name FROM v_topic_cat_detail WHERE is_del=false and id=$1 LIMIT 1";
    super::query_row(client, sql, &[&id]).await
}

首先,使文章的浏览次数加1,然后返回文章的数据。

前面章节说过,Postgresql支持RETURNING,为什么此处要写两句SQL?

原因在于,我们需要返回的数据来自于视图,如果:

  • 直接UPDATE topics...RETURNING ...并不能满足所需要的字段,比如category_name
  • 视图原则上是只读的,所以不能UPDATE v_topic_cat_detail...RETURING... 

另外,此处不需要用事务,因为如果UPDATE出错,那么整个函数都退出了(返回值是Err(AppError))——除非你把SELECT写在UPDATE前面。

路由

本功能的路由定义在src/handler/frontend/mod.rs文件。