first commit

This commit is contained in:
李运家 2024-03-10 09:46:37 +08:00
commit 4cf8b7eb08
40 changed files with 3928 additions and 0 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
DATABASE_URL=mysql://lyj:1325479Lyj!@47.95.198.7:13206/demo_rs

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/.logger

8
.idea/.gitignore 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

View 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
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/chuanyue-service.iml" filepath="$PROJECT_DIR$/.idea/chuanyue-service.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml 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>

3093
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View 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
View 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" }

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

View File

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

16
api/src/lib.rs Normal file
View 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
View 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
View 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
View 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"] }

View 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 { }

View 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 { }

View 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 ;

View 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
View File

@ -0,0 +1,2 @@
pub mod entities;
pub mod models;

View 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
View File

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

22
library/Cargo.toml Normal file
View 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"

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

View 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
View File

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

5
library/src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
extern crate self as library;
pub mod core;
pub mod resp;
pub mod middleware;

View 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()
}

View File

@ -0,0 +1,3 @@
pub mod req_id;
pub mod req_log;
pub mod cors;

View 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
}

View 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
View File

@ -0,0 +1,3 @@
pub mod rejection;
pub mod response;
pub mod status;

View 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>;

View 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>;

View 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
View 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" }

View 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
View File

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

9
src/main.rs Normal file
View 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;
}