首页 > 其他分享 >使用axum构建博客系统 - 分类管理

使用axum构建博客系统 - 分类管理

时间:2023-10-21 20:36:47浏览次数:46  
标签:category axum 分类管理 pub client let 博客 id name

本章开始,我们将对博客的具体业务进行实现。首先,我们实现博客分类的管理功能。

数据库结构

CREATE TABLE categories  (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  is_del BOOLEAN NOT NULL DEFAULT FALSE
);
字段说明
id 主键。唯一标识,自增长。
name 分类名称
is_del 是否删除

数据模型

// src/model.rs

/// 分类
#[derive(PostgresMapper, Serialize)]
#[pg_mapper(table="categories")]
pub struct Category {
    pub id:i32,
    pub name:String,
    pub is_del:bool,
}

/// 分类ID
#[derive(PostgresMapper, Serialize)]
#[pg_mapper(table="categories")]
pub struct CategoryID {
    pub id:i32,
}

Category

该结构体的字段与数据表的字段一一对应。

CategoryID

为满足数据库通用操作的泛型约束,我们需要对新插入的ID单独定义一个结构体,以便在插入数据之后,能获取到返回的ID。

数据库操作

以下代码均位于 src/db/category.rs

创建新分类

pub async fn create(client: &Client, frm: &form::CreateCategory) -> Result<CategoryID> {
    // 名称是否存在
    let n = super::count(
        client,
        "SELECT COUNT(*) FROM categories WHERE name=$1",
        &[&frm.name],
    )
    .await?;
    if n > 0 {
        return Err(AppError::duplicate("同名的分类已存在"));
    }

    super::insert(
        client,
        "INSERT INTO categories (name, is_del) VALUES ($1, false) RETURNING id",
        &[&frm.name],
        "创建分类失败",
    )
    .await
}

form::CreateCategory是一个表单类,用于收集用户通过表单提交的数据。该类的定义请参见下一节的“表单类”部分。

  • 首先,我们判断表单提交的分类是否存在:使用提交的分类名称进行count(),如果结果大于0,说明该分类已存在
  • 如果提交的分类不存在,则插入到数据库中

获取所有分类

pub async fn list(client: &Client) -> Result<Vec<Category>> {
    super::query(
        client,
        "SELECT id,name,is_del FROM categories WHERE is_del=false ORDER BY id ASC LIMIT 1000",
        &[],
    )
    .await
}

我们从数据库中获取所有未删除的分类数据。按惯例,分类是不需要进行分页的——我们近乎肯定的假设,大部分情况下,博客的分类都不会很多。

删除或恢复分类

pub async fn del_or_restore(client: &Client, id: i32, is_del: bool) -> Result<bool> {
    let n = super::del_or_restore(client, "categories", &id, is_del).await?;
    Ok(n > 0)
}

这里调用了父模块的同名方法:

// src/db/mod.rs

async fn del_or_restore(
    client: &impl GenericClient,
    table:&str,
    id: &(dyn ToSql + Sync),
    is_del: bool,
) -> Result<u64> {
    let sql = format!("UPDATE {} SET is_del=$1 WHERE id=$2", table);
    execute(client, &sql, &[ &is_del, id]).await
}

我们是通过is_del来标识是否删除的,所以只要修改该字段对应的值就可以实现删除或恢复。

修改分类

pub async fn edit(client: &Client, frm: &form::EditCategory) -> Result<bool> {
    // 名称是否存在
    let n = super::count(
        client,
        "SELECT COUNT(*) FROM categories WHERE name=$1 AND id<>$2",
        &[&frm.name, &frm.id],
    )
    .await?;
    if n > 0 {
        return Err(AppError::duplicate("同名的分类已存在"));
    }

    let n = super::execute(
        client,
        "UPDATE categories SET name=$1 WHERE id=$2",
        &[&frm.name, &frm.id],
    )
    .await?;
    Ok(n > 0)
}

form::EditCategory也是一个表单类,其定义请参见下方的“表单类”部分。

注意,修改时判断是否存在的条件:除了限定名称之外,还要限定ID不等于当前要修改的分类的ID。

根据ID查找分类

pub async fn find(client: &Client, id:i32) ->Result<Category> {
    super::query_row(client, "SELECT id, name, is_del FROM categories WHERE id=$1 LIMIT 1", &[&id]).await
}

表单类

// src/form.rs

/// 创建分类的表单
#[derive(Deserialize)]
pub struct CreateCategory {
    pub name:String,
}

/// 修改分类的表单
#[derive(Deserialize)]
pub struct EditCategory {
    pub id:i32,
    pub name:String,
}

AppError

// src/error.rs
pub enum AppErrorType {
    // ...
    Duplicate,
}

impl AppError {
    // ...
    pub fn duplicate(msg: &str) -> Self {
        Self::from_str(msg, AppErrorType::Duplicate)
    }
}

新加了  AppErrorType::Duplicate枚举,以及对应的 duplicate(),用于标识记录是否已经存在。

配置

由于要对数据库进行操作,所以我们需要配置信息。

// src/config.rs

#[derive(Deserialize)]
pub struct WebConfig {
    pub addr:String,
}
#[derive(Deserialize)]
pub struct Config {
    pub web:WebConfig,
    pub pg: deadpool_postgres::Config,
}

impl Config {
    pub fn from_env()->Result<Self, config::ConfigError> {
        let mut cfg = config::Config::new();
        cfg.merge(config::Environment::new())?;
        cfg.try_into()
    }
}

配置文件示例(.env):

WEB.ADDR=127.0.0.1:9527
PG.HOST=pg.axum.rs
PG.PORT=5432
PG.USER=blog
PG.PASSWORD=axum.rs
PG.DBNAME=blog
PG.POOL.MAX_SIZE=30

状态共享

为了在handler间共享数据库连接池,我们需要定义状态共享:

// src/lib.rs

/// 共享状态
pub struct AppState {
    /// 数据库连接
    pub pool: deadpool_postgres::Pool,
}

同时,需要将状态共享加入到main()中:

// src/main.rs

#[tokio::main]
async fn main() {
   // ...
    dotenv().ok();
    let cfg = config::Config::from_env().expect("初始化配置失败");
    let pool = cfg.pg.create_pool(None, tokio_postgres::NoTls).expect("创建数据库连接池失败");


    let frentend_routers = frontend::router();
    let backend_routers = backend::router();
    let app = Router::new()
        .nest("/", frentend_routers)
        .nest("/admin", backend_routers)
        .layer(Extension(Arc::new(AppState { pool})));

   //...
}

模板

以下模板位于 templates/backend 目录

母模板 base.html

<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
                <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
                    <h1 class="h2">{% block title%}标题{%endblock%}</h1>
                   {% block toolbar %}{%endblock%} 
                </div>
                <div>
                    {% block msg%} {%endblock%}
                    {%block content%}内容{%endblock%}
                </div>
            </main>

母模板增加了各个块的定义。

提示信息模板 msg.html

为了显示提示信息,增了该模板

{% match msg %}
{% when Some with (msg) %}
  <div class="alert alert-info" role="alert">
    {{msg}}
  </div>
{% when None %}
{% endmatch %}

以下模板均位于 templates/backend/category 目录

添加分类模板 add.html

{% extends "./../base.html" %}
{% block title%}添加分类{%endblock%}
{% block toolbar %} {% include "./toolbar.html" %} {%endblock%}
{% block content %}
<form method="post" action="/admin/category/add">
    <div class="mb-3">
        <label for="name" class="form-label">名称</label>
        <input type="text" name="name" id="name" class="form-control" placeholder="分类名称" required>
    </div>
    <button type="submit" class="btn btn-primary">提交</button>
</form>
{%endblock%}

修改分类模板 edit.html

{% extends "./../base.html" %}
{% block title%}修改分类{%endblock%}
{% block toolbar %} {% include "./toolbar.html" %} {%endblock%}
{% block content %}
<form method="post" action="/admin/category/edit/{{item.id}}">
    <input type="hidden" value="{{item.id}}" name="id">
    <div class="mb-3">
        <label for="name" class="form-label">名称</label>
        <input type="text" name="name" id="name" class="form-control" placeholder="分类名称" value="{{ item.name }}" required>
    </div>
    <button type="submit" class="btn btn-primary">提交</button>
</form>
{%endblock%}

分类列表模板 index.html

{% extends "./../base.html" %}
{% block title%}所有分类{%endblock%}
{% block toolbar %} {% include "./toolbar.html" %} {%endblock%}
{% block msg %} {%include "../msg.index"%} {%endblock%}
{% block content %}
<table class="table table-striped table-hover">
    <thead>
        <tr>
            <th>ID</th>
            <th>名称</th>
            <th>操作</th>
        </tr>
    </thead>
    <tbody>
        {% for item in list%}
        <tr>
            <td>{{ item.id }}</td>
            <td>{{ item.name }}</td>
            <td>
                <a href="/admin/category/edit/{{ item.id }}" class="btn btn-primary btn-sm">修改</a>
                <a href="/admin/category/del/{{ item.id }}" class="btn btn-danger btn-sm" onclick="return confirm('确定删除「{{ item.name }}」');">删除</a>
            </td>
        </tr>
        {%endfor%}
    </tbody>
</table>
{%endblock%}

工具栏模板 toolbar.html


<div class="btn-toolbar mb-2 mb-md-0">
    <div class="btn-group me-2">
        <a href="/admin/category" type="button" class="btn btn-sm btn-outline-secondary">列表</a>
        <a href="/admin/category/add" type="button" class="btn btn-sm btn-outline-secondary">添加</a>
    </div>
</div>

视图类

// src/view/backend/category.rs

#[derive(Template)]
#[template(path="backend/category/add.html")]
pub struct Add {}

#[derive(Template)]
#[template(path="backend/category/index.html")]
pub struct Index{
    pub list: Vec<Category>,
    pub msg: Option<String>,
}
#[derive(Template)]
#[template(path="backend/category/edit.html")]
pub struct Edit{
    pub item: Category,
}

添加分类视图Add

没有额外的字段,单纯的关联模板文件。

分类列表视图Index

  • list:所有未删除的分类
  • msg:提示信息

修改分类视图Edit

  • item:读取需要修改的分类的当前信息,以便填充到表单中

handler

以下代码位于 src/handler/backend/category.rs 文件

添加分类

/// 添加分类UI
pub async fn add_ui()->Result<HtmlView> {
    let handler_name = "backend/category/add_ui";
    let tmpl = Add{};
    render(tmpl).map_err(log_error(handler_name))
}

/// 添加分类
pub async fn add(
    Extension(state):Extension<Arc<AppState>>,
    Form(frm):Form<form::CreateCategory>,
) -> Result<RedirectView> {
    let handler_name = "backend/category/add";
    let client = get_client(&state).await.map_err(log_error(handler_name))?;
    category::create(&client, &frm).await.map_err(log_error(handler_name))?;
    redirect("/admin/category?msg=分类添加成功")
}

add_ui:显示添加分类的模板渲染出来的页面。

add:处理添加分类的逻辑。添加成功后,跳转到分类列表页。这里出现的 RedirectView 、get_client() 和 redirect() 请参见本章后面部分的说明。

分类列表

pub async fn index(
    Extension(state):Extension<Arc<AppState>>,
    Query(args):Query<Args>,
) ->Result<HtmlView> {
    let handler_name = "backend/category/index";
    let client = get_client(&state).await.map_err(log_error(handler_name))?;
    let list = category::list(&client).await.map_err(log_error(handler_name))?;
    let tmpl = Index { list, msg:args.msg };
    render(tmpl).map_err(log_error(handler_name))
}

从数据库中读取所有分类,并将通过模板将其渲染。其中的Args表示URL查询参数,它的定义在本章的下文。

删除分类

pub async fn del(
    Extension(state):Extension<Arc<AppState>>,
    Path(id):Path<i32>,
) -> Result<RedirectView> {
    let handler_name = "backend/category/del";
    let client = get_client(&state).await.map_err(log_error(handler_name))?;
    category::del_or_restore(&client, id, true).await.map_err(log_error(handler_name))?;
    redirect("/admin/category?msg=分类删除成功")
}

修改分类

pub async fn edit_ui(
    Extension(state):Extension<Arc<AppState>>,
    Path(id):Path<i32>,
) -> Result<HtmlView> {
    let handler_name = "backend/category/edit_ui";
    let client = get_client(&state).await.map_err(log_error(handler_name))?;
    let item = category::find(&client, id).await.map_err(log_error(handler_name))?;
    let tmpl = Edit { item };
    render(tmpl).map_err(log_error(handler_name))
}

pub async fn edit(
    Extension(state):Extension<Arc<AppState>>,
    Form(frm):Form<EditCategory>,
)->Result<RedirectView> {
    let handler_name = "backend/category/edit";
    let client = get_client(&state).await.map_err(log_error(handler_name))?;
    category::edit(&client, &frm).await.map_err(log_error(handler_name))?;
    redirect("/admin/category?msg=分类修改成功")
}

edit_ui:从数据库读取指定分类的数据,并渲染到页面上。

edit:修改分类的处理逻辑。

RedirectView

用于跳转的视图:

// src/handler/mod.rs
type RedirectView = (StatusCode, HeaderMap, ());

redirect()

跳转到指定的URL:

// src/handler/mod.rs

fn redirect(url:&str) -> Result<RedirectView> {
    let mut hm = HeaderMap::new();
    hm.append(header::LOCATION,url.parse().unwrap()) ;
    Ok((StatusCode::FOUND, hm, ()))
}

get_client()

从连接池中获取数据库连接:

async fn get_client(state: &AppState) -> Result<Client> {
   state.pool.get().await.map_err(AppError::from)
}

Args

// src/handler/backend/mod.rs
#[derive(Deserialize)]
pub struct Args {
    pub msg: Option<String>,
    pub page: Option<u32>,
}
impl Args {
    pub fn msg(&self) -> String {
        self.msg.clone().unwrap_or("".to_string())
    }
    pub fn page(&self) -> u32 {
        self.page.unwrap_or(0)
    }
}
  • msg字段:可选的提示消息
  • page字段:可选的分页页码
  • msg()方法:如果msg字段为None,返回空字符串""
  • page()方法:如果page字段为None,返回0

路由

把handler加到路由中:

pub fn router() -> Router {
    let category_router = Router::new()
        .route("/", get(category::index))
        .route("/add", get(category::add_ui).post(category::add))
        .route("/del/:id", get(category::del))
        .route("/edit/:id", get(category::edit_ui).post(category::edit))
        ;
    Router::new()
        .route("/", get(index))
        .nest("/category", category_router)
}

标签:category,axum,分类管理,pub,client,let,博客,id,name
From: https://www.cnblogs.com/pythonClub/p/17779453.html

相关文章

  • 使用axum构建博客系统 - 文章管理
    本章我们将实现博客的文章管理功能。数据库结构CREATETABLEtopics(idBIGSERIALPRIMARYKEY,titleVARCHAR(255)NOTNULL,category_idINTNOTNULL,summaryVARCHAR(255)NOTNULL,markdownVARCHARNOTNULL,htmlVARCHARNOTNULL,hitINTNOTNUL......
  • 使用axum构建博客系统 - 后台管理菜单及首页模板
    目前,后台管理功能基本完成,但还有两个工作没做:清理后台管理的导航菜单以及后台管理首页的模板。后台管理菜单<!--templates/backend/base.html--><!--...--><divclass="container-fluid"><divclass="row"><navid="sidebarMenu"c......
  • 使用axum构建博客系统 - 鉴权与登录
    本章实现后台管理的鉴权,以及管理员的登录、注销功能。涉及的知识点有:cookie及中间件等。数据库结构CREATETABLEadmins(idSERIALPRIMARYKEY,emailVARCHAR(255)NOTNULL,passwordVARCHAR(255)NOTNULL,is_delBOOLEANNOTNULLDEFAULTFALSE);字段说......
  • 使用axum构建博客系统 - 网站首页
    后台管理完成后,我们开始进入前台功能的开发。本章我们将完成博客首页的开发。母模板templates/frontend/base.html是时候对前台母模板进行数据填充和块的定义了:<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="utf-8"/><metaname="viewport"c......
  • 使用axum构建博客系统 - 分类文章列表
    本章将实现博客的分类文章列表功能。模板请参见代码仓库的templates/frontend/topic_list.html视图类请参见代码仓库的src/view/frontend/topic.rshandler//src/handler/frontend/topic.rspubasyncfnlist(Extension(state):Extension<Arc<AppState>>,Path(......
  • 使用axum构建博客系统 - 文章详情
    本章将实现博客文章的详情显示功能。数据库视图CREATEVIEWv_topic_cat_detailASSELECTt.id,title,html,hit,dateline,category_id,t.is_del,c.nameAScategory_nameFROMtopicsAStINNERJOINcategoriesAScONt.cate......
  • 使用axum构建博客系统 - 存档文章列表
    本章将实现存档文章列表功能。注意,本章涉及较多PostgreSQL知识,如果你对相关知识不熟悉,可以先让代码跑起来,再去了解相关知识。模板本功能模板文件是templates/frontend/topic_arch.html。视图类本功能视图类定义在src/view/frontend/topic.rs文件。handler//src/handler/fro......
  • 每日博客
    1.Hive是由Facebook公司开发的一个构建在Hadoop之上的数据仓库工具,在某种程度上可以看作是用户编程接口,其本身并不存储和处理数据2.Hive一般依赖于分布式文件系统HDFS,而传统数据库则依赖于本地文件系统,Hive和传统关系数据库都支持分区,传统关系数据库很难实现横向扩展,Hive具......
  • 博客园主题美化
    一、首页主题预览二、主题部署1.开通js权限2.css代码禁用css模板点击查看代码#loading{bottom:0;left:0;position:fixed;right:0;top:0;z-index:9999;background-color:#f4f5f5;pointer-events:none;}.loader-inner{will-change:transform;width:40px;height:40px;positi......
  • 模拟集成电路设计系列博客——3.1.3 稳压电路
    3.1.3稳压电路稳压器的目标是产生一个低噪声并能提供电流的电压源。他们一般来说用于这种情节:当一个关键模拟电路必须和其他的电路工作在同一个电源供电下时。如下图所示,其他的电路向共用的电源中引入了很大的噪声,使用稳压器可以为关键电路提供一个更加干净的电源。数字电路一般......