This commit is contained in:
quantulr
2023-11-12 22:30:58 +08:00
commit bd1bb4952a
28 changed files with 3101 additions and 0 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
DATABASE_URL=mysql://kirara:kirara169482@localhost:3306/caszl
UPLOAD_PATH=D:/Downloads/upload_path

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

11
.idea/caszl-front-rs.iml generated Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/caszl-front-rs.iml" filepath="$PROJECT_DIR$/.idea/caszl-front-rs.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

2618
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "caszl-front-rs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.6.20"
chrono = "0.4.31"
dotenvy = "0.15.7"
sea-orm = { version = "0.12.4", features = ["sqlx-mysql", "runtime-tokio-native-tls", "macros"] }
serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108"
tokio = { version = "1.34.0", features = ["full"] }

1
src/dto.rs Normal file
View File

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

1
src/dto/article.rs Normal file
View File

@ -0,0 +1 @@

2
src/entity.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod la_article;
pub mod la_article_category;

32
src/entity/la_article.rs Normal file
View File

@ -0,0 +1,32 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, DeriveEntityModel, Eq)]
#[serde(rename_all = "camelCase")]
#[sea_orm(table_name = "la_article")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: u32,
pub cid: u32,
pub title: String,
pub intro: String,
pub summary: Option<String>,
pub image: String,
#[sea_orm(column_type = "Text", nullable)]
pub content: Option<String>,
pub author: String,
pub visit: u32,
pub sort: u32,
pub is_show: u8,
pub is_delete: u8,
pub create_time: u32,
pub update_time: u32,
pub delete_time: u32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,22 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "la_article_category")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: u32,
pub name: String,
pub sort: u16,
pub is_show: u8,
pub is_delete: u8,
pub create_time: Option<u32>,
pub update_time: Option<u32>,
pub delete_time: Option<u32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

1
src/handler.rs Normal file
View File

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

30
src/handler/article.rs Normal file
View File

@ -0,0 +1,30 @@
use crate::entity::la_article;
use crate::params::article::{ArticleDetailParams, ArticleListParams};
use crate::state::app::AppState;
use axum::extract::{Query, State};
use axum::Json;
use chrono::{TimeZone, Utc};
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QueryTrait};
use serde_json::{json, Value};
use std::fmt::Debug;
use std::sync::Arc;
use crate::response::article::{ArticleDetail, ArticleListItem};
use crate::response::common::{BaseResponse, PageResponse};
use crate::service::artile::ArticleService;
pub async fn article_list(
State(state): State<Arc<AppState>>,
query: Query<ArticleListParams>,
) -> Result<Json<BaseResponse<PageResponse<ArticleListItem>>>, Json<BaseResponse<Vec<u8>>>> {
let article_service = ArticleService { state };
article_service.article_list(query.0).await
}
pub async fn article_detail(
State(state): State<Arc<AppState>>,
query: Query<ArticleDetailParams>,
) -> Result<Json<BaseResponse<ArticleDetail>>, Json<BaseResponse<Vec<u8>>>> {
let article_service = ArticleService { state };
article_service.article_detail(query.0).await
}

31
src/main.rs Normal file
View File

@ -0,0 +1,31 @@
mod dto;
mod entity;
mod handler;
mod params;
mod repository;
mod response;
mod routes;
mod service;
mod state;
use crate::state::app::AppState;
use axum::{Server, ServiceExt};
use sea_orm::Database;
use std::env;
use std::sync::Arc;
#[tokio::main]
async fn main() {
dotenvy::dotenv().ok();
let db_url = env::var("DATABASE_URL").expect(".env 文件中没有设置 DATABASE_URL");
let db_conn = Database::connect(&db_url).await.expect("数据库链接失败");
let app_state = AppState { db_conn };
let app = routes::create_routes(Arc::new(app_state));
Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app)
.await
.unwrap();
}

1
src/params.rs Normal file
View File

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

17
src/params/article.rs Normal file
View File

@ -0,0 +1,17 @@
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArticleListParams {
pub page_no: Option<u64>,
pub page_size: Option<u64>,
pub cid: Option<u32>,
pub sort: Option<String>,
pub keyword: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArticleDetailParams {
pub id: String,
}

1
src/repository.rs Normal file
View File

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

View File

@ -0,0 +1 @@

2
src/response.rs Normal file
View File

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

42
src/response/article.rs Normal file
View File

@ -0,0 +1,42 @@
use crate::entity::la_article;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArticleListItem {
pub id: u32,
pub title: String,
pub image: Option<String>,
pub intro: Option<String>,
pub visit: u32,
pub collect: bool,
pub create_time: String,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArticleDetail {
pub id: u32,
pub cid: Option<u32>,
pub category: Option<String>,
pub title: String,
pub intro: Option<String>,
pub summary: Option<String>,
pub image: Option<String>,
pub content: Option<String>,
pub author: String,
pub visit: u32,
pub sort: u32,
pub is_collect: u32,
pub create_time: String,
pub update_time: Option<String>,
pub prev: Option<SiblingArticle>,
pub next: Option<SiblingArticle>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SiblingArticle {
pub id: u32,
pub title: String,
}

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

@ -0,0 +1,19 @@
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BaseResponse<T: Serialize> {
pub code: u16,
pub message: String,
pub data: T,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PageResponse<T: Serialize> {
pub page_no: u64,
pub page_size: u64,
pub count: u64,
pub lists: Vec<T>,
}

20
src/routes.rs Normal file
View File

@ -0,0 +1,20 @@
use crate::handler::article::{article_detail, article_list};
use crate::state::app::AppState;
use axum::routing::{get, IntoMakeService};
use axum::{Router, ServiceExt};
use std::sync::Arc;
pub fn create_routes(app_state: Arc<AppState>) -> IntoMakeService<Router> {
Router::new()
.nest(
"/api",
Router::new()
.nest("/article", Router::new().route("/list", get(article_list)))
.nest(
"/pc",
Router::new().route("/articleDetail", get(article_detail)),
),
)
.with_state(app_state)
.into_make_service()
}

1
src/service.rs Normal file
View File

@ -0,0 +1 @@
pub mod artile;

202
src/service/artile.rs Normal file
View File

@ -0,0 +1,202 @@
use std::sync::Arc;
use crate::entity::la_article;
use crate::params::article::{ArticleDetailParams, ArticleListParams};
use crate::response::article::{ArticleDetail, ArticleListItem, SiblingArticle};
use crate::response::common::{BaseResponse, PageResponse};
use crate::state::app::AppState;
use axum::http::StatusCode;
use axum::Json;
use chrono::{TimeZone, Utc};
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QueryTrait};
pub struct ArticleService {
pub state: Arc<AppState>,
}
impl ArticleService {
pub fn new(state: &Arc<AppState>) -> Self {
Self {
state: Arc::clone(state),
}
}
pub async fn article_list(
&self,
params: ArticleListParams,
) -> Result<Json<BaseResponse<PageResponse<ArticleListItem>>>, Json<BaseResponse<Vec<u8>>>>
{
let page_no = params.page_no.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let articles_selection = la_article::Entity::find()
.filter(la_article::Column::IsDelete.ne(1))
.filter(la_article::Column::IsShow.ne(0));
let count = match articles_selection.clone().count(&self.state.db_conn).await {
Ok(total) => total,
Err(_) => 0,
};
let articles_result = articles_selection
.apply_if(Some(params.sort), |mut query, v| {
match v {
Some(sort_str) => {
// 最热
if sort_str == "hot" {
query
.order_by_desc(la_article::Column::Visit)
.order_by_desc(la_article::Column::Id)
}
// 最新
else if sort_str == "new" {
query.order_by_desc(la_article::Column::Id)
} else {
query
.order_by_desc(la_article::Column::Sort)
.order_by_desc(la_article::Column::Id)
}
}
None => query,
}
})
.paginate(&self.state.db_conn, params.page_size.unwrap_or(20))
.fetch_page(params.page_no.unwrap_or(1) - 1)
.await;
match articles_result {
Ok(articles) => {
let mut article_list: Vec<ArticleListItem> = vec![];
for article in articles {
let dt = Utc.timestamp_opt(article.create_time as i64, 0);
let article_item = ArticleListItem {
id: article.id,
title: article.title,
image: Some(article.image),
intro: Some(article.intro),
visit: article.visit,
collect: false,
create_time: dt.unwrap().format("%Y-%m-%d %H:%M:%S").to_string(),
};
article_list.push(article_item);
}
let page_data = PageResponse {
count,
page_no,
page_size,
lists: article_list,
};
let response = BaseResponse {
code: StatusCode::OK.as_u16(),
message: "成功".to_string(),
data: page_data,
};
Ok(Json(response))
}
Err(err) => {
let err_resp = BaseResponse {
code: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: format!("{}", err),
data: vec![],
};
Err(Json(err_resp))
}
}
}
pub async fn article_detail(
&self,
params: ArticleDetailParams,
) -> Result<Json<BaseResponse<ArticleDetail>>, Json<BaseResponse<Vec<u8>>>> {
let id: u32 = match params.id.parse() {
Ok(id) => id,
Err(_) => {
let err_resp = BaseResponse {
code: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "请输入正确的文章 id".to_string(),
data: vec![],
};
return Err(Json(err_resp));
}
};
match la_article::Entity::find_by_id(id)
.filter(la_article::Column::IsDelete.eq(0))
.filter(la_article::Column::IsShow.eq(1))
.one(&self.state.db_conn)
.await
{
Ok(Some(article)) => {
// 上一篇文章
let prev_result = la_article::Entity::find()
.filter(la_article::Column::IsDelete.eq(0))
.filter(la_article::Column::IsShow.eq(1))
.filter(la_article::Column::Id.lt(article.id))
.order_by_desc(la_article::Column::Sort)
.order_by_desc(la_article::Column::Id)
.one(&self.state.db_conn)
.await;
let prev = match prev_result {
Ok(Some(article)) => Some(SiblingArticle {
id: article.id,
title: article.title,
}),
_ => None
};
let next_result = la_article::Entity::find()
.filter(la_article::Column::IsDelete.eq(0))
.filter(la_article::Column::IsShow.eq(1))
.filter(la_article::Column::Id.gt(article.id))
.order_by_asc(la_article::Column::Sort)
.order_by_asc(la_article::Column::Id)
.one(&self.state.db_conn)
.await;
let next = match next_result {
Ok(Some(article)) => Some(SiblingArticle {
id: article.id,
title: article.title,
}),
_ => None
};
let article_detail = ArticleDetail {
id: article.id,
cid: Option::from(article.cid),
category: None,
title: article.title,
intro: Option::from(article.intro),
summary: article.summary,
image: Option::from(article.image),
content: article.content,
author: article.author,
visit: article.visit,
sort: article.sort,
is_collect: 0,
create_time: "".to_string(),
update_time: None,
prev,
next,
};
let resp = BaseResponse {
code: StatusCode::OK.as_u16(),
message: "成功".to_string(),
data: article_detail,
};
Ok(Json(resp))
}
Ok(None) => {
let err_resp = BaseResponse {
code: StatusCode::MULTIPLE_CHOICES.as_u16(),
message: "文章数据不存在!".to_string(),
data: vec![],
};
Err(Json(err_resp))
}
Err(err) => {
let err_resp = BaseResponse {
code: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: format!("{}", err),
data: vec![],
};
Err(Json(err_resp))
}
}
}
}

1
src/state.rs Normal file
View File

@ -0,0 +1 @@
pub mod app;

5
src/state/app.rs Normal file
View File

@ -0,0 +1,5 @@
use sea_orm::DatabaseConnection;
pub struct AppState {
pub db_conn: DatabaseConnection,
}