服务架构调整
This commit is contained in:
parent
5d75c53356
commit
3f1941667c
2
.env
2
.env
|
@ -1,5 +1,5 @@
|
||||||
# 日志级别
|
# 日志级别
|
||||||
RUST_LOG=INFO
|
RUST_LOG=INFO
|
||||||
|
|
||||||
# 运行模式
|
# 运行模式 要与配置文件相符 默认为dev
|
||||||
RUN_MODE=dev
|
RUN_MODE=dev
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -71,3 +71,13 @@ 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
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod template;
|
||||||
|
pub mod err;
|
||||||
|
pub mod response;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, err::AppError>;
|
||||||
|
|
|
@ -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, ()))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MysqlConfig {
|
||||||
|
pub dsn: String,
|
||||||
|
pub max_cons: u32,
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod server;
|
||||||
|
pub mod database;
|
||||||
|
pub mod app;
|
|
@ -0,0 +1,8 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
///当前服务地址
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
}
|
|
@ -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())
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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, ()))
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod logger;
|
||||||
|
pub mod config;
|
||||||
|
pub mod database;
|
39
src/lib.rs
39
src/lib.rs
|
@ -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();
|
||||||
|
}
|
||||||
|
|
48
src/main.rs
48
src/main.rs
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod article;
|
||||||
|
pub mod category;
|
Loading…
Reference in New Issue