first commit
This commit is contained in:
commit
4cf8b7eb08
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
||||
DATABASE_URL=mysql://lyj:1325479Lyj!@47.95.198.7:13206/demo_rs
|
||||
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
/.logger
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal 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
|
15
.idea/chuanyue-service.iml
Normal file
15
.idea/chuanyue-service.iml
Normal file
@ -0,0 +1,15 @@
|
||||
<?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$/api/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/domain/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/library/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/service/src" isTestSource="false" />
|
||||
<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
Normal file
8
.idea/modules.xml
Normal 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/chuanyue-service.iml" filepath="$PROJECT_DIR$/.idea/chuanyue-service.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
3093
Cargo.lock
generated
Normal file
3093
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "chuanyue-service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
members = [".", "api", "domain","library", "service"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
|
||||
|
||||
api = { path = "api" }
|
||||
library = { path = "library" }
|
17
api/Cargo.toml
Normal file
17
api/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "api"
|
||||
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.7.4"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
tower-http = { version = "0.5.2", features = ["trace"] }
|
||||
validator = { version = "0.17", features = ["derive"] }
|
||||
|
||||
library = { path = "../library" }
|
||||
domain = { path = "../domain" }
|
||||
service = { path = "../service" }
|
16
api/src/controller/game_account.rs
Normal file
16
api/src/controller/game_account.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use axum::Json;
|
||||
use validator::Validate;
|
||||
use domain::models::game_account::GameAccountCreate;
|
||||
use library::resp::response::{ResErr, ResOK, ResResult};
|
||||
|
||||
pub async fn create(
|
||||
Json(req): Json<GameAccountCreate>
|
||||
) -> ResResult<ResOK<()>> {
|
||||
if let Err(err) = req.validate() {
|
||||
return Err(ResErr::ErrParams(Some(err.to_string())))
|
||||
}
|
||||
|
||||
service::game_account::create(req).await?;
|
||||
|
||||
Ok(ResOK(None))
|
||||
}
|
1
api/src/controller/mod.rs
Normal file
1
api/src/controller/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod game_account;
|
16
api/src/lib.rs
Normal file
16
api/src/lib.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use library::config;
|
||||
|
||||
mod router;
|
||||
mod controller;
|
||||
|
||||
pub async fn serve() {
|
||||
let addr = format!("0.0.0.0:{}", config!().server.port);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tracing::info!("server listening {}", addr);
|
||||
|
||||
axum::serve(listener, router::init()).await.unwrap();
|
||||
}
|
34
api/src/router.rs
Normal file
34
api/src/router.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use axum::body::Body;
|
||||
use axum::http::Request;
|
||||
use axum::Router;
|
||||
use axum::routing::{get, post};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use crate::controller;
|
||||
|
||||
pub(crate) fn init() -> Router {
|
||||
let open = Router::new().route("/", get(|| async { "hello" }));
|
||||
|
||||
let auth = Router::new()
|
||||
.route("/game_accounts", post(controller::game_account::create));
|
||||
|
||||
Router::new()
|
||||
.nest("/", open)
|
||||
.nest("/v1", auth)
|
||||
.layer(axum::middleware::from_fn(library::middleware::req_log::handle))
|
||||
.layer(axum::middleware::from_fn(library::middleware::cors::handle))
|
||||
.layer(
|
||||
TraceLayer::new_for_http().make_span_with(|request: &Request<Body>| {
|
||||
let req_id = match request
|
||||
.headers()
|
||||
.get("x-request-id")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
Some(v) => v.to_string(),
|
||||
None => String::from("unknown"),
|
||||
};
|
||||
|
||||
tracing::error_span!("request_id", id = req_id)
|
||||
}),
|
||||
)
|
||||
.layer(axum::middleware::from_fn(library::middleware::req_id::handle))
|
||||
}
|
13
app.toml
Normal file
13
app.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[server]
|
||||
name = "chuanyue-server"
|
||||
port = 8080
|
||||
debug = true
|
||||
|
||||
[logger]
|
||||
dir = ".logger"
|
||||
prefix = "tower_defense_server"
|
||||
level = "DEBUG"
|
||||
|
||||
[database]
|
||||
url = "mysql://lyj:1325479Lyj!@47.95.198.7:13206/demo_rs"
|
||||
options = { min_conns = 10, max_conns = 20, conn_timeout = 30, idle_timeout = 300, max_lifetime = 60, sql_logging = true }
|
11
domain/Cargo.toml
Normal file
11
domain/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "domain"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
sea-orm = { version = "0.12.14", features = ["sqlx-mysql", "runtime-tokio-rustls", "macros", "debug-print"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
validator = { version = "0.17", features = ["derive"] }
|
11
domain/src/entities/account.rs
Normal file
11
domain/src/entities/account.rs
Normal file
@ -0,0 +1,11 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.14
|
||||
|
||||
|
||||
|
||||
use sea_orm :: entity :: prelude :: * ;
|
||||
|
||||
# [derive (Clone , Debug , PartialEq , DeriveEntityModel , Eq)] # [sea_orm (table_name = "account")] pub struct Model { # [sea_orm (primary_key)] pub id : u64 , # [sea_orm (unique)] pub username : String , pub password : String , pub salt : String , pub role : i8 , pub realname : String , pub login_at : i64 , pub login_token : String , pub created_at : i64 , pub updated_at : i64 , }
|
||||
|
||||
# [derive (Copy , Clone , Debug , EnumIter , DeriveRelation)] pub enum Relation { }
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel { }
|
11
domain/src/entities/game_account.rs
Normal file
11
domain/src/entities/game_account.rs
Normal file
@ -0,0 +1,11 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.14
|
||||
|
||||
|
||||
|
||||
use sea_orm :: entity :: prelude :: * ;
|
||||
|
||||
# [derive (Clone , Debug , PartialEq , DeriveEntityModel , Eq)] # [sea_orm (table_name = "game_account")] pub struct Model { # [sea_orm (primary_key)] pub id : u64 , pub username : String , pub email : Option < String > , # [sea_orm (unique)] pub platform_id : String , pub user_type : String , pub country_code : String , pub created_at : DateTime , }
|
||||
|
||||
# [derive (Copy , Clone , Debug , EnumIter , DeriveRelation)] pub enum Relation { }
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel { }
|
6
domain/src/entities/mod.rs
Normal file
6
domain/src/entities/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.14
|
||||
|
||||
pub mod prelude ;
|
||||
|
||||
pub mod account ;
|
||||
pub mod game_account ;
|
4
domain/src/entities/prelude.rs
Normal file
4
domain/src/entities/prelude.rs
Normal file
@ -0,0 +1,4 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.14
|
||||
|
||||
pub use super :: account :: Entity as Account ;
|
||||
pub use super :: game_account :: Entity as GameAccount ;
|
2
domain/src/lib.rs
Normal file
2
domain/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod entities;
|
||||
pub mod models;
|
16
domain/src/models/game_account.rs
Normal file
16
domain/src/models/game_account.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Debug, Validate, Deserialize, Serialize)]
|
||||
pub struct GameAccountCreate {
|
||||
#[validate(length(min = 1, message = "用户名称不能为空"))]
|
||||
pub username: String,
|
||||
#[validate(length(min = 1, message = "电子邮箱不能为空"))]
|
||||
pub email: String,
|
||||
#[validate(length(min = 1, message = "平台ID不能为空"))]
|
||||
pub platform_id: String,
|
||||
#[validate(length(min = 1, message = "用户类型不能为空"))]
|
||||
pub user_type: String,
|
||||
#[validate(length(min = 1, message = "用户所属区域不能为空"))]
|
||||
pub country_code: String
|
||||
}
|
1
domain/src/models/mod.rs
Normal file
1
domain/src/models/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod game_account;
|
22
library/Cargo.toml
Normal file
22
library/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "library"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
toml = "0.8.10"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
once_cell = "1.19.0"
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["json"] }
|
||||
chrono = "0.4.35"
|
||||
sea-orm = { version = "0.12.14", features = ["sqlx-mysql", "runtime-tokio-rustls", "macros", "debug-print"] }
|
||||
axum = "0.7.4"
|
||||
axum-extra = "0.9.2"
|
||||
thiserror = "1.0.57"
|
||||
ulid = "1.1.2"
|
||||
serde_json = "1.0.114"
|
||||
http = "1.1.0"
|
||||
http-body = "1.0.0"
|
||||
http-body-util = "0.1.0"
|
70
library/src/core/config.rs
Normal file
70
library/src/core/config.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::sync::OnceLock;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub server: Server,
|
||||
pub logger: Logger,
|
||||
pub database: Database
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Server {
|
||||
pub name: String,
|
||||
pub port: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Database {
|
||||
pub url: String,
|
||||
pub options: Options
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Options {
|
||||
pub min_conns: u32,
|
||||
pub max_conns: u32,
|
||||
pub conn_timeout: u64,
|
||||
pub idle_timeout: u64,
|
||||
pub max_lifetime: u64,
|
||||
pub sql_logging: bool
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Logger {
|
||||
pub dir: String,
|
||||
pub prefix: String,
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! config {
|
||||
() => {
|
||||
library::core::config::config()
|
||||
};
|
||||
}
|
||||
|
||||
const CFG_FILE: &str = "app.toml";
|
||||
|
||||
static CFG: OnceLock<Config> = OnceLock::new();
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let mut file = match File::open(CFG_FILE) {
|
||||
Ok(f) => f,
|
||||
Err(err) => panic!("读取配置文件失败:{}", err)
|
||||
};
|
||||
let mut config_str = String::new();
|
||||
match file.read_to_string(&mut config_str) {
|
||||
Ok(s) => s,
|
||||
Err(err) => panic!("读取配置文件内容失败:{}", err)
|
||||
};
|
||||
toml::from_str(&*config_str).expect("格式化配置数据失败")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config() -> &'static Config {
|
||||
CFG.get_or_init(|| Config::default())
|
||||
}
|
40
library/src/core/db.rs
Normal file
40
library/src/core/db.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
|
||||
use crate::core::config::Config;
|
||||
|
||||
static DB: OnceLock<DatabaseConnection> = OnceLock::new();
|
||||
|
||||
pub async fn init_database(config: &Config) {
|
||||
let db_cfg = &config.database;
|
||||
let mut conn_option = ConnectOptions::new(&db_cfg.url);
|
||||
|
||||
conn_option.min_connections(db_cfg.options.min_conns)
|
||||
.max_connections(db_cfg.options.max_conns)
|
||||
.connect_timeout(Duration::from_secs(db_cfg.options.conn_timeout))
|
||||
.idle_timeout(Duration::from_secs(db_cfg.options.idle_timeout))
|
||||
.max_lifetime(Duration::from_secs(db_cfg.options.max_lifetime))
|
||||
.sqlx_logging(db_cfg.options.sql_logging);
|
||||
|
||||
let conn = Database::connect(conn_option)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("数据库连接失败:{}", e));
|
||||
|
||||
let _ = conn
|
||||
.ping()
|
||||
.await
|
||||
.is_err_and(|e| panic!("数据库连接失败:{}", e));
|
||||
|
||||
let _ = DB.set(conn);
|
||||
}
|
||||
|
||||
pub fn conn() -> &'static DatabaseConnection {
|
||||
DB.get().unwrap_or_else(|| panic!("数据库连接未初始化"))
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! db {
|
||||
() => {
|
||||
library::core::db::conn()
|
||||
};
|
||||
}
|
58
library/src/core/logger.rs
Normal file
58
library/src/core/logger.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use chrono::Local;
|
||||
use tracing::Level;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::fmt;
|
||||
use tracing_subscriber::fmt::format::Writer;
|
||||
use tracing_subscriber::fmt::time::FormatTime;
|
||||
use tracing_subscriber::fmt::writer::MakeWriterExt;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use crate::core::config::Config;
|
||||
|
||||
// 格式化日志的输出时间格式
|
||||
struct LocalTimer;
|
||||
|
||||
impl FormatTime for LocalTimer {
|
||||
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
|
||||
write!(w, "{}", Local::now().format("%Y-%m-%d %H:%M:%S"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_log(config: &Config) -> (WorkerGuard, WorkerGuard) {
|
||||
let logger_cfg = config.logger.clone();
|
||||
let (stdout_tracing_appender, std_guard) = tracing_appender::non_blocking(std::io::stdout());
|
||||
let (file_tracing_appender, file_guard) = tracing_appender::non_blocking(tracing_appender::rolling::daily(logger_cfg.dir, logger_cfg.prefix));
|
||||
|
||||
// 初始化并设置日志格式(定制和筛选日志)
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
fmt::layer()
|
||||
.with_writer(file_tracing_appender.with_max_level(package_level(&logger_cfg.level)))
|
||||
.with_ansi(true) // 关掉ansi的颜色输出功能
|
||||
.with_timer(LocalTimer)
|
||||
)
|
||||
.with(
|
||||
fmt::layer()
|
||||
.with_writer(stdout_tracing_appender.with_max_level(package_level(&logger_cfg.level)))
|
||||
// .with_file(true)
|
||||
.with_line_number(true) // 写入标准输出
|
||||
.with_ansi(false) // 关掉ansi的颜色输出功能
|
||||
.with_timer(LocalTimer)
|
||||
.json()
|
||||
.flatten_event(true)
|
||||
)
|
||||
.init(); // 初始化并将SubScriber设置为全局SubScriber
|
||||
|
||||
(std_guard, file_guard)
|
||||
}
|
||||
|
||||
fn package_level(level: &String) -> Level {
|
||||
match level.to_uppercase().as_str() {
|
||||
"TRACE" => Level::TRACE,
|
||||
"DEBUG" => Level::DEBUG,
|
||||
"INFO" => Level::INFO,
|
||||
"WARN" => Level::WARN,
|
||||
"ERROR" => Level::ERROR,
|
||||
_ => Level::INFO,
|
||||
}
|
||||
}
|
3
library/src/core/mod.rs
Normal file
3
library/src/core/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod config;
|
||||
pub mod logger;
|
||||
pub mod db;
|
5
library/src/lib.rs
Normal file
5
library/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
||||
extern crate self as library;
|
||||
|
||||
pub mod core;
|
||||
pub mod resp;
|
||||
pub mod middleware;
|
34
library/src/middleware/cors.rs
Normal file
34
library/src/middleware/cors.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use axum::{
|
||||
extract::Request,
|
||||
http::{HeaderMap, HeaderValue, Method, StatusCode},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
http::header::{ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_CREDENTIALS,
|
||||
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS},
|
||||
};
|
||||
|
||||
pub async fn handle(request: Request, next: Next) -> Response {
|
||||
let mut cors_headers = HeaderMap::new();
|
||||
|
||||
cors_headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
|
||||
cors_headers.insert(
|
||||
ACCESS_CONTROL_ALLOW_CREDENTIALS,
|
||||
HeaderValue::from_static("true"),
|
||||
);
|
||||
cors_headers.insert(
|
||||
ACCESS_CONTROL_ALLOW_METHODS,
|
||||
HeaderValue::from_static("GET, POST, PUT, DELETE, OPTIONS"),
|
||||
);
|
||||
cors_headers.insert(
|
||||
ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
HeaderValue::from_static("content-type, authorization, withCredentials"),
|
||||
);
|
||||
|
||||
if request.method() == Method::OPTIONS {
|
||||
return (StatusCode::NO_CONTENT, cors_headers).into_response();
|
||||
}
|
||||
|
||||
let response = next.run(request).await;
|
||||
|
||||
(cors_headers, response).into_response()
|
||||
}
|
3
library/src/middleware/mod.rs
Normal file
3
library/src/middleware/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod req_id;
|
||||
pub mod req_log;
|
||||
pub mod cors;
|
22
library/src/middleware/req_id.rs
Normal file
22
library/src/middleware/req_id.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use axum::extract::Request;
|
||||
use axum::http::{HeaderName, HeaderValue};
|
||||
use axum::middleware::Next;
|
||||
use axum::response::Response;
|
||||
use ulid::Ulid;
|
||||
|
||||
pub async fn handle(mut request: Request, next: Next) -> Response {
|
||||
let req_id = HeaderValue::from_str(&Ulid::new().to_string()).unwrap_or_else(|err| {
|
||||
tracing::error!("error is {}", err);
|
||||
HeaderValue::from_static("unknown")
|
||||
});
|
||||
|
||||
request.headers_mut()
|
||||
.insert(HeaderName::from_static("x-request-id"), req_id.to_owned());
|
||||
|
||||
let mut response = next.run(request).await;
|
||||
|
||||
response.headers_mut()
|
||||
.insert(HeaderName::from_static("x-request-id"), req_id);
|
||||
|
||||
response
|
||||
}
|
106
library/src/middleware/req_log.rs
Normal file
106
library/src/middleware/req_log.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use std::collections::HashMap;
|
||||
use axum::body::Body;
|
||||
use axum::extract::Request;
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use crate::resp::response::ResErr;
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
pub async fn handle(request: Request, next: Next) -> Response {
|
||||
let enter_time = chrono::Local::now();
|
||||
let req_method = request.method().to_string();
|
||||
let req_uri = request.uri().to_string();
|
||||
let req_header = header_to_string(request.headers());
|
||||
// let identity = match request.extensions().get::<Identity>() {
|
||||
// Some(v) => v.to_string(),
|
||||
// None => String::from("<none>"),
|
||||
// };
|
||||
|
||||
let (response, body) = match drain_body(request, next).await {
|
||||
Err(err) => return err.into_response(),
|
||||
Ok(v) => v,
|
||||
};
|
||||
|
||||
let duration = chrono::Local::now()
|
||||
.signed_duration_since(enter_time)
|
||||
.to_string();
|
||||
|
||||
tracing::info!(
|
||||
method = req_method,
|
||||
uri = req_uri,
|
||||
headers = req_header,
|
||||
// identity = identity,
|
||||
body = body,
|
||||
duration = duration,
|
||||
"请求记录"
|
||||
);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
fn header_to_string(h: &HeaderMap) -> String {
|
||||
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
for k in h.keys() {
|
||||
let mut vals: Vec<String> = Vec::new();
|
||||
|
||||
for v in h.get_all(k) {
|
||||
if let Ok(s) = v.to_str() {
|
||||
vals.push(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
map.insert(k.to_string(), vals);
|
||||
}
|
||||
|
||||
match serde_json::to_string(&map) {
|
||||
Ok(v) => v,
|
||||
Err(_) => String::from("<none>"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn drain_body(request: Request, next: Next) -> Result<(Response, Option<String>), ResErr> {
|
||||
let ok = match request
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
Some(v) => {
|
||||
if v.starts_with("application/json")
|
||||
|| v.starts_with("application/x-www-form-urlencoded")
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
|
||||
if !ok {
|
||||
return Ok((next.run(request).await, None));
|
||||
}
|
||||
|
||||
let (parts, body) = request.into_parts();
|
||||
|
||||
// this wont work if the body is an long running stream
|
||||
let bytes = match body.collect().await {
|
||||
Ok(v) => v.to_bytes(),
|
||||
Err(err) => {
|
||||
tracing::error!(error = ?err, "err parse request body");
|
||||
return Err(ResErr::ErrSystem(None));
|
||||
}
|
||||
};
|
||||
|
||||
let body = std::str::from_utf8(&bytes)
|
||||
.and_then(|s| Ok(s.to_string()))
|
||||
.ok();
|
||||
|
||||
let response = next
|
||||
.run(Request::from_parts(parts, Body::from(bytes)))
|
||||
.await;
|
||||
|
||||
Ok((response, body))
|
||||
}
|
3
library/src/resp/mod.rs
Normal file
3
library/src/resp/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod rejection;
|
||||
pub mod response;
|
||||
pub mod status;
|
34
library/src/resp/rejection.rs
Normal file
34
library/src/resp/rejection.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use axum::{
|
||||
extract::rejection::JsonRejection,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::WithRejection;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::response::ResErr;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MyRejection {
|
||||
// The `#[from]` attribute generates `From<JsonRejection> for MyRejection`
|
||||
// implementation. See `thiserror` docs for more information
|
||||
#[error(transparent)]
|
||||
JSONExtractor(#[from] JsonRejection),
|
||||
}
|
||||
|
||||
// We implement `IntoResponse` so MyRejection can be used as a response
|
||||
impl IntoResponse for MyRejection {
|
||||
fn into_response(self) -> Response {
|
||||
let err = match self {
|
||||
MyRejection::JSONExtractor(x) => match x {
|
||||
JsonRejection::JsonDataError(e) => ResErr::ErrData(Some(e.body_text())),
|
||||
JsonRejection::JsonSyntaxError(e) => ResErr::ErrData(Some(e.body_text())),
|
||||
JsonRejection::MissingJsonContentType(e) => ResErr::ErrData(Some(e.body_text())),
|
||||
_ => ResErr::ErrSystem(None),
|
||||
},
|
||||
};
|
||||
|
||||
err.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub type IRejection<T> = WithRejection<T, MyRejection>;
|
104
library/src/resp/response.rs
Normal file
104
library/src/resp/response.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::status::Status;
|
||||
|
||||
pub struct ResOK<T>(pub Option<T>)
|
||||
where
|
||||
T: Serialize;
|
||||
|
||||
impl<T> IntoResponse for ResOK<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
let ResOK(data) = self;
|
||||
let status = Status::OK(data);
|
||||
|
||||
Json(status.to_reply()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ResErr {
|
||||
Error(i32, String),
|
||||
ErrParams(Option<String>),
|
||||
ErrAuth(Option<String>),
|
||||
ErrPerm(Option<String>),
|
||||
ErrNotFound(Option<String>),
|
||||
ErrSystem(Option<String>),
|
||||
ErrData(Option<String>),
|
||||
ErrService(Option<String>),
|
||||
}
|
||||
|
||||
use ResErr::*;
|
||||
|
||||
impl IntoResponse for ResErr {
|
||||
fn into_response(self) -> Response {
|
||||
let status = match self {
|
||||
Error(code, msg) => Status::<()>::Err(code, msg),
|
||||
ErrParams(msg) => {
|
||||
let code = 10000;
|
||||
|
||||
match msg {
|
||||
Some(v) => Status::<()>::Err(code, v),
|
||||
None => Status::<()>::Err(code, String::from("参数错误")),
|
||||
}
|
||||
}
|
||||
ErrAuth(msg) => {
|
||||
let code = 20000;
|
||||
|
||||
match msg {
|
||||
Some(v) => Status::<()>::Err(code, v),
|
||||
None => Status::<()>::Err(code, String::from("未授权,请先登录")),
|
||||
}
|
||||
}
|
||||
ErrPerm(msg) => {
|
||||
let code = 30000;
|
||||
|
||||
match msg {
|
||||
Some(v) => Status::<()>::Err(code, v),
|
||||
None => Status::<()>::Err(code, String::from("权限不足")),
|
||||
}
|
||||
}
|
||||
ErrNotFound(msg) => {
|
||||
let code = 40000;
|
||||
|
||||
match msg {
|
||||
Some(v) => Status::<()>::Err(code, v),
|
||||
None => Status::<()>::Err(code, String::from("数据不存在")),
|
||||
}
|
||||
}
|
||||
ErrSystem(msg) => {
|
||||
let code = 50000;
|
||||
|
||||
match msg {
|
||||
Some(v) => Status::<()>::Err(code, v),
|
||||
None => Status::<()>::Err(code, String::from("内部服务器错误,请稍后重试")),
|
||||
}
|
||||
}
|
||||
ErrData(msg) => {
|
||||
let code = 60000;
|
||||
|
||||
match msg {
|
||||
Some(v) => Status::<()>::Err(code, v),
|
||||
None => Status::<()>::Err(code, String::from("数据异常")),
|
||||
}
|
||||
}
|
||||
ErrService(msg) => {
|
||||
let code = 70000;
|
||||
|
||||
match msg {
|
||||
Some(v) => Status::<()>::Err(code, v),
|
||||
None => Status::<()>::Err(code, String::from("服务异常")),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Json(status.to_reply()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub type ResResult<T> = std::result::Result<T, ResErr>;
|
48
library/src/resp/status.rs
Normal file
48
library/src/resp/status.rs
Normal file
@ -0,0 +1,48 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Reply<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
pub code: i32,
|
||||
pub err: bool,
|
||||
pub msg: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
pub enum Status<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
OK(Option<T>),
|
||||
Err(i32, String),
|
||||
}
|
||||
|
||||
impl<T> Status<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
pub fn to_reply(self) -> Reply<T> {
|
||||
let mut resp = Reply {
|
||||
code: 0,
|
||||
err: false,
|
||||
msg: String::from("OK"),
|
||||
data: None,
|
||||
};
|
||||
|
||||
match self {
|
||||
Status::OK(data) => {
|
||||
resp.data = data;
|
||||
}
|
||||
Status::Err(code, msg) => {
|
||||
resp.code = code;
|
||||
resp.err = true;
|
||||
resp.msg = msg;
|
||||
}
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
14
service/Cargo.toml
Normal file
14
service/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.40"
|
||||
chrono = "0.4.35"
|
||||
sea-orm = { version = "0.12.14", features = ["sqlx-mysql", "runtime-tokio-rustls", "macros", "debug-print"] }
|
||||
|
||||
library = { path = "../library" }
|
||||
domain = { path = "../domain" }
|
43
service/src/game_account.rs
Normal file
43
service/src/game_account.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
|
||||
use domain::entities::game_account;
|
||||
use domain::entities::prelude::GameAccount;
|
||||
use domain::models::game_account::GameAccountCreate;
|
||||
use library::db;
|
||||
use library::resp::response::ResErr::{ErrPerm, ErrSystem};
|
||||
use library::resp::response::{ResOK, ResResult};
|
||||
|
||||
pub async fn create(req: GameAccountCreate) -> ResResult<ResOK<()>> {
|
||||
match GameAccount::find()
|
||||
.filter(game_account::Column::PlatformId.eq(req.platform_id.clone()))
|
||||
.count(db!())
|
||||
.await {
|
||||
Err(err) => {
|
||||
tracing::error!(error = ?err, "err find game account");
|
||||
return Err(ErrSystem(None));
|
||||
}
|
||||
Ok(v) => {
|
||||
if v > 0 {
|
||||
return Err(ErrPerm(Some("用户已存在".to_string())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let now = chrono::Local::now().naive_local();
|
||||
|
||||
let model = game_account::ActiveModel {
|
||||
username: Set(req.username),
|
||||
email: Set(Option::from(req.email)),
|
||||
platform_id: Set(req.platform_id),
|
||||
user_type: Set(req.user_type),
|
||||
country_code: Set(req.country_code),
|
||||
created_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Err(err) = GameAccount::insert(model).exec(db!()).await {
|
||||
tracing::error!(error = ?err, "err insert game account");
|
||||
return Err(ErrSystem(None));
|
||||
}
|
||||
|
||||
Ok(ResOK(None))
|
||||
}
|
1
service/src/lib.rs
Normal file
1
service/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod game_account;
|
9
src/main.rs
Normal file
9
src/main.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use library::config;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let (_std_guard, _file_guard) = library::core::logger::init_log(config!());
|
||||
library::core::db::init_database(config!()).await;
|
||||
|
||||
api::serve().await;
|
||||
}
|
Loading…
Reference in New Issue
Block a user