rust:axum+sqlx实战学习笔记6

1,001 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

经过前面的学习,我们搭建了一套非常简陋的web服务,具备一定的jwt鉴权和用户注册、登录功能。 接下来,我将一步步进行拓展,最终搭建起一套博客系统。

那么,下面继续开始我们的学习之旅。

首先,调整登录接口

之前写的用户登录接口只是一个简单的测试jwt鉴权用途,现在我们把之前的用户登录接口进行修改,把它调整为一个正常的用户登录接口。

废话不多说,直接cv代码:

//登录响应体
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoginResponse {
    pub id: Option<i32>,
    pub role: Option<Role>,
    pub username: String,
    pub avatar: Option<String>,
    pub access_token: String,
    pub token_type: String,
}

//登录
pub async fn login(Json(body): Json<LoginPayload>) -> impl IntoResponse {
    let result = servers::show(body.clone()).await;
    match result {
        Ok(res) => {
            if body.username != res.username {
                return Json(RespVO::<LoginResponse>::from_error("用户错误!"));
            } else if body.password != res.password {
                return Json(RespVO::<LoginResponse>::from_error("密码错误!"));
            }
            let token = init_token(res.clone());
            let arg = AuthToken::new(token);
            let params = LoginResponse {
                id: res.id,
                role: res.role,
                username: res.username,
                avatar: res.avatar,
                access_token: arg.access_token,
                token_type: arg.token_type,
            };
            Json(RespVO::<LoginResponse>::from_result(&params))
        }
        Err(err) => {
            tracing::error!("login: {:?}", err);
            let info = err.to_string();
            let code = StatusCode::FORBIDDEN;
            Json(RespVO::<LoginResponse>::from_error_info(code, &info))
        }
    }
}

//创建token
fn init_token(payload: AuthPayload) -> String {
    let claims = AuthPayload {
        id: None,
        username: payload.username.to_owned(),
        password: payload.password.to_owned(),
        phone: payload.phone.to_owned(),
        sex: payload.sex.to_owned(),
        email: payload.email.to_owned(),
        avatar: payload.avatar.to_owned(),
        role: payload.role.clone(),
        status: payload.status.clone(),
        create_time: None,
        // token到期时间
        // exp: Some(2000000000), // May 2033
    };
    //创建token, Create the authorization token
    let token = encode(&Header::default(), &claims, &KEYS.encoding)
        .map_err(|_| Json(RespVO::<AuthToken>::from_error("token创建失败!")))
        .unwrap();
    token
}

现在新增了登录响应体,对登录的响应信息进行处理。同时,把生成token的方法提取出来,将以前的jwt鉴权去掉。 现在,我有了一个正常的登录接口,可以返回token和一些用户信息。

新增文章评论与回复接口

看到这里,可能部分同学会觉得很奇怪。为什么博客系统,一开始不是新增文章的接口,而是评论与回复呢? 原因有很多,比如文章增删改查的接口很复杂,联动的数据表比较多,巴拉巴拉。。。 其实,最主要的原因的:文章部分的接口太多,我还没写好,没法cv代码。。。

那么,先从简单的评论与回复开始。这里我把评论和回复分成2个表,其实字段都是差不多的,只是回复比评论多一个parent_id字段,用来表示它的父级。

这里的文件目录结构也是和以前的一样:

1661758161844.png

当然,首先要进行路由注册:

//不需要权限认证的路由
fn init_router() -> Router {
    let app = Router::new()
        .merge(auth::register()) //注册
        .merge(auth::login()) //登录
        .nest("/article", article::handler_articles()) //处理文章
        .nest("/tag", tag::handler_tags()) //处理标签
        .nest("/category", category::handler_category()) //处理分类
        .nest("/comment", comment::handler_comment()) //处理评论
        .nest("/reply", reply::handler_rely()); //处理回复
    return app;
}

先把我们要用的路由统一注册好。这里,眼力过人的同学肯定发现了,路由注册的方法似乎和以前不一样了。 聪明的同学大概能看出来nest方法其实第一个参数就是一级路由。那么二级路由藏在哪里了呢,那自然是跟后面的参数里。而这个nest函数本身就是返回一个路由。

一、评论接口

在api.rs里面,写上:

use super::handlers::{add_comment, get_comment_by_page};
use axum::{
    routing::{delete, get, post, put},
    Router,
};

//创建一个 TodoList
pub fn handler_comment() -> Router {
    //构建注册路由
    Router::new()
        .route("/page", get(get_comment_by_page))
        .route("/add", post(add_comment))
}

其实真正的二级路由,现在放在api.rs文件。

接下来是handler函数:

use super::dto::{CommentList, CommentsList};
use super::servers;
use crate::common::{
    response::RespVO,
    types::{PageInfo, PageResult},
};
use axum::{
    extract::{Path, Query},
    http::StatusCode,
    response::IntoResponse,
    Json,
};

//查询评论列表
pub async fn get_comment_by_page(Query(req): Query<PageInfo>) -> impl IntoResponse {
    let result = servers::list(req.clone()).await;
    let total = servers::total(req.id.unwrap()).await;
    match result {
        Ok(res) => {
            let mut list: Vec<CommentsList> = vec![];
            for v in res.into_iter() {
                let replies = servers::reply_list(&v.id.unwrap()).await;
                if let Ok(comments) = replies {
                    let item = CommentsList {
                        replies: comments,
                        id: v.id,
                        content: v.content,
                        article_id: v.article_id,
                        nick_name: v.nick_name,
                        create_time: v.create_time,
                    };
                    list.push(item);
                }
            }
            let response = PageResult {
                list,
                total: total.unwrap(),
                pageSize: req.pageSize.unwrap_or(10),
                pageNum: req.pageNum.unwrap_or(1),
            };
            Json(RespVO::<PageResult<CommentsList>>::from_result(&response))
        }
        Err(err) => {
            tracing::error!("get_articles_lists: {:?}", err);
            let info = err.to_string();
            let code = StatusCode::NOT_FOUND;
            Json(RespVO::<PageResult<CommentsList>>::from_error_info(
                code, &info,
            ))
        }
    }
}

//新增文章表
pub async fn add_comment(Json(req): Json<CommentList>) -> impl IntoResponse {
    let result = servers::create(req).await;
    match result {
        Ok(_res) => {
            let msg = "新增成功!".to_string();
            Json(RespVO::<String>::from_result(&msg))
        }
        Err(err) => {
            tracing::error!("add_article: {:?}", err);
            let info = err.to_string();
            let code = StatusCode::BAD_REQUEST;
            Json(RespVO::<String>::from_error_info(code, &info))
        }
    }
}

然后是servers.rs文件,放置数据库操作方法:

use super::dto::CommentList;
use crate::common::types::{PageInfo, TotalResponse};
use crate::db;
use crate::routers::reply::RelyList;
use sqlx::{self, Error};

/**
 * 测试接口: 查评论列表
 */
pub async fn list(req: PageInfo) -> Result<Vec<CommentList>, Error> {
    let page_num = req.pageNum.unwrap_or(1);
    let page_size = req.pageSize.unwrap_or(10);
    let limit = (page_num - 1) * page_size;
    let pool = db::get_pool().unwrap();
    let sql = "SELECT id, article_id, content, create_time, nick_name FROM comments
        WHERE article_id = ? AND deleted = 0
        ORDER BY create_time DESC
        LIMIT ?, ?;";
    let list = sqlx::query_as::<_, CommentList>(sql)
        .bind(req.id.unwrap())
        .bind(limit)
        .bind(page_size)
        .fetch_all(pool)
        .await?;
    Ok(list)
}

/**
 * 测试接口: 查回复列表
 */
pub async fn reply_list(id: &u32) -> Result<Vec<RelyList>, Error> {
    let pool = db::get_pool().unwrap();
    let sql = "select
	a.id,
	a.comment_id,
	a.content,
	a.create_time,
	a.article_id,
    a.nick_name,
	a.parent_id,
	b.nick_name as reply_name
from
	reply a
join reply b on
	(a.parent_id = b.id)
where
	(a.comment_id = ?)
union
    select
	c.id,
	c.comment_id,
	c.content,
	c.create_time,
    c.article_id,
	c.nick_name,
	c.parent_id,
	null as reply_name
from
	reply c
left join comments d on
	c.comment_id = d.id
where
	(c.parent_id is null and c.comment_id = ?)
order by create_time asc;";
    let list = sqlx::query_as::<_, RelyList>(sql)
        .bind(id)
        .bind(id)
        .fetch_all(pool)
        .await?;
    Ok(list)
}

/**
 * 测试接口: 新增评论
 */
pub async fn create(detail: CommentList) -> Result<u64, Error> {
    let sql = "INSERT IGNORE INTO comments (content, article_id, nick_name) VALUES (?, ?, ?);";
    let pool = db::get_pool().unwrap();
    let affect_rows = sqlx::query(sql)
        .bind(&detail.content)
        .bind(&detail.article_id)
        .bind(&detail.nick_name)
        .execute(pool)
        .await?
        .rows_affected();
    Ok(affect_rows)
}

/**
 * 测试接口: 查评论总数
 */
pub async fn total(id: u32) -> Result<i64, Error> {
    let pool = db::get_pool().unwrap();
    let sql = "SELECT COUNT(*) AS total FROM comments WHERE article_id = ?;";
    let total_response = sqlx::query_as::<_, TotalResponse>(sql)
        .bind(id)
        .fetch_one(pool)
        .await?;
    Ok(total_response.total)
}

最后是这个接口所用到的数据结构写在dto.rs文件:

use crate::routers::reply::RelyList;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
/// 评论列表模型
#[derive(Debug, Clone, Deserialize, Serialize, FromRow)]
pub struct CommentList {
    pub id: Option<u32>,
    pub content: String,
    pub article_id: u32,
    pub nick_name: String,
    pub create_time: Option<DateTime<Utc>>,
}

/// 评论和回复列表模型
#[derive(Debug, Clone, Deserialize, Serialize, FromRow)]
pub struct CommentsList {
    pub id: Option<u32>,
    pub content: String,
    pub article_id: u32,
    pub nick_name: String,
    pub replies: Vec<RelyList>,
    pub create_time: Option<DateTime<Utc>>,
}

当然,最后不要忘记在mod.rs中对各个文件进行引入和导出:

mod api;
mod dto;
mod handlers;
mod servers;

pub use api::handler_comment;

这里,其实可以看出,单个评论与评论+回复的数据结构是有所区别的,所以我们还需要回复的数据结构,把查询全部评论的接口补充完整。

二、回复接口

聪明的同学估计已经知道回复接口这部分代码要怎么写了,不就是cv大法嘛(bushi)

统一在api.rs配置好路由:

use super::handlers::add_rely;
use axum::{
    routing::{delete, get, post, put},
    Router,
};

//创建一个 TodoList
pub fn handler_rely() -> Router {
    //构建注册路由
    Router::new().route("/add", post(add_rely))
}

然后handlers.rs文件写上handler函数:

use super::dto::RelyList;
use super::servers;
use crate::common::{
    response::RespVO,
    types::{PageInfo, PageResult},
};
use axum::{
    extract::{Path, Query},
    http::StatusCode,
    response::IntoResponse,
    Json,
};

//新增回复表
pub async fn add_rely(Json(req): Json<RelyList>) -> impl IntoResponse {
    let result = servers::create(req).await;
    match result {
        Ok(_res) => {
            let msg = "新增成功!".to_string();
            Json(RespVO::<String>::from_result(&msg))
        }
        Err(err) => {
            tracing::error!("add_article: {:?}", err);
            let info = err.to_string();
            let code = StatusCode::BAD_REQUEST;
            Json(RespVO::<String>::from_error_info(code, &info))
        }
    }
}

当然少不了servers.rs文件处理数据库:

use super::dto::RelyList;
use crate::db;
use sqlx::{self, Error};

/**
 * 测试接口: 增
 */
pub async fn create(detail: RelyList) -> Result<u64, Error> {
    let sql =
        "INSERT IGNORE INTO reply (content, article_id, comment_id, parent_id, nick_name) VALUES (?, ?, ?, ?, ?);";
    let pool = db::get_pool().unwrap();
    let affect_rows = sqlx::query(sql)
        .bind(&detail.content)
        .bind(&detail.article_id)
        .bind(&detail.comment_id)
        .bind(&detail.parent_id)
        .bind(&detail.nick_name)
        .execute(pool)
        .await?
        .rows_affected();
    Ok(affect_rows)
}

以及dto.rs文件存放数据结构:

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
/// 文章列表模型
#[derive(Debug, Clone, Deserialize, Serialize, FromRow)]
pub struct RelyList {
    pub id: Option<u32>,
    pub content: String,
    pub article_id: u32,
    pub nick_name: String,
    pub comment_id: u32,
    pub parent_id: Option<u32>,
    pub create_time: Option<DateTime<Utc>>,
}

最后在mod.rs导出模块:

mod api;
mod dto;
mod handlers;
mod servers;

pub use api::handler_rely;
pub use dto::RelyList;

同样,会有聪明人发现导出的内容似乎多了一个,因为在前面评论的接口中带有回复,所以需要用到回复的数据结构RelyList,这自然是要进行导出的。其实前面查询回复的数据库操作方法,理论上是要写在 回复的servers文件里的,不过我当时写接口时候没留意到,就先这样了。

经过一通cv,现在我们有了一个相对完整的评论与回复接口。要说技术难度吧,其实都是cv,真正的难度在前面的铺垫里学习过了。当然,部分同学会认为这个根本就不是完整的crud,没有删除和修改!

因为我太懒,所以就没写单独的修改和删除。。。

有兴趣的同学,可以补上这部分(课后作业?)