携手创作,共同成长!这是我参与「掘金日新计划 · 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(¶ms))
}
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字段,用来表示它的父级。
这里的文件目录结构也是和以前的一样:
当然,首先要进行路由注册:
//不需要权限认证的路由
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,没有删除和修改!
因为我太懒,所以就没写单独的修改和删除。。。
有兴趣的同学,可以补上这部分(课后作业?)