From 3f1941667c2f8d29033384bfdf77a214047efab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E5=A4=A9?= Date: Wed, 7 Feb 2024 09:47:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E6=9E=B6=E6=9E=84=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- Cargo.lock | 81 ++++++++++++- Cargo.toml | 2 + config/default.toml | 5 +- src/{ => common}/err.rs | 10 ++ src/common/mod.rs | 6 + src/common/response.rs | 15 +++ src/common/template.rs | 15 +++ src/config.rs | 46 -------- src/config/app.rs | 10 ++ src/config/database.rs | 7 ++ src/config/mod.rs | 3 + src/config/server.rs | 8 ++ src/handler/article.rs | 134 ++++----------------- src/handler/category.rs | 231 +++++++------------------------------ src/handler/index.rs | 6 +- src/handler/mod.rs | 40 +------ src/initialize/config.rs | 30 +++++ src/initialize/database.rs | 23 ++++ src/initialize/logger.rs | 18 +++ src/initialize/mod.rs | 3 + src/lib.rs | 39 ++++++- src/main.rs | 48 ++------ src/router.rs | 30 ----- src/router/mod.rs | 13 +++ src/service/article.rs | 115 ++++++++++++++++++ src/service/category.rs | 191 ++++++++++++++++++++++++++++++ src/service/mod.rs | 2 + 28 files changed, 666 insertions(+), 467 deletions(-) rename src/{ => common}/err.rs (87%) create mode 100644 src/common/mod.rs create mode 100644 src/common/response.rs create mode 100644 src/common/template.rs delete mode 100644 src/config.rs create mode 100644 src/config/app.rs create mode 100644 src/config/database.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/server.rs create mode 100644 src/initialize/config.rs create mode 100644 src/initialize/database.rs create mode 100644 src/initialize/logger.rs create mode 100644 src/initialize/mod.rs delete mode 100644 src/router.rs create mode 100644 src/router/mod.rs create mode 100644 src/service/article.rs create mode 100644 src/service/category.rs create mode 100644 src/service/mod.rs diff --git a/.env b/.env index 7db7232..12399a3 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # 日志级别 RUST_LOG=INFO -# 运行模式 +# 运行模式 要与配置文件相符 默认为dev RUN_MODE=dev diff --git a/Cargo.lock b/Cargo.lock index 37f253a..fbc45a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,9 +252,11 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "state", "tokio", "tracing", "tracing-subscriber", + "validator", ] [[package]] @@ -755,6 +757,19 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -990,6 +1005,16 @@ dependencies = [ "cc", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.5.0" @@ -1116,6 +1141,21 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1864,6 +1904,12 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2363,6 +2409,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2768,7 +2823,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] @@ -2787,6 +2842,21 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +dependencies = [ + "idna 0.4.0", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2899,6 +2969,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index aa719fe..1b0ed08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,5 @@ tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "time", "local-time"] } serde_json = "1.0.113" chrono = "0.4.33" +state = "0.6.0" +validator = "0.16.1" diff --git a/config/default.toml b/config/default.toml index 7f08d84..535bbd6 100644 --- a/config/default.toml +++ b/config/default.toml @@ -1,5 +1,6 @@ -[web] -addr = "0.0.0.0:9527" +[server] +port = 9527 +host = "0.0.0.0" [mysql] max_cons = 5 diff --git a/src/err.rs b/src/common/err.rs similarity index 87% rename from src/err.rs rename to src/common/err.rs index 0ad6023..6068622 100644 --- a/src/err.rs +++ b/src/common/err.rs @@ -70,4 +70,14 @@ impl axum::response::IntoResponse for AppError { }; msg.into_response() } +} + + +/// 记录错误 +pub fn log_error(handler_name: &str) -> Box AppError> { + let handler_name = handler_name.to_string(); + Box::new(move |err| { + tracing::error!("{}: {:?}", handler_name, err); + err + }) } \ No newline at end of file diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..255e740 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,6 @@ +pub mod template; +pub mod err; +pub mod response; + +pub type Result = std::result::Result; + diff --git a/src/common/response.rs b/src/common/response.rs new file mode 100644 index 0000000..9ee8b3f --- /dev/null +++ b/src/common/response.rs @@ -0,0 +1,15 @@ +use axum::http::{header, HeaderMap, StatusCode}; +use axum::response::Html; +use super::Result; + +pub type HtmlResponse = Html; +pub type RedirectResponse = (StatusCode, HeaderMap, ()); + +pub fn redirect(url: &str) -> Result { + let mut header = HeaderMap::new(); + header.insert(header::LOCATION, url.parse().unwrap()); + Ok((StatusCode::FOUND, header, ())) +} + + + diff --git a/src/common/template.rs b/src/common/template.rs new file mode 100644 index 0000000..fe915d9 --- /dev/null +++ b/src/common/template.rs @@ -0,0 +1,15 @@ +use askama::Template; +use axum::response::Html; + +use crate::common::err::{AppError, log_error}; +use crate::common::response::HtmlResponse; +use crate::common::Result; + +/// 渲染模板 +pub fn render(tpl: T, handler_name: &str) -> Result { + let out = tpl + .render() + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + Ok(Html(out)) +} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index dfd3b11..0000000 --- a/src/config.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::env; -use config::File; -use serde::Deserialize; -use crate::err::AppError; -use crate::Result; - -#[derive(Deserialize)] -pub struct MysqlConfig { - pub dsn: String, - pub max_cons: u32, -} - - -#[derive(Deserialize)] -pub struct WebConfig { - pub addr: String, -} - - -#[derive(Deserialize)] -pub struct AppConfig { - pub mysql: MysqlConfig, - pub web: WebConfig, -} - -impl AppConfig { - pub fn new() -> Result { - let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "dev".into()); - - // https://github.com/mehcode/config-rs/blob/master/examples/hierarchical-env/settings.rs - config::Config::builder() - // Start off by merging in the "default" configuration file - .add_source(File::with_name("config/default")) - // Add in the current environment file - // Default to 'dev' env - // Note that this file is _optional_ - .add_source( - File::with_name(&format!("config/{}", run_mode)).required(false), - ) - .build() - .map_err(AppError::from)? - .try_deserialize() - .map_err(AppError::from) - } -} - diff --git a/src/config/app.rs b/src/config/app.rs new file mode 100644 index 0000000..9b4a57d --- /dev/null +++ b/src/config/app.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +use crate::config::database::MysqlConfig; +use crate::config::server::ServerConfig; + +#[derive(Deserialize)] +pub struct AppConfig { + pub mysql: MysqlConfig, + pub server: ServerConfig, +} diff --git a/src/config/database.rs b/src/config/database.rs new file mode 100644 index 0000000..de17596 --- /dev/null +++ b/src/config/database.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct MysqlConfig { + pub dsn: String, + pub max_cons: u32, +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..6c081a7 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,3 @@ +pub mod server; +pub mod database; +pub mod app; diff --git a/src/config/server.rs b/src/config/server.rs new file mode 100644 index 0000000..a189a46 --- /dev/null +++ b/src/config/server.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct ServerConfig { + ///当前服务地址 + pub host: String, + pub port: u16, +} diff --git a/src/handler/article.rs b/src/handler/article.rs index c48c7bb..bcfcfb8 100644 --- a/src/handler/article.rs +++ b/src/handler/article.rs @@ -1,130 +1,40 @@ -use std::sync::Arc; +use axum::{Form, Router}; +use axum::extract::Query; +use axum::routing::get; -use axum::extract::{Query, State}; -use axum::Form; -use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, NotSet, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect}; -use sea_orm::ActiveValue::Set; - -use crate::entity::{article, category, tag}; -use crate::err::AppError; +use crate::common::{response::HtmlResponse, Result}; +use crate::common::response::{redirect, RedirectResponse}; +use crate::common::template::render; use crate::form::ArticleForm; -use crate::handler::{get_conn, HtmlResponse, log_error, redirect, RedirectResponse, render}; use crate::param::ArticleParams; -use crate::Result; -use crate::state::AppState; -use crate::view; +use crate::service::article::ArticleService; -pub async fn index( - State(state): State>, - Query(params): Query, -) -> Result { +pub fn init_router() -> Router { + Router::new() + .route("/", get(index)) + .route("/add", get(add_ui).post(add)) + .route("/tags", get(list_with_tags)) +} + +pub async fn index(Query(params): Query) -> Result { let handler_name = "article/index"; - let conn = get_conn(&state); - - // 构建条件: 所有未被删除的文章 - let condition = Condition::all() - .add(article::Column::IsDel.eq(false)) - .add_option( - params.keyword_opt().map(|n| article::Column::Title.contains(&n)) - ) - .add_option(params.is_del_opt().map(|n| article::Column::IsDel.eq(n))); - - let mut select = article::Entity::find() - .filter(condition); - - let page = params.page(); // 当前页码 - let page_size = params.page_size(); // 每页条数,默认15 - - if let Some(ord) = params.order() { - select = select.order_by(category::Column::Id, ord); - } - - let paginator = select - .find_also_related(category::Entity) - .paginate(conn, page_size); - - let page_total = paginator.num_pages().await.map_err(AppError::from)?; - - let list: Vec<(article::Model, Option)> = paginator - .fetch_page(page) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - - let tpl = view::ArticlesTemplate { - list, - page_total, - params, - }; + let tpl = ArticleService::index(handler_name, params).await?; render(tpl, handler_name) } -pub async fn add_ui(State(state): State>) -> Result { +pub async fn add_ui() -> Result { let handler_name = "article/add_ui"; - let conn = get_conn(&state); - - let categies = category::Entity::find() - .filter(category::Column::IsDel.eq(false)) - .limit(100) - .order_by_asc(category::Column::Id) - .all(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - - let tpl = view::ArticleAddTemplate { categies }; + let tpl = ArticleService::add_ui(handler_name).await?; render(tpl, handler_name) } -pub async fn add( - State(state): State>, - Form(frm): Form, -) -> Result { +pub async fn add(Form(frm): Form) -> Result { let handler_name = "article/add"; - let conn = get_conn(&state); - - article::ActiveModel { - id: NotSet, - title: Set(frm.title), - category_id: Set(frm.category_id), - content: Set(frm.content), - ..Default::default() - }.save(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - + ArticleService::add(handler_name, frm).await?; redirect("/article?msg=文章添加成功") } -pub async fn list_with_tags( State(state): State>) -> Result { +pub async fn list_with_tags() -> Result { let handler_name = "article/list_with_tags"; - let conn = get_conn(&state); - - let list = article::Entity::find() - .find_with_related(tag::Entity) - .all(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - - let mut ss = vec![]; - - for item in list { - let (article, tags) = item; - let tags = tags - .iter() - .map(|tag| format!("【#{} - {}】", &tag.id, &tag.name)) - .collect::>() - .join(",") - .to_string(); - - let s = format!( - "文章ID: {}, 文章标题: {}, 标签: {}", - &article.id, &article.title, tags, - ); - ss.push(s); - } - - Ok(ss.join("\n").to_string()) + ArticleService::list_with_tags(handler_name).await } \ No newline at end of file diff --git a/src/handler/category.rs b/src/handler/category.rs index 764cb75..9f87290 100644 --- a/src/handler/category.rs +++ b/src/handler/category.rs @@ -1,86 +1,41 @@ -use std::sync::Arc; +use axum::{Form, Router}; +use axum::extract::{Path, Query}; +use axum::routing::get; -use axum::extract::{Path, Query, State}; -use axum::Form; -use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder}; -use sea_orm::ActiveValue::Set; - -use crate::{form, Result, view}; -use crate::entity::{article, category}; -use crate::err::AppError; -use crate::handler::{get_conn, HtmlResponse, log_error, redirect, RedirectResponse, render}; +use crate::{form, view}; +use crate::common::{response::HtmlResponse, Result}; +use crate::common::response::{redirect, RedirectResponse}; +use crate::common::template::render; use crate::param::{CategoryParams, DelParams}; -use crate::state::AppState; +use crate::service::category::CategoryService; -pub async fn index( - State(state): State>, - Query(params): Query, -) -> Result { +pub fn init_router() -> Router { + Router::new() + .route("/", get(index)) + .route( + "/add", + get(add_ui).post(add), + ) + .route( + "/edit/:id", + get(edit_ui).post(edit), + ) + .route("/del/:id", get(del)) + .route("/del/:id/:real", get(del)) + .route("/articles/:id", get(articles)) +} + +pub async fn index(Query(params): Query) -> Result { let handler_name = "category/index"; - let conn = get_conn(&state); - - let page = params.page(); // 当前页码 - let page_size = params.page_size(); // 每页条数,默认15 - let mut select = category::Entity::find() - .filter( - Condition::all() - .add_option( - params.keyword_opt().map(|n| category::Column::Name.contains(&n)) - ) - .add_option(params.is_del_opt().map(|n| category::Column::IsDel.eq(n))) - ); - - if let Some(ord) = params.order() { - select = select.order_by(category::Column::Id, ord); - } - - let paginator = select.paginate(conn, page_size); - - let page_total = paginator.num_pages().await.map_err(AppError::from)?; - - if page_total == 0 { - let tpl = view::CategoryTemplate { - categories: vec![], - params, - page_total, - }; - return render(tpl, handler_name); - } - - let categories = paginator - .fetch_page(page) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - - let tpl = view::CategoryTemplate { - categories, - params, - page_total, - }; + let tpl = CategoryService::index(handler_name, params).await?; render(tpl, handler_name) } -pub async fn find( - State(state): State>, - Path(id): Path, -) -> Result { - let id = u32::try_from(id).or(Err(AppError::notfound()))?; - +pub async fn find(Path(id): Path) -> Result { let handler_name = "category/find"; - let conn = get_conn(&state); - - let cate = category::Entity::find_by_id(id) - .one(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - - match cate { - None => Err(AppError::notfound()), - Some(cate) => Ok(format!("id: {}, 名称: {}", cate.id, cate.name)), - } + let category_model = CategoryService::find(handler_name, id).await?; + Ok(format!("id: {}, 名称: {}", category_model.id, category_model.name)) } pub async fn add_ui() -> Result { @@ -89,145 +44,37 @@ pub async fn add_ui() -> Result { render(tpl, handler_name) } -pub async fn add( - State(state): State>, - Form(frm): Form, -) -> Result { +pub async fn add(Form(frm): Form) -> Result { let handler_name = "category/add"; - let conn = get_conn(&state); - - let am = category::ActiveModel { - name: Set(frm.name), - // all other attributes are `NotSet` - ..Default::default() - }; - - let added_category = am.insert(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - - let url = format!("/category?msg=分类添加成功,ID是:{}", added_category.id); - + let added_category_id = CategoryService::add(handler_name, frm).await?; + let url = format!("/category?msg=分类添加成功,ID是:{}", added_category_id); redirect(url.as_str()) } -pub async fn edit_ui( - State(state): State>, - Path(id): Path, -) -> Result { - let id = u32::try_from(id).or(Err(AppError::notfound()))?; - +pub async fn edit_ui(Path(id): Path) -> Result { let handler_name = "category/edit_ui"; - let conn = get_conn(&state); - - let cate = category::Entity::find_by_id(id) - .one(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))? - .ok_or(AppError::notfound())?; - - let tpl = view::CategoryEditTemplate { category: cate }; + let tpl = CategoryService::edit_ui(handler_name, id).await?; render(tpl, handler_name) } pub async fn edit( - State(state): State>, Path(id): Path, Form(frm): Form, ) -> Result { - let id = u32::try_from(id).or(Err(AppError::notfound()))?; - let handler_name = "category/edit"; - let conn = get_conn(&state); - - let cate = category::Entity::find_by_id(id) - .one(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))? - .ok_or(AppError::notfound())?; - - let mut am = cate.into_active_model(); - am.name = Set(frm.name); - - am.update(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - + CategoryService::edit(handler_name, id, frm).await?; redirect("/category?msg=分类修改成功") } -pub async fn del( - State(state): State>, - Path(params): Path, -) -> Result { +pub async fn del(Path(params): Path) -> Result { let handler_name = "category/del"; - let conn = get_conn(&state); - let real = params.real.unwrap_or(false); - - let cate = category::Entity::find_by_id(params.id) - .one(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))? - .ok_or(AppError::notfound())?; - - if real { - cate.delete(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - } else { - let mut am = cate.into_active_model(); - am.is_del = Set(true); - am.save(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - } - + CategoryService::del(handler_name, params).await?; redirect("/category?msg=分类删除成功") } -pub async fn articles( - State(state): State>, - Path(id): Path, - Query(params): Query, -) -> Result { +pub async fn articles(Path(id): Path, Query(params): Query) -> Result { let handler_name = "category/articles"; - let conn = get_conn(&state); - - let cate = category::Entity::find_by_id(id) - .one(conn) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))? - .ok_or(AppError::notfound()) - .map_err(log_error(handler_name))?; - - let paginator = cate.find_related(article::Entity) - .paginate(conn, params.page_size()); - - let articles = paginator.fetch_page(params.page()) - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - - let page_total = paginator - .num_pages() - .await - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - - let tpl = view::CategoryArticlesTemplate { - params, - page_total, - category: cate, - articles, - }; + let tpl = CategoryService::articles(handler_name, id, params).await?; render(tpl, handler_name) } \ No newline at end of file diff --git a/src/handler/index.rs b/src/handler/index.rs index 145de9a..7372a35 100644 --- a/src/handler/index.rs +++ b/src/handler/index.rs @@ -1,5 +1,7 @@ -use crate::handler::{HtmlResponse, render}; -use crate::{Result, view}; +use crate::common::response::HtmlResponse; +use crate::common::Result; +use crate::common::template::render; +use crate::view; pub async fn index() -> Result { let handler_name = "index"; diff --git a/src/handler/mod.rs b/src/handler/mod.rs index b529870..360ba42 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -1,45 +1,7 @@ -use askama::Template; -use axum::http::{header, HeaderMap, StatusCode}; -use axum::response::Html; -use sea_orm::DatabaseConnection; - pub use index::index; -use crate::err::AppError; -use crate::Result; -use crate::state::AppState; - pub mod category; + mod index; pub mod article; -fn get_conn(state: &AppState) -> &DatabaseConnection { - &state.conn -} - -type HtmlResponse = Html; -type RedirectResponse = (StatusCode, HeaderMap, ()); - -/// 渲染模板 -fn render(tpl: T, handler_name: &str) -> Result { - let out = tpl - .render() - .map_err(AppError::from) - .map_err(log_error(handler_name))?; - Ok(Html(out)) -} - -/// 记录错误 -fn log_error(handler_name: &str) -> Box AppError> { - let handler_name = handler_name.to_string(); - Box::new(move |err| { - tracing::error!("{}: {:?}", handler_name, err); - err - }) -} - -fn redirect(url: &str) -> Result { - let mut header = HeaderMap::new(); - header.insert(header::LOCATION, url.parse().unwrap()); - Ok((StatusCode::FOUND, header, ())) -} \ No newline at end of file diff --git a/src/initialize/config.rs b/src/initialize/config.rs new file mode 100644 index 0000000..209eb60 --- /dev/null +++ b/src/initialize/config.rs @@ -0,0 +1,30 @@ +use std::env; + +use config::File; + +use crate::APPLICATION_CONTEXT; +use crate::config::app::AppConfig; + +/// 初始化配置信息 +pub async fn init_config() { + let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "dev".into()); + + // https://github.com/mehcode/config-rs/blob/master/examples/hierarchical-env/settings.rs + let config: AppConfig = config::Config::builder() + // Start off by merging in the "default" configuration file + .add_source(File::with_name("config/default")) + // Add in the current environment file + // Default to 'dev' env + // Note that this file is _optional_ + .add_source( + File::with_name(&format!("config/{}", run_mode)).required(false), + ) + .build() + .map_err(|e| tracing::error!("config build failed: {}", e.to_string())) + .unwrap() + .try_deserialize() + .map_err(|e| tracing::error!("config init failed: {}", e.to_string())) + .unwrap(); + + APPLICATION_CONTEXT.set::(config); +} \ No newline at end of file diff --git a/src/initialize/database.rs b/src/initialize/database.rs new file mode 100644 index 0000000..b43b93d --- /dev/null +++ b/src/initialize/database.rs @@ -0,0 +1,23 @@ +use sea_orm::{ConnectOptions, Database, DatabaseConnection}; +use tracing::{error, info}; + +use crate::APPLICATION_CONTEXT; +use crate::config::app::AppConfig; + +pub async fn init_database() { + let cfg = APPLICATION_CONTEXT.get::(); + + let mut opt = ConnectOptions::new(&cfg.mysql.dsn); + opt.max_connections(cfg.mysql.max_cons); + + let sea_orm_conn = Database::connect(opt).await.unwrap(); + + sea_orm_conn.ping() + .await + .map_err(|e| error!("database init failed: {}", e.to_string())) + .unwrap(); + + APPLICATION_CONTEXT.set::(sea_orm_conn); + + info!("sea_orm[mysql] database success! {}", &cfg.mysql.dsn); +} \ No newline at end of file diff --git a/src/initialize/logger.rs b/src/initialize/logger.rs new file mode 100644 index 0000000..b5c7357 --- /dev/null +++ b/src/initialize/logger.rs @@ -0,0 +1,18 @@ +use tracing_subscriber::{EnvFilter, fmt}; +use tracing_subscriber::fmt::time::LocalTime; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +pub fn init_trace_logger() { + // 设置日志级别 根据RUST_LOG + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + + let timer = LocalTime::rfc_3339(); + // 构建layer 控制台打印 + let console_layer = fmt::layer().with_timer(timer); + + tracing_subscriber::registry() + .with(env_filter) + .with(console_layer) + .init(); +} diff --git a/src/initialize/mod.rs b/src/initialize/mod.rs new file mode 100644 index 0000000..0c80ea4 --- /dev/null +++ b/src/initialize/mod.rs @@ -0,0 +1,3 @@ +pub mod logger; +pub mod config; +pub mod database; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8e2b18e..e256cd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,40 @@ -pub mod entity; +use ::state::type_map::TypeMapSendSync; +use tracing::info; + pub mod state; -pub mod config; +pub mod entity; +// pub mod state; pub mod handler; pub mod view; -pub mod err; -pub mod router; pub mod param; pub mod form; +pub mod service; +pub mod config; +pub mod router; +pub mod initialize; +pub mod common; -pub type Result = std::result::Result; \ No newline at end of file + +/// 整个项目上下文 ApplicationContext +pub static APPLICATION_CONTEXT: TypeMapSendSync = TypeMapSendSync::new(); +// pub static APPLICATION_CONTEXT: TypeMap![Send + Sync] = ::new(); + +pub async fn init_context() { + // 环境变量初始化 解析 .env 文件 + dotenvy::dotenv().expect("解析.env文件失败"); + + // 日志初始化 + initialize::logger::init_trace_logger(); + + // 配置文件初始化 + initialize::config::init_config().await; + + info!("ConfigContext init complete"); + + // 初始化数据源 + initialize::database::init_database().await; + info!("DataBase init complete"); + + // 冻结 只能使用get 避免额外的开销 + // APPLICATION_CONTEXT.freeze(); +} diff --git a/src/main.rs b/src/main.rs index aab6960..06c8487 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,49 +1,23 @@ -use std::sync::Arc; +use std::net::IpAddr; -use sea_orm::{ConnectOptions, Database}; use tokio::signal; -use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; -use tracing_subscriber::fmt::time::LocalTime; - -use axum_with_seaorm::{config::AppConfig, router, state::AppState}; - -fn init_trace_logger() { - // 设置日志级别 根据RUST_LOG - let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); - - let timer = LocalTime::rfc_3339(); - // 构建layer 控制台打印 - let console_layer = fmt::layer().with_timer(timer); - - tracing_subscriber::registry() - .with(env_filter) - .with(console_layer) - .init(); -} +// use tracing_subscriber::util::SubscriberInitExt; +use axum_with_seaorm::{APPLICATION_CONTEXT, init_context, router}; +use axum_with_seaorm::config::app::AppConfig; #[tokio::main] async fn main() { - // 解析 .env 文件 - dotenvy::dotenv().expect("解析.env文件失败"); + // 初始化 + init_context().await; - let _guard = init_trace_logger(); + let cfg = APPLICATION_CONTEXT.get::(); + let ip_addr = cfg.server.host.parse::().unwrap(); - let cfg = AppConfig::new() - .map_err(|e| tracing::error!("初始化配置失败:{}", e.to_string())) - .unwrap(); + let app = router::init(); + let listener = tokio::net::TcpListener::bind((ip_addr, cfg.server.port)).await.unwrap(); - let mut opt = ConnectOptions::new(&cfg.mysql.dsn); - opt.max_connections(cfg.mysql.max_cons); - - let app = router::init() - .with_state(Arc::new(AppState { - conn: Database::connect(opt).await.unwrap(), - })); - - let listener = tokio::net::TcpListener::bind(&cfg.web.addr).await.unwrap(); - - tracing::info!("服务器运行于: {}", listener.local_addr().unwrap()); + tracing::info!("Server runs on: {}", listener.local_addr().unwrap()); axum::serve(listener, app).with_graceful_shutdown(shutdown_signal()).await.unwrap() } diff --git a/src/router.rs b/src/router.rs deleted file mode 100644 index dfc2a17..0000000 --- a/src/router.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::sync::Arc; -use axum::routing::get; -use crate::handler; -use crate::state::AppState; - -pub fn init() -> axum::Router> { - let category_router = axum::Router::new() - .route("/", get(handler::category::index)) - .route( - "/add", - get(handler::category::add_ui).post(handler::category::add), - ) - .route( - "/edit/:id", - get(handler::category::edit_ui).post(handler::category::edit), - ) - .route("/del/:id", get(handler::category::del)) - .route("/del/:id/:real", get(handler::category::del)) - .route("/articles/:id", get(handler::category::articles)); - - let article_router = axum::Router::new() - .route("/", get(handler::article::index)) - .route("/add", get(handler::article::add_ui).post(handler::article::add)) - .route("/tags", get(handler::article::list_with_tags)); - - axum::Router::new() - .route("/", get(handler::index)) - .nest("/category", category_router) - .nest("/article", article_router) -} diff --git a/src/router/mod.rs b/src/router/mod.rs new file mode 100644 index 0000000..3960d3f --- /dev/null +++ b/src/router/mod.rs @@ -0,0 +1,13 @@ +use axum::routing::get; +use crate::handler; +use crate::handler::{category, article}; + +pub fn init() -> axum::Router { + let category_router = category::init_router(); + let article_router = article::init_router(); + + axum::Router::new() + .route("/", get(handler::index)) + .nest("/category", category_router) + .nest("/article", article_router) +} diff --git a/src/service/article.rs b/src/service/article.rs new file mode 100644 index 0000000..9428631 --- /dev/null +++ b/src/service/article.rs @@ -0,0 +1,115 @@ +use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, NotSet, PaginatorTrait}; +use sea_orm::{ActiveModelTrait, QueryFilter, QueryOrder, QuerySelect}; +use sea_orm::ActiveValue::Set; + +use crate::{APPLICATION_CONTEXT, view}; +use crate::common::err::{AppError, log_error}; +use crate::common::Result; +use crate::entity::{article, category, tag}; +use crate::form::ArticleForm; +use crate::param::ArticleParams; + +pub struct ArticleService; + +impl ArticleService { + pub async fn index(handler_name: &str, params: ArticleParams) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + + let condition = Condition::all() + .add(article::Column::IsDel.eq(false)) + .add_option( + params.keyword_opt().map(|n| article::Column::Title.contains(&n)) + ) + .add_option(params.is_del_opt().map(|n| article::Column::IsDel.eq(n))); + + let mut select = article::Entity::find() + .filter(condition); + + let page = params.page(); // 当前页码 + let page_size = params.page_size(); // 每页条数,默认15 + + if let Some(ord) = params.order() { + select = select.order_by(category::Column::Id, ord); + } + + let paginator = select + .find_also_related(category::Entity) + .paginate(conn, page_size); + + let page_total = paginator.num_pages().await.map_err(AppError::from)?; + + let list: Vec<(article::Model, Option)> = paginator + .fetch_page(page) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + + Ok(view::ArticlesTemplate { + list, + page_total, + params, + }) + } + + pub async fn add_ui(handler_name: &str) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + + let categies = category::Entity::find() + .filter(category::Column::IsDel.eq(false)) + .limit(100) + .order_by_asc(category::Column::Id) + .all(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + + Ok(view::ArticleAddTemplate { categies }) + } + + pub async fn add(handler_name: &str, frm: ArticleForm) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + article::ActiveModel { + id: NotSet, + title: Set(frm.title), + category_id: Set(frm.category_id), + content: Set(frm.content), + ..Default::default() + }.save(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name)) + } + pub async fn list_with_tags(handler_name: &str) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + + let list = article::Entity::find() + .find_with_related(tag::Entity) + .all(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + + let mut ss = vec![]; + + for item in list { + let (article, tags) = item; + let tags = tags + .iter() + .map(|tag| format!("【#{} - {}】", &tag.id, &tag.name)) + .collect::>() + .join(",") + .to_string(); + + let s = format!( + "文章ID: {}, 文章标题: {}, 标签: {}", + &article.id, &article.title, tags, + ); + ss.push(s); + } + + Ok(ss.join("\n").to_string()) + } +} + + + diff --git a/src/service/category.rs b/src/service/category.rs new file mode 100644 index 0000000..2597c81 --- /dev/null +++ b/src/service/category.rs @@ -0,0 +1,191 @@ +use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, EntityTrait, IntoActiveModel, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder}; +use sea_orm::ActiveValue::Set; + +use crate::{APPLICATION_CONTEXT, form, view}; +use crate::common::err::{AppError, log_error}; +use crate::common::Result; +use crate::entity::{article, category}; +use crate::param::{CategoryParams, DelParams}; + +pub struct CategoryService; + +impl CategoryService { + pub async fn index(handler_name: &str, params: CategoryParams) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + + let page = params.page(); // 当前页码 + let page_size = params.page_size(); // 每页条数,默认15 + let mut select = category::Entity::find() + .filter( + Condition::all() + .add_option( + params.keyword_opt().map(|n| category::Column::Name.contains(&n)) + ) + .add_option(params.is_del_opt().map(|n| category::Column::IsDel.eq(n))) + ); + + if let Some(ord) = params.order() { + select = select.order_by(category::Column::Id, ord); + } + + let paginator = select.paginate(conn, page_size); + + let page_total = paginator.num_pages().await.map_err(AppError::from)?; + + if page_total == 0 { + return Ok(view::CategoryTemplate { + categories: vec![], + params, + page_total, + }); + } + + let categories = paginator + .fetch_page(page) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + + Ok(view::CategoryTemplate { + categories, + params, + page_total, + }) + } + + pub async fn find(handler_name: &str, id: u32) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + + let cate = category::Entity::find_by_id(id) + .one(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + + match cate { + None => Err(AppError::notfound()), + Some(cate) => Ok(cate), + } + } + + pub async fn add(handler_name: &str, frm: form::CategoryForm) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + + let am = category::ActiveModel { + name: Set(frm.name), + // all other attributes are `NotSet` + ..Default::default() + }; + + let added_category = am.insert(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + + Ok(added_category.id) + } + + pub async fn edit_ui(handler_name: &str, id: u32) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + + let cate = category::Entity::find_by_id(id) + .one(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))? + .ok_or(AppError::notfound())?; + + Ok(view::CategoryEditTemplate { category: cate }) + } + + pub async fn edit(handler_name: &str, id: u32, frm: form::CategoryForm) -> Result<()> { + let conn = APPLICATION_CONTEXT.get::(); + + let cate = category::Entity::find_by_id(id) + .one(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))? + .ok_or(AppError::notfound())?; + + let mut am = cate.into_active_model(); + am.name = Set(frm.name); + + am.save(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + + // am.update(conn) + // .await + // .map_err(AppError::from) + // .map_err(log_error(handler_name))?; + + Ok(()) + } + + + pub async fn del(handler_name: &str, params: DelParams) -> Result<()> { + let conn = APPLICATION_CONTEXT.get::(); + let real = params.real.unwrap_or(false); + + let cate = category::Entity::find_by_id(params.id) + .one(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))? + .ok_or(AppError::notfound())?; + + if real { + cate.delete(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + } else { + let mut am = cate.into_active_model(); + am.is_del = Set(true); + am.save(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + } + + Ok(()) + } + + pub async fn articles(handler_name: &str, id: u32, params: CategoryParams) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + + let cate = category::Entity::find_by_id(id) + .one(conn) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))? + .ok_or(AppError::notfound()) + .map_err(log_error(handler_name))?; + + let paginator = cate.find_related(article::Entity) + .paginate(conn, params.page_size()); + + let articles = paginator.fetch_page(params.page()) + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + + let page_total = paginator + .num_pages() + .await + .map_err(AppError::from) + .map_err(log_error(handler_name))?; + + Ok(view::CategoryArticlesTemplate { + params, + page_total, + category: cate, + articles, + }) + } +} + + + diff --git a/src/service/mod.rs b/src/service/mod.rs new file mode 100644 index 0000000..3e7e78f --- /dev/null +++ b/src/service/mod.rs @@ -0,0 +1,2 @@ +pub mod article; +pub mod category; \ No newline at end of file