服务架构调整

This commit is contained in:
胡天 2024-02-07 09:47:43 +08:00
parent 5d75c53356
commit 3f1941667c
28 changed files with 666 additions and 467 deletions

2
.env
View File

@ -1,5 +1,5 @@
# 日志级别 # 日志级别
RUST_LOG=INFO RUST_LOG=INFO
# 运行模式 # 运行模式 要与配置文件相符 默认为dev
RUN_MODE=dev RUN_MODE=dev

81
Cargo.lock generated
View File

@ -252,9 +252,11 @@ dependencies = [
"sea-orm", "sea-orm",
"serde", "serde",
"serde_json", "serde_json",
"state",
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"validator",
] ]
[[package]] [[package]]
@ -755,6 +757,19 @@ dependencies = [
"slab", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@ -990,6 +1005,16 @@ dependencies = [
"cc", "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]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@ -1116,6 +1141,21 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 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]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -1864,6 +1904,12 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -2363,6 +2409,15 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "state"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
dependencies = [
"loom",
]
[[package]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@ -2768,7 +2823,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna 0.5.0",
"percent-encoding", "percent-encoding",
] ]
@ -2787,6 +2842,21 @@ dependencies = [
"serde", "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]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"
@ -2899,6 +2969,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"

View File

@ -17,3 +17,5 @@ tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "time", "local-time"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "time", "local-time"] }
serde_json = "1.0.113" serde_json = "1.0.113"
chrono = "0.4.33" chrono = "0.4.33"
state = "0.6.0"
validator = "0.16.1"

View File

@ -1,5 +1,6 @@
[web] [server]
addr = "0.0.0.0:9527" port = 9527
host = "0.0.0.0"
[mysql] [mysql]
max_cons = 5 max_cons = 5

View File

@ -70,4 +70,14 @@ impl axum::response::IntoResponse for AppError {
}; };
msg.into_response() msg.into_response()
} }
}
/// 记录错误
pub fn log_error(handler_name: &str) -> Box<dyn Fn(AppError) -> AppError> {
let handler_name = handler_name.to_string();
Box::new(move |err| {
tracing::error!("{}: {:?}", handler_name, err);
err
})
} }

6
src/common/mod.rs Normal file
View File

@ -0,0 +1,6 @@
pub mod template;
pub mod err;
pub mod response;
pub type Result<T> = std::result::Result<T, err::AppError>;

15
src/common/response.rs Normal file
View File

@ -0,0 +1,15 @@
use axum::http::{header, HeaderMap, StatusCode};
use axum::response::Html;
use super::Result;
pub type HtmlResponse = Html<String>;
pub type RedirectResponse = (StatusCode, HeaderMap, ());
pub fn redirect(url: &str) -> Result<RedirectResponse> {
let mut header = HeaderMap::new();
header.insert(header::LOCATION, url.parse().unwrap());
Ok((StatusCode::FOUND, header, ()))
}

15
src/common/template.rs Normal file
View File

@ -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<T: Template>(tpl: T, handler_name: &str) -> Result<HtmlResponse> {
let out = tpl
.render()
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
Ok(Html(out))
}

View File

@ -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<Self> {
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)
}
}

10
src/config/app.rs Normal file
View File

@ -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,
}

7
src/config/database.rs Normal file
View File

@ -0,0 +1,7 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct MysqlConfig {
pub dsn: String,
pub max_cons: u32,
}

3
src/config/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod server;
pub mod database;
pub mod app;

8
src/config/server.rs Normal file
View File

@ -0,0 +1,8 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct ServerConfig {
///当前服务地址
pub host: String,
pub port: u16,
}

View File

@ -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 crate::common::{response::HtmlResponse, Result};
use axum::Form; use crate::common::response::{redirect, RedirectResponse};
use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, NotSet, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect}; use crate::common::template::render;
use sea_orm::ActiveValue::Set;
use crate::entity::{article, category, tag};
use crate::err::AppError;
use crate::form::ArticleForm; use crate::form::ArticleForm;
use crate::handler::{get_conn, HtmlResponse, log_error, redirect, RedirectResponse, render};
use crate::param::ArticleParams; use crate::param::ArticleParams;
use crate::Result; use crate::service::article::ArticleService;
use crate::state::AppState;
use crate::view;
pub async fn index( pub fn init_router() -> Router {
State(state): State<Arc<AppState>>, Router::new()
Query(params): Query<ArticleParams>, .route("/", get(index))
) -> Result<HtmlResponse> { .route("/add", get(add_ui).post(add))
.route("/tags", get(list_with_tags))
}
pub async fn index(Query(params): Query<ArticleParams>) -> Result<HtmlResponse> {
let handler_name = "article/index"; let handler_name = "article/index";
let conn = get_conn(&state); let tpl = ArticleService::index(handler_name, params).await?;
// 构建条件: 所有未被删除的文章
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<category::Model>)> = paginator
.fetch_page(page)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
let tpl = view::ArticlesTemplate {
list,
page_total,
params,
};
render(tpl, handler_name) render(tpl, handler_name)
} }
pub async fn add_ui(State(state): State<Arc<AppState>>) -> Result<HtmlResponse> { pub async fn add_ui() -> Result<HtmlResponse> {
let handler_name = "article/add_ui"; let handler_name = "article/add_ui";
let conn = get_conn(&state); let tpl = ArticleService::add_ui(handler_name).await?;
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 };
render(tpl, handler_name) render(tpl, handler_name)
} }
pub async fn add( pub async fn add(Form(frm): Form<ArticleForm>) -> Result<RedirectResponse> {
State(state): State<Arc<AppState>>,
Form(frm): Form<ArticleForm>,
) -> Result<RedirectResponse> {
let handler_name = "article/add"; let handler_name = "article/add";
let conn = get_conn(&state); ArticleService::add(handler_name, frm).await?;
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))?;
redirect("/article?msg=文章添加成功") redirect("/article?msg=文章添加成功")
} }
pub async fn list_with_tags( State(state): State<Arc<AppState>>) -> Result<String> { pub async fn list_with_tags() -> Result<String> {
let handler_name = "article/list_with_tags"; let handler_name = "article/list_with_tags";
let conn = get_conn(&state); ArticleService::list_with_tags(handler_name).await
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::<Vec<String>>()
.join(",")
.to_string();
let s = format!(
"文章ID: {}, 文章标题: {}, 标签: {}",
&article.id, &article.title, tags,
);
ss.push(s);
}
Ok(ss.join("\n").to_string())
} }

View File

@ -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 crate::{form, view};
use axum::Form; use crate::common::{response::HtmlResponse, Result};
use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder}; use crate::common::response::{redirect, RedirectResponse};
use sea_orm::ActiveValue::Set; use crate::common::template::render;
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::param::{CategoryParams, DelParams}; use crate::param::{CategoryParams, DelParams};
use crate::state::AppState; use crate::service::category::CategoryService;
pub async fn index( pub fn init_router() -> Router {
State(state): State<Arc<AppState>>, Router::new()
Query(params): Query<CategoryParams>, .route("/", get(index))
) -> Result<HtmlResponse> { .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<CategoryParams>) -> Result<HtmlResponse> {
let handler_name = "category/index"; let handler_name = "category/index";
let conn = get_conn(&state); let tpl = CategoryService::index(handler_name, params).await?;
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,
};
render(tpl, handler_name) render(tpl, handler_name)
} }
pub async fn find( pub async fn find(Path(id): Path<u32>) -> Result<String> {
State(state): State<Arc<AppState>>,
Path(id): Path<i32>,
) -> Result<String> {
let id = u32::try_from(id).or(Err(AppError::notfound()))?;
let handler_name = "category/find"; let handler_name = "category/find";
let conn = get_conn(&state); let category_model = CategoryService::find(handler_name, id).await?;
Ok(format!("id: {}, 名称: {}", category_model.id, category_model.name))
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)),
}
} }
pub async fn add_ui() -> Result<HtmlResponse> { pub async fn add_ui() -> Result<HtmlResponse> {
@ -89,145 +44,37 @@ pub async fn add_ui() -> Result<HtmlResponse> {
render(tpl, handler_name) render(tpl, handler_name)
} }
pub async fn add( pub async fn add(Form(frm): Form<form::CategoryForm>) -> Result<RedirectResponse> {
State(state): State<Arc<AppState>>,
Form(frm): Form<form::CategoryForm>,
) -> Result<RedirectResponse> {
let handler_name = "category/add"; let handler_name = "category/add";
let conn = get_conn(&state); let added_category_id = CategoryService::add(handler_name, frm).await?;
let url = format!("/category?msg=分类添加成功ID是{}", added_category_id);
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);
redirect(url.as_str()) redirect(url.as_str())
} }
pub async fn edit_ui( pub async fn edit_ui(Path(id): Path<u32>) -> Result<HtmlResponse> {
State(state): State<Arc<AppState>>,
Path(id): Path<i32>,
) -> Result<HtmlResponse> {
let id = u32::try_from(id).or(Err(AppError::notfound()))?;
let handler_name = "category/edit_ui"; let handler_name = "category/edit_ui";
let conn = get_conn(&state); let tpl = CategoryService::edit_ui(handler_name, id).await?;
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 };
render(tpl, handler_name) render(tpl, handler_name)
} }
pub async fn edit( pub async fn edit(
State(state): State<Arc<AppState>>,
Path(id): Path<u32>, Path(id): Path<u32>,
Form(frm): Form<form::CategoryForm>, Form(frm): Form<form::CategoryForm>,
) -> Result<RedirectResponse> { ) -> Result<RedirectResponse> {
let id = u32::try_from(id).or(Err(AppError::notfound()))?;
let handler_name = "category/edit"; let handler_name = "category/edit";
let conn = get_conn(&state); CategoryService::edit(handler_name, id, frm).await?;
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))?;
redirect("/category?msg=分类修改成功") redirect("/category?msg=分类修改成功")
} }
pub async fn del( pub async fn del(Path(params): Path<DelParams>) -> Result<RedirectResponse> {
State(state): State<Arc<AppState>>,
Path(params): Path<DelParams>,
) -> Result<RedirectResponse> {
let handler_name = "category/del"; let handler_name = "category/del";
let conn = get_conn(&state); CategoryService::del(handler_name, params).await?;
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))?;
}
redirect("/category?msg=分类删除成功") redirect("/category?msg=分类删除成功")
} }
pub async fn articles( pub async fn articles(Path(id): Path<u32>, Query(params): Query<CategoryParams>) -> Result<HtmlResponse> {
State(state): State<Arc<AppState>>,
Path(id): Path<u32>,
Query(params): Query<CategoryParams>,
) -> Result<HtmlResponse> {
let handler_name = "category/articles"; let handler_name = "category/articles";
let conn = get_conn(&state); let tpl = CategoryService::articles(handler_name, id, params).await?;
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,
};
render(tpl, handler_name) render(tpl, handler_name)
} }

View File

@ -1,5 +1,7 @@
use crate::handler::{HtmlResponse, render}; use crate::common::response::HtmlResponse;
use crate::{Result, view}; use crate::common::Result;
use crate::common::template::render;
use crate::view;
pub async fn index() -> Result<HtmlResponse> { pub async fn index() -> Result<HtmlResponse> {
let handler_name = "index"; let handler_name = "index";

View File

@ -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; pub use index::index;
use crate::err::AppError;
use crate::Result;
use crate::state::AppState;
pub mod category; pub mod category;
mod index; mod index;
pub mod article; pub mod article;
fn get_conn(state: &AppState) -> &DatabaseConnection {
&state.conn
}
type HtmlResponse = Html<String>;
type RedirectResponse = (StatusCode, HeaderMap, ());
/// 渲染模板
fn render<T: Template>(tpl: T, handler_name: &str) -> Result<HtmlResponse> {
let out = tpl
.render()
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
Ok(Html(out))
}
/// 记录错误
fn log_error(handler_name: &str) -> Box<dyn Fn(AppError) -> AppError> {
let handler_name = handler_name.to_string();
Box::new(move |err| {
tracing::error!("{}: {:?}", handler_name, err);
err
})
}
fn redirect(url: &str) -> Result<RedirectResponse> {
let mut header = HeaderMap::new();
header.insert(header::LOCATION, url.parse().unwrap());
Ok((StatusCode::FOUND, header, ()))
}

30
src/initialize/config.rs Normal file
View File

@ -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::<AppConfig>(config);
}

View File

@ -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::<AppConfig>();
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::<DatabaseConnection>(sea_orm_conn);
info!("sea_orm[mysql] database success! {}", &cfg.mysql.dsn);
}

18
src/initialize/logger.rs Normal file
View File

@ -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();
}

3
src/initialize/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod logger;
pub mod config;
pub mod database;

View File

@ -1,11 +1,40 @@
pub mod entity; use ::state::type_map::TypeMapSendSync;
use tracing::info;
pub mod state; pub mod state;
pub mod config; pub mod entity;
// pub mod state;
pub mod handler; pub mod handler;
pub mod view; pub mod view;
pub mod err;
pub mod router;
pub mod param; pub mod param;
pub mod form; pub mod form;
pub mod service;
pub mod config;
pub mod router;
pub mod initialize;
pub mod common;
pub type Result<T> = std::result::Result<T, err::AppError>;
/// 整个项目上下文 ApplicationContext
pub static APPLICATION_CONTEXT: TypeMapSendSync = TypeMapSendSync::new();
// pub static APPLICATION_CONTEXT: TypeMap![Send + Sync] = <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();
}

View File

@ -1,49 +1,23 @@
use std::sync::Arc; use std::net::IpAddr;
use sea_orm::{ConnectOptions, Database};
use tokio::signal; use tokio::signal;
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; // use tracing_subscriber::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 axum_with_seaorm::{APPLICATION_CONTEXT, init_context, router};
use axum_with_seaorm::config::app::AppConfig;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// 解析 .env 文件 // 初始化
dotenvy::dotenv().expect("解析.env文件失败"); init_context().await;
let _guard = init_trace_logger(); let cfg = APPLICATION_CONTEXT.get::<AppConfig>();
let ip_addr = cfg.server.host.parse::<IpAddr>().unwrap();
let cfg = AppConfig::new() let app = router::init();
.map_err(|e| tracing::error!("初始化配置失败:{}", e.to_string())) let listener = tokio::net::TcpListener::bind((ip_addr, cfg.server.port)).await.unwrap();
.unwrap();
let mut opt = ConnectOptions::new(&cfg.mysql.dsn); tracing::info!("Server runs on: {}", listener.local_addr().unwrap());
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());
axum::serve(listener, app).with_graceful_shutdown(shutdown_signal()).await.unwrap() axum::serve(listener, app).with_graceful_shutdown(shutdown_signal()).await.unwrap()
} }

View File

@ -1,30 +0,0 @@
use std::sync::Arc;
use axum::routing::get;
use crate::handler;
use crate::state::AppState;
pub fn init() -> axum::Router<Arc<AppState>> {
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)
}

13
src/router/mod.rs Normal file
View File

@ -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)
}

115
src/service/article.rs Normal file
View File

@ -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<view::ArticlesTemplate> {
let conn = APPLICATION_CONTEXT.get::<DatabaseConnection>();
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<category::Model>)> = 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<view::ArticleAddTemplate> {
let conn = APPLICATION_CONTEXT.get::<DatabaseConnection>();
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<article::ActiveModel> {
let conn = APPLICATION_CONTEXT.get::<DatabaseConnection>();
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<String> {
let conn = APPLICATION_CONTEXT.get::<DatabaseConnection>();
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::<Vec<String>>()
.join(",")
.to_string();
let s = format!(
"文章ID: {}, 文章标题: {}, 标签: {}",
&article.id, &article.title, tags,
);
ss.push(s);
}
Ok(ss.join("\n").to_string())
}
}

191
src/service/category.rs Normal file
View File

@ -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<view::CategoryTemplate> {
let conn = APPLICATION_CONTEXT.get::<DatabaseConnection>();
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<category::Model> {
let conn = APPLICATION_CONTEXT.get::<DatabaseConnection>();
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<u32> {
let conn = APPLICATION_CONTEXT.get::<DatabaseConnection>();
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<view::CategoryEditTemplate> {
let conn = APPLICATION_CONTEXT.get::<DatabaseConnection>();
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::<DatabaseConnection>();
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::<DatabaseConnection>();
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<view::CategoryArticlesTemplate> {
let conn = APPLICATION_CONTEXT.get::<DatabaseConnection>();
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,
})
}
}

2
src/service/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod article;
pub mod category;