update
This commit is contained in:
4
.env
4
.env
@@ -1,5 +1,5 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL=mysql://root:114514@localhost:3306/astarcupLocal
|
||||
DATABASE_URL=mysql://root:114514@localhost:3306/astarcupDev
|
||||
|
||||
# OAuth Configuration
|
||||
OSU_CLIENT_ID =
|
||||
@@ -12,7 +12,7 @@ JWT_EXPIRY_HOURS=24
|
||||
|
||||
# Server Configuration
|
||||
PORT=3001
|
||||
HOST=0.0.0.0
|
||||
HOST=localhost
|
||||
LOG_LEVEL=info
|
||||
|
||||
# CORS Configuration
|
||||
|
||||
68
Cargo.lock
generated
68
Cargo.lock
generated
@@ -105,6 +105,7 @@ dependencies = [
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
"config",
|
||||
"decimal",
|
||||
"dotenv",
|
||||
"futures",
|
||||
"jsonwebtoken",
|
||||
@@ -121,6 +122,7 @@ dependencies = [
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"urlencoding",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
@@ -328,6 +330,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
@@ -722,6 +730,20 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decimal"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a8ab77e91baeb15034c3be91e87bff4665c9036216148e4996d9a9f5792114d"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cc",
|
||||
"libc",
|
||||
"ord_subset",
|
||||
"rustc-serialize",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -1669,7 +1691,7 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
"redox_syscall 0.7.0",
|
||||
]
|
||||
@@ -1911,7 +1933,7 @@ version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -1955,6 +1977,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ord_subset"
|
||||
version = "3.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdcf5505c0f054ce51fa0fa74142738930a45d5ac1faacae4dd4e2f54afe00fa"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
@@ -2452,7 +2480,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2461,7 +2489,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2591,7 +2619,7 @@ version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -2685,6 +2713,12 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-serialize"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -2700,7 +2734,7 @@ version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -2919,7 +2953,7 @@ version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -2932,7 +2966,7 @@ version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -3267,7 +3301,7 @@ dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"bigdecimal",
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -3314,7 +3348,7 @@ dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"bigdecimal",
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -3464,7 +3498,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
@@ -3745,7 +3779,7 @@ version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
@@ -3919,6 +3953,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -4163,7 +4203,7 @@ version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
@@ -4642,7 +4682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
|
||||
@@ -51,6 +51,8 @@ futures = "0.3"
|
||||
axum-extra = { version = "0.12.5", features = ["typed-header"] }
|
||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||
decimal = "2.1.0"
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing
|
||||
|
||||
219
src/api/auth.rs
219
src/api/auth.rs
@@ -1,6 +1,25 @@
|
||||
use axum::{Router, routing::get};
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn routes() -> Router {
|
||||
use crate::{
|
||||
dto::{
|
||||
ApiResponse,
|
||||
user::{CreateUserRequest, OsuUserData},
|
||||
},
|
||||
error::Result,
|
||||
service::{ServiceState, user::UserService},
|
||||
};
|
||||
|
||||
pub fn routes() -> Router<ServiceState> {
|
||||
Router::new()
|
||||
.route("/osu", get(osu_auth))
|
||||
.route("/callback/osu", get(osu_callback))
|
||||
@@ -8,18 +27,198 @@ pub fn routes() -> Router {
|
||||
.route("/logout", get(logout))
|
||||
}
|
||||
|
||||
async fn osu_auth() -> &'static str {
|
||||
"osu! OAuth 认证"
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OsuCallbackQuery {
|
||||
code: String,
|
||||
state: String,
|
||||
}
|
||||
|
||||
async fn osu_callback() -> &'static str {
|
||||
"osu! OAuth 回调"
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OsuTokenResponse {
|
||||
access_token: String,
|
||||
token_type: String,
|
||||
expires_in: u32,
|
||||
refresh_token: String,
|
||||
scope: String,
|
||||
}
|
||||
|
||||
async fn refresh_token() -> &'static str {
|
||||
"刷新Token"
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct AuthResponse {
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
user: crate::dto::user::UserResponse,
|
||||
}
|
||||
|
||||
async fn logout() -> &'static str {
|
||||
"登出"
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/auth/osu",
|
||||
responses(
|
||||
(status = 302, description = "重定向到OSU授权页面"),
|
||||
),
|
||||
tag = "auth"
|
||||
)]
|
||||
async fn osu_auth() -> Redirect {
|
||||
let client_id = env::var("OSU_CLIENT_ID").expect("OSU_CLIENT_ID not set");
|
||||
let redirect_uri = env::var("OSU_REDIRECT_URI").expect("OSU_REDIRECT_URI not set");
|
||||
let state = Uuid::new_v4().to_string();
|
||||
|
||||
let auth_url = format!(
|
||||
"https://osu.ppy.sh/oauth/authorize?client_id={}&redirect_uri={}&response_type=code&scope=identify%20public&state={}",
|
||||
client_id,
|
||||
urlencoding::encode(&redirect_uri),
|
||||
state
|
||||
);
|
||||
|
||||
Redirect::to(&auth_url)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/auth/callback/osu",
|
||||
params(
|
||||
("code" = String, Query, description = "OSU OAuth回调代码"),
|
||||
("state" = String, Query, description = "状态参数"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "OAuth回调成功"),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 500, description = "内部服务器错误"),
|
||||
),
|
||||
tag = "auth"
|
||||
)]
|
||||
async fn osu_callback(
|
||||
Query(params): Query<OsuCallbackQuery>,
|
||||
State(state): State<ServiceState>,
|
||||
) -> Result<Json<ApiResponse<AuthResponse>>> {
|
||||
let client_id = env::var("OSU_CLIENT_ID").expect("OSU_CLIENT_ID not set");
|
||||
let client_secret = env::var("OSU_CLIENT_SECRET").expect("OSU_CLIENT_SECRET not set");
|
||||
let redirect_uri = env::var("OSU_REDIRECT_URI").expect("OSU_REDIRECT_URI not set");
|
||||
|
||||
// 获取访问令牌
|
||||
let client = Client::new();
|
||||
let token_response = client
|
||||
.post("https://osu.ppy.sh/oauth/token")
|
||||
.json(&serde_json::json!({
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"code": params.code,
|
||||
"grant_type": "authorization_code",
|
||||
"redirect_uri": redirect_uri
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::error::AppError::ExternalApiError(format!("Failed to get token: {}", e))
|
||||
})?
|
||||
.json::<OsuTokenResponse>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::error::AppError::ExternalApiError(format!(
|
||||
"Failed to parse token response: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
// 获取用户信息
|
||||
let user_data = client
|
||||
.get("https://osu.ppy.sh/api/v2/me")
|
||||
.header(
|
||||
"Authorization",
|
||||
format!("Bearer {}", token_response.access_token),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::error::AppError::ExternalApiError(format!("Failed to get user info: {}", e))
|
||||
})?
|
||||
.json::<OsuUserData>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::error::AppError::ExternalApiError(format!("Failed to parse user info: {}", e))
|
||||
})?;
|
||||
|
||||
// 处理用户数据
|
||||
let user_service = UserService::new(state.db);
|
||||
let existing_user = user_service.find_by_osu_id(&user_data.id).await?;
|
||||
|
||||
let user = match existing_user {
|
||||
Some(mut existing) => {
|
||||
// 更新现有用户
|
||||
let update_request = crate::dto::user::UpdateUserRequest {
|
||||
username: Some(user_data.username),
|
||||
avatar_url: user_data.avatar_url,
|
||||
pp: user_data.statistics.as_ref().and_then(|s| s.pp),
|
||||
global_rank: user_data.statistics.as_ref().and_then(|s| s.global_rank),
|
||||
country: user_data.country.as_ref().map(|c| c.code.clone()),
|
||||
country_rank: user_data.statistics.as_ref().and_then(|s| s.country_rank),
|
||||
approved: None,
|
||||
season: None,
|
||||
};
|
||||
user_service.update(existing.id, update_request).await?
|
||||
}
|
||||
None => {
|
||||
// 创建新用户
|
||||
let create_request = CreateUserRequest {
|
||||
username: user_data.username,
|
||||
avatar_url: user_data.avatar_url,
|
||||
pp: user_data.statistics.as_ref().and_then(|s| s.pp),
|
||||
global_rank: user_data.statistics.as_ref().and_then(|s| s.global_rank),
|
||||
country: user_data.country.as_ref().map(|c| c.code.clone()),
|
||||
country_rank: user_data.statistics.as_ref().and_then(|s| s.country_rank),
|
||||
};
|
||||
user_service.create(user_data.id, create_request).await?
|
||||
}
|
||||
};
|
||||
|
||||
// 生成JWT令牌
|
||||
let osu_id = user.osu_id.clone();
|
||||
let user_group_str = match user.user_group {
|
||||
crate::entity::sea_orm_active_enums::UserGroup::Player => "player",
|
||||
crate::entity::sea_orm_active_enums::UserGroup::Admin => "admin",
|
||||
};
|
||||
let jwt_token =
|
||||
crate::utils::jwt::generate_token(osu_id, user_group_str.to_string()).map_err(|e| {
|
||||
crate::error::AppError::InternalError(format!("Failed to generate token: {}", e))
|
||||
})?;
|
||||
|
||||
let auth_response = AuthResponse {
|
||||
access_token: jwt_token,
|
||||
refresh_token: token_response.refresh_token,
|
||||
user: crate::dto::user::UserResponse::from(user),
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(auth_response)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/auth/refresh",
|
||||
responses(
|
||||
(status = 200, description = "令牌刷新成功"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 500, description = "内部服务器错误"),
|
||||
),
|
||||
tag = "auth"
|
||||
)]
|
||||
async fn refresh_token() -> Result<Json<ApiResponse<AuthResponse>>> {
|
||||
// 实现令牌刷新逻辑
|
||||
// 这里需要从请求中获取refresh_token,然后调用OSU API刷新令牌
|
||||
// 暂时返回未实现
|
||||
Err(crate::error::AppError::NotImplemented(
|
||||
"Token refresh not implemented".into(),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/auth/logout",
|
||||
responses(
|
||||
(status = 200, description = "登出成功"),
|
||||
),
|
||||
tag = "auth"
|
||||
)]
|
||||
async fn logout() -> Result<Json<ApiResponse<()>>> {
|
||||
// 实现登出逻辑
|
||||
// 这里需要清除客户端的令牌
|
||||
Ok(Json(ApiResponse::success_with_message((), "登出成功")))
|
||||
}
|
||||
|
||||
@@ -14,26 +14,36 @@ pub fn configure_routes() -> Router<ServiceState> {
|
||||
.route("/health", get(|| async { "OK" }))
|
||||
.nest("/api/auth", auth::routes())
|
||||
.nest("/api/users", user::routes())
|
||||
.with_state(())
|
||||
}
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
auth::osu_auth,
|
||||
auth::osu_callback,
|
||||
auth::refresh_token,
|
||||
auth::logout,
|
||||
user::get_users,
|
||||
user::get_user,
|
||||
user::update_user,
|
||||
user::update_user_group,
|
||||
user::delete_user,
|
||||
user::update_registration_status,
|
||||
user::update_season,
|
||||
user::update_approved,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
crate::dto::user::UserResponse,
|
||||
crate::dto::user::UpdateUserRequest,
|
||||
crate::dto::user::UpdateUserGroupRequest,
|
||||
crate::dto::user::UpdateRegistrationStatusRequest,
|
||||
crate::dto::user::UpdateSeasonRequest,
|
||||
crate::dto::user::UpdateApprovedRequest,
|
||||
crate::dto::user::UserQueryParams,
|
||||
crate::dto::ApiResponse<crate::dto::user::UserResponse>,
|
||||
crate::dto::PaginatedResponse<crate::dto::user::UserResponse>,
|
||||
crate::dto::ApiResponseSchema<crate::dto::user::UserResponse>,
|
||||
crate::dto::ApiResponseSchema<crate::api::auth::AuthResponse>,
|
||||
crate::dto::PaginatedResponseSchema<crate::dto::user::UserResponse>,
|
||||
user::PaginatedUserResponse,
|
||||
)
|
||||
),
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
dto::{
|
||||
ApiResponse, PaginatedResponse,
|
||||
user::{
|
||||
CreateUserRequest, UpdateUserGroupRequest, UpdateUserRequest, UserQueryParams,
|
||||
CreateUserRequest, UpdateApprovedRequest, UpdateRegistrationStatusRequest, UpdateSeasonRequest, UpdateUserGroupRequest, UpdateUserRequest, UserQueryParams,
|
||||
UserResponse,
|
||||
},
|
||||
},
|
||||
@@ -25,6 +25,9 @@ pub fn routes() -> Router<ServiceState> {
|
||||
.route("/{id}", put(update_user))
|
||||
.route("/{id}", delete(delete_user))
|
||||
.route("/{id}/user-group", put(update_user_group))
|
||||
.route("/{id}/registration-status", put(update_registration_status))
|
||||
.route("/{id}/season", put(update_season))
|
||||
.route("/{id}/approve", put(update_approved))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -173,6 +176,88 @@ async fn delete_user(
|
||||
Ok(Json(ApiResponse::success_with_message((), "用户删除成功")))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}/registration-status",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
),
|
||||
request_body = UpdateRegistrationStatusRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新注册状态成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn update_registration_status(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Json(data): Json<UpdateRegistrationStatusRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.update_registration_status(id, data).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}/season",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
),
|
||||
request_body = UpdateSeasonRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新赛季成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn update_season(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Json(data): Json<UpdateSeasonRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.update_season(id, data).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}/approve",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
),
|
||||
request_body = UpdateApprovedRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新审批状态成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn update_approved(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Json(data): Json<UpdateApprovedRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
let service = UserService::new(state.db);
|
||||
// 这里需要从认证中间件获取用户信息,检查是否为管理员
|
||||
// 暂时硬编码为true,实际项目中需要从JWT令牌中解析
|
||||
let is_admin = true;
|
||||
let user = service.update_approved(id, data, is_admin).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[derive(ToSchema)]
|
||||
pub struct PaginatedUserResponse {
|
||||
items: Vec<UserResponse>,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateMapCommentRequest {
|
||||
pub map_selection_id: i32,
|
||||
pub osu_id: String,
|
||||
pub username: String,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateMapCommentRequest {
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct MapCommentQueryParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub map_selection_id: Option<i32>,
|
||||
pub osu_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateMapSelectionRequest {
|
||||
pub beatmap_id: i32,
|
||||
pub beatmapset_id: i32,
|
||||
pub title: String,
|
||||
pub title_unicode: Option<String>,
|
||||
pub artist: String,
|
||||
pub artist_unicode: Option<String>,
|
||||
pub version: String,
|
||||
pub creator: String,
|
||||
pub star_rating: String,
|
||||
pub bpm: String,
|
||||
pub total_length: i32,
|
||||
pub max_combo: i32,
|
||||
pub ar: String,
|
||||
pub cs: String,
|
||||
pub od: String,
|
||||
pub hp: String,
|
||||
pub selected_mods: String,
|
||||
pub mod_position: i32,
|
||||
pub comment: Option<String>,
|
||||
pub selected_by: String,
|
||||
pub season: String,
|
||||
pub category: String,
|
||||
pub url: String,
|
||||
pub cover_url: Option<String>,
|
||||
pub approved: Option<i32>,
|
||||
pub padding: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateMapSelectionRequest {
|
||||
pub title: Option<String>,
|
||||
pub title_unicode: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub artist_unicode: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub creator: Option<String>,
|
||||
pub star_rating: Option<String>,
|
||||
pub bpm: Option<String>,
|
||||
pub total_length: Option<i32>,
|
||||
pub max_combo: Option<i32>,
|
||||
pub ar: Option<String>,
|
||||
pub cs: Option<String>,
|
||||
pub od: Option<String>,
|
||||
pub hp: Option<String>,
|
||||
pub selected_mods: Option<String>,
|
||||
pub mod_position: Option<i32>,
|
||||
pub comment: Option<String>,
|
||||
pub season: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub url: Option<String>,
|
||||
pub cover_url: Option<String>,
|
||||
pub approved: Option<i32>,
|
||||
pub padding: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct MapSelectionQueryParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub season: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub selected_by: Option<String>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
use sea_orm::prelude::{Date, Time};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateMatchRoomRequest {
|
||||
pub room_name: String,
|
||||
pub round_number: i32,
|
||||
#[schema(value_type = String)]
|
||||
pub match_date: Date,
|
||||
#[schema(value_type = String)]
|
||||
pub match_time: Time,
|
||||
pub match_number: i32,
|
||||
pub max_participants: Option<i32>,
|
||||
pub created_by: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateMatchRoomRequest {
|
||||
pub room_name: Option<String>,
|
||||
pub round_number: Option<i32>,
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub match_date: Option<Date>,
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub match_time: Option<Time>,
|
||||
pub match_number: Option<i32>,
|
||||
pub max_participants: Option<i32>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct MatchRoomQueryParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub round_number: Option<i32>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateMatchScheduleRequest {
|
||||
pub room_id: i32,
|
||||
pub player1_osu_id: String,
|
||||
pub player1_username: String,
|
||||
pub player2_osu_id: String,
|
||||
pub player2_username: String,
|
||||
pub created_by: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateMatchScheduleRequest {
|
||||
pub red_score: Option<i32>,
|
||||
pub blue_score: Option<i32>,
|
||||
pub status: Option<String>,
|
||||
pub replay_link: Option<String>,
|
||||
pub match_link: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct MatchScheduleQueryParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub room_id: Option<i32>,
|
||||
pub status: Option<String>,
|
||||
pub player_osu_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateMessageRequest {
|
||||
pub sender_osu_id: String,
|
||||
pub sender_username: String,
|
||||
pub receiver_osu_id: String,
|
||||
pub receiver_username: String,
|
||||
pub r#type: Option<String>,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub related_matchup_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateMessageRequest {
|
||||
pub title: Option<String>,
|
||||
pub content: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub response_action: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct MessageQueryParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub sender_osu_id: Option<String>,
|
||||
pub receiver_osu_id: Option<String>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ pub mod tournament_settings;
|
||||
pub mod user;
|
||||
|
||||
/// 通用响应结构
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct ApiResponse<T: utoipa::ToSchema> {
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub success: bool,
|
||||
pub data: Option<T>,
|
||||
pub message: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl<T: utoipa::ToSchema> ApiResponse<T> {
|
||||
impl<T> ApiResponse<T> {
|
||||
pub fn success(data: T) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
@@ -55,6 +55,14 @@ impl<T: utoipa::ToSchema> ApiResponse<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct ApiResponseSchema<T: utoipa::ToSchema> {
|
||||
pub success: bool,
|
||||
pub data: Option<T>,
|
||||
pub message: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 分页请求参数
|
||||
#[derive(Debug, serde::Deserialize, utoipa::IntoParams)]
|
||||
pub struct PaginationParams {
|
||||
@@ -72,8 +80,8 @@ fn default_page_size() -> u64 {
|
||||
20
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct PaginatedResponse<T: utoipa::ToSchema> {
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct PaginatedResponse<T> {
|
||||
pub items: Vec<T>,
|
||||
pub total: u64,
|
||||
pub page: u64,
|
||||
@@ -81,7 +89,7 @@ pub struct PaginatedResponse<T: utoipa::ToSchema> {
|
||||
pub total_pages: u64,
|
||||
}
|
||||
|
||||
impl<T: utoipa::ToSchema> PaginatedResponse<T> {
|
||||
impl<T> PaginatedResponse<T> {
|
||||
pub fn new(items: Vec<T>, total: u64, page: u64, page_size: u64) -> Self {
|
||||
let total_pages = (total as f64 / page_size as f64).ceil() as u64;
|
||||
Self {
|
||||
@@ -93,3 +101,12 @@ impl<T: utoipa::ToSchema> PaginatedResponse<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct PaginatedResponseSchema<T: utoipa::ToSchema> {
|
||||
pub items: Vec<T>,
|
||||
pub total: u64,
|
||||
pub page: u64,
|
||||
pub page_size: u64,
|
||||
pub total_pages: u64,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreatePlayerMatchupRequest {
|
||||
pub player1_osu_id: String,
|
||||
pub player1_username: String,
|
||||
pub player2_osu_id: String,
|
||||
pub player2_username: String,
|
||||
pub created_by: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdatePlayerMatchupRequest {
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct PlayerMatchupQueryParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub status: Option<String>,
|
||||
pub player_osu_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateTournamentSettingRequest {
|
||||
pub tournament_name: String,
|
||||
pub max_pp_for_registration: Option<f32>,
|
||||
pub min_pp_for_registration: Option<f32>,
|
||||
pub current_season: Option<String>,
|
||||
pub current_season_stage: Option<String>,
|
||||
pub mappool_visible: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateTournamentSettingRequest {
|
||||
pub tournament_name: Option<String>,
|
||||
pub max_pp_for_registration: Option<f32>,
|
||||
pub min_pp_for_registration: Option<f32>,
|
||||
pub current_season: Option<String>,
|
||||
pub current_season_stage: Option<String>,
|
||||
pub mappool_visible: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct TournamentSettingQueryParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub current_season: Option<String>,
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ pub struct UserResponse {
|
||||
pub approved: Option<i32>,
|
||||
pub user_group: String,
|
||||
pub season: Option<i32>,
|
||||
pub registration_status: Option<String>,
|
||||
pub gu_server_user_id: Option<i32>,
|
||||
pub gu_server_username: Option<String>,
|
||||
pub created_at: String,
|
||||
@@ -75,6 +76,9 @@ impl From<UserModel> for UserResponse {
|
||||
approved: user.approved,
|
||||
user_group: format!("{:?}", user.user_group),
|
||||
season: user.season,
|
||||
registration_status: user
|
||||
.registration_status
|
||||
.map(|status| format!("{:?}", status)),
|
||||
gu_server_user_id: user.gu_server_user_id,
|
||||
gu_server_username: user.gu_server_username,
|
||||
created_at: user.created_at.to_rfc3339(),
|
||||
@@ -88,7 +92,23 @@ pub struct UpdateUserGroupRequest {
|
||||
pub user_group: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, utoipa::IntoParams)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateRegistrationStatusRequest {
|
||||
pub registration_status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateSeasonRequest {
|
||||
pub season: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateApprovedRequest {
|
||||
pub approved: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, utoipa::IntoParams, utoipa::ToSchema)]
|
||||
#[schema(description = "用户查询参数")]
|
||||
pub struct UserQueryParams {
|
||||
pub username: Option<String>,
|
||||
pub country: Option<String>,
|
||||
|
||||
@@ -30,3 +30,14 @@ pub enum UserGroup {
|
||||
#[sea_orm(string_value = "admin")]
|
||||
Admin,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "registration_status")]
|
||||
pub enum RegistrationStatus {
|
||||
#[sea_orm(string_value = "pending")]
|
||||
Pending,
|
||||
#[sea_orm(string_value = "confirmed")]
|
||||
Confirmed,
|
||||
#[sea_orm(string_value = "rejected")]
|
||||
Rejected,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use super::sea_orm_active_enums::UserGroup;
|
||||
use super::sea_orm_active_enums::{RegistrationStatus, UserGroup};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
@@ -22,6 +22,7 @@ pub struct Model {
|
||||
pub approved: Option<i32>,
|
||||
pub user_group: UserGroup,
|
||||
pub season: Option<i32>,
|
||||
pub registration_status: Option<RegistrationStatus>,
|
||||
pub gu_server_user_id: Option<i32>,
|
||||
pub gu_server_username: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
@@ -36,6 +36,9 @@ pub enum AppError {
|
||||
|
||||
#[error("内部服务器错误: {0}")]
|
||||
InternalError(String),
|
||||
|
||||
#[error("功能未实现: {0}")]
|
||||
NotImplemented(String),
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
@@ -48,7 +51,7 @@ impl AppError {
|
||||
AppError::BusinessError(_) => StatusCode::BAD_REQUEST,
|
||||
AppError::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AppError::ExternalApiError(_) => StatusCode::BAD_GATEWAY,
|
||||
AppError::DatabaseError(_) | AppError::InternalError(_) => {
|
||||
AppError::DatabaseError(_) | AppError::InternalError(_) | AppError::NotImplemented(_) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
@@ -65,6 +68,7 @@ impl AppError {
|
||||
AppError::ConfigError(msg) => format!("配置错误: {}", msg),
|
||||
AppError::ExternalApiError(msg) => format!("外部API错误: {}", msg),
|
||||
AppError::InternalError(msg) => format!("内部服务器错误: {}", msg),
|
||||
AppError::NotImplemented(msg) => format!("功能未实现: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("数据库连接成功");
|
||||
|
||||
// 创建服务状态
|
||||
let service_state = Arc::new(ServiceState::new(db));
|
||||
let service_state = ServiceState::new(db);
|
||||
|
||||
// 配置应用
|
||||
let app = api::configure_routes()
|
||||
.layer(middleware::configure_middleware())
|
||||
.with_state(service_state);
|
||||
|
||||
// 启动服务器
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::map_comments::{CreateMapCommentRequest, MapCommentQueryParams, UpdateMapCommentRequest},
|
||||
entity::map_comments,
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
pub struct MapCommentService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl MapCommentService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
params: MapCommentQueryParams,
|
||||
) -> Result<(Vec<map_comments::Model>, u64)> {
|
||||
let mut query = map_comments::Entity::find();
|
||||
|
||||
if let Some(map_selection_id) = params.map_selection_id {
|
||||
query = query.filter(map_comments::Column::MapSelectionId.eq(map_selection_id));
|
||||
}
|
||||
|
||||
if let Some(osu_id) = params.osu_id {
|
||||
query = query.filter(map_comments::Column::OsuId.eq(osu_id));
|
||||
}
|
||||
|
||||
query = query.order_by_desc(map_comments::Column::CreatedAt);
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
|
||||
let total = query.clone().count(&self.db).await?;
|
||||
let map_comments = query
|
||||
.paginate(&self.db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await?;
|
||||
|
||||
Ok((map_comments, total))
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: i32) -> Result<map_comments::Model> {
|
||||
map_comments::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("地图评论 {} 不存在", id)))
|
||||
}
|
||||
|
||||
pub async fn create(&self, data: CreateMapCommentRequest) -> Result<map_comments::Model> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let map_comment = map_comments::ActiveModel {
|
||||
map_selection_id: Set(data.map_selection_id),
|
||||
osu_id: Set(data.osu_id),
|
||||
username: Set(data.username),
|
||||
comment: Set(data.comment),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(map_comment.insert(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&self,
|
||||
id: i32,
|
||||
data: UpdateMapCommentRequest,
|
||||
) -> Result<map_comments::Model> {
|
||||
let mut map_comment: map_comments::ActiveModel = self.find_by_id(id).await?.into();
|
||||
|
||||
if let Some(comment) = data.comment {
|
||||
map_comment.comment = Set(Some(comment));
|
||||
}
|
||||
|
||||
map_comment.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(map_comment.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: i32) -> Result<()> {
|
||||
let result = map_comments::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AppError::NotFound(format!("地图评论 {} 不存在", id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
use sea_orm::prelude::Decimal;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::map_selections::{
|
||||
CreateMapSelectionRequest, MapSelectionQueryParams, UpdateMapSelectionRequest,
|
||||
},
|
||||
entity::map_selections,
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
pub struct MapSelectionService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl MapSelectionService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
params: MapSelectionQueryParams,
|
||||
) -> Result<(Vec<map_selections::Model>, u64)> {
|
||||
let mut query = map_selections::Entity::find();
|
||||
|
||||
if let Some(season) = params.season {
|
||||
query = query.filter(map_selections::Column::Season.eq(season));
|
||||
}
|
||||
|
||||
if let Some(category) = params.category {
|
||||
query = query.filter(map_selections::Column::Category.eq(category));
|
||||
}
|
||||
|
||||
if let Some(selected_by) = params.selected_by {
|
||||
query = query.filter(map_selections::Column::SelectedBy.eq(selected_by));
|
||||
}
|
||||
|
||||
query = query.order_by_desc(map_selections::Column::CreatedAt);
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
|
||||
let total = query.clone().count(&self.db).await?;
|
||||
let map_selections = query
|
||||
.paginate(&self.db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await?;
|
||||
|
||||
Ok((map_selections, total))
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: i32) -> Result<map_selections::Model> {
|
||||
map_selections::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("地图选择 {} 不存在", id)))
|
||||
}
|
||||
|
||||
pub async fn create(&self, data: CreateMapSelectionRequest) -> Result<map_selections::Model> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let map_selection = map_selections::ActiveModel {
|
||||
beatmap_id: Set(data.beatmap_id),
|
||||
beatmapset_id: Set(data.beatmapset_id),
|
||||
title: Set(data.title),
|
||||
title_unicode: Set(data.title_unicode),
|
||||
artist: Set(data.artist),
|
||||
artist_unicode: Set(data.artist_unicode),
|
||||
version: Set(data.version),
|
||||
creator: Set(data.creator),
|
||||
star_rating: Set(Decimal::from(data.star_rating.parse::<i64>().unwrap())),
|
||||
bpm: Set(Decimal::from(data.bpm.parse::<i64>().unwrap())),
|
||||
total_length: Set(data.total_length),
|
||||
max_combo: Set(data.max_combo),
|
||||
ar: Set(Decimal::from(data.ar.parse::<i64>().unwrap())),
|
||||
cs: Set(Decimal::from(data.cs.parse::<i64>().unwrap())),
|
||||
od: Set(Decimal::from(data.od.parse::<i64>().unwrap())),
|
||||
hp: Set(Decimal::from(data.hp.parse::<i64>().unwrap())),
|
||||
selected_mods: Set(data.selected_mods),
|
||||
mod_position: Set(data.mod_position),
|
||||
comment: Set(data.comment),
|
||||
selected_by: Set(data.selected_by),
|
||||
selected_at: Set(now),
|
||||
season: Set(data.season),
|
||||
category: Set(data.category),
|
||||
url: Set(data.url),
|
||||
cover_url: Set(data.cover_url),
|
||||
approved: Set(data.approved),
|
||||
padding: Set(data.padding),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(map_selection.insert(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&self,
|
||||
id: i32,
|
||||
data: UpdateMapSelectionRequest,
|
||||
) -> Result<map_selections::Model> {
|
||||
let mut map_selection: map_selections::ActiveModel = self.find_by_id(id).await?.into();
|
||||
|
||||
if let Some(title) = data.title {
|
||||
map_selection.title = Set(title);
|
||||
}
|
||||
|
||||
if let Some(title_unicode) = data.title_unicode {
|
||||
map_selection.title_unicode = Set(Some(title_unicode));
|
||||
}
|
||||
|
||||
if let Some(artist) = data.artist {
|
||||
map_selection.artist = Set(artist);
|
||||
}
|
||||
|
||||
if let Some(artist_unicode) = data.artist_unicode {
|
||||
map_selection.artist_unicode = Set(Some(artist_unicode));
|
||||
}
|
||||
|
||||
if let Some(version) = data.version {
|
||||
map_selection.version = Set(version);
|
||||
}
|
||||
|
||||
if let Some(creator) = data.creator {
|
||||
map_selection.creator = Set(creator);
|
||||
}
|
||||
|
||||
if let Some(star_rating) = data.star_rating {
|
||||
map_selection.star_rating = Set(Decimal::from(star_rating.parse::<i64>().unwrap()));
|
||||
}
|
||||
|
||||
if let Some(bpm) = data.bpm {
|
||||
map_selection.bpm = Set(Decimal::from(bpm.parse::<i64>().unwrap()));
|
||||
}
|
||||
|
||||
if let Some(total_length) = data.total_length {
|
||||
map_selection.total_length = Set(total_length);
|
||||
}
|
||||
|
||||
if let Some(max_combo) = data.max_combo {
|
||||
map_selection.max_combo = Set(max_combo);
|
||||
}
|
||||
|
||||
if let Some(ar) = data.ar {
|
||||
map_selection.ar = Set(Decimal::from(ar.parse::<i64>().unwrap()));
|
||||
}
|
||||
|
||||
if let Some(cs) = data.cs {
|
||||
map_selection.cs = Set(Decimal::from(cs.parse::<i64>().unwrap()));
|
||||
}
|
||||
|
||||
if let Some(od) = data.od {
|
||||
map_selection.od = Set(Decimal::from(od.parse::<i64>().unwrap()));
|
||||
}
|
||||
|
||||
if let Some(hp) = data.hp {
|
||||
map_selection.hp = Set(Decimal::from(hp.parse::<i64>().unwrap()));
|
||||
}
|
||||
|
||||
if let Some(selected_mods) = data.selected_mods {
|
||||
map_selection.selected_mods = Set(selected_mods);
|
||||
}
|
||||
|
||||
if let Some(mod_position) = data.mod_position {
|
||||
map_selection.mod_position = Set(mod_position);
|
||||
}
|
||||
|
||||
if let Some(comment) = data.comment {
|
||||
map_selection.comment = Set(Some(comment));
|
||||
}
|
||||
|
||||
if let Some(season) = data.season {
|
||||
map_selection.season = Set(season);
|
||||
}
|
||||
|
||||
if let Some(category) = data.category {
|
||||
map_selection.category = Set(category);
|
||||
}
|
||||
|
||||
if let Some(url) = data.url {
|
||||
map_selection.url = Set(url);
|
||||
}
|
||||
|
||||
if let Some(cover_url) = data.cover_url {
|
||||
map_selection.cover_url = Set(Some(cover_url));
|
||||
}
|
||||
|
||||
if let Some(approved) = data.approved {
|
||||
map_selection.approved = Set(Some(approved));
|
||||
}
|
||||
|
||||
if let Some(padding) = data.padding {
|
||||
map_selection.padding = Set(Some(padding));
|
||||
}
|
||||
|
||||
map_selection.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(map_selection.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: i32) -> Result<()> {
|
||||
let result = map_selections::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AppError::NotFound(format!("地图选择 {} 不存在", id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
prelude::{Date, Time},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::match_rooms::{CreateMatchRoomRequest, MatchRoomQueryParams, UpdateMatchRoomRequest},
|
||||
entity::{match_rooms, sea_orm_active_enums::Status},
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
pub struct MatchRoomService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl MatchRoomService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
params: MatchRoomQueryParams,
|
||||
) -> Result<(Vec<match_rooms::Model>, u64)> {
|
||||
let mut query = match_rooms::Entity::find();
|
||||
|
||||
if let Some(round_number) = params.round_number {
|
||||
query = query.filter(match_rooms::Column::RoundNumber.eq(round_number));
|
||||
}
|
||||
|
||||
if let Some(status) = params.status {
|
||||
let status_enum = match status.as_str() {
|
||||
"available" => Status::Available,
|
||||
"scheduled" => Status::Scheduled,
|
||||
"completed" => Status::Completed,
|
||||
_ => return Err(AppError::ValidationError("无效的状态".into())),
|
||||
};
|
||||
query = query.filter(match_rooms::Column::Status.eq(Some(status_enum)));
|
||||
}
|
||||
|
||||
query = query.order_by_asc(match_rooms::Column::Id);
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
|
||||
let total = query.clone().count(&self.db).await?;
|
||||
let match_rooms = query
|
||||
.paginate(&self.db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await?;
|
||||
|
||||
Ok((match_rooms, total))
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: i32) -> Result<match_rooms::Model> {
|
||||
match_rooms::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("匹配房间 {} 不存在", id)))
|
||||
}
|
||||
|
||||
pub async fn create(&self, data: CreateMatchRoomRequest) -> Result<match_rooms::Model> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let match_room = match_rooms::ActiveModel {
|
||||
room_name: Set(data.room_name),
|
||||
round_number: Set(data.round_number),
|
||||
match_date: Set(data.match_date),
|
||||
match_time: Set(data.match_time),
|
||||
match_number: Set(data.match_number),
|
||||
max_participants: Set(data.max_participants),
|
||||
status: Set(Some(Status::Available)),
|
||||
created_by: Set(data.created_by),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(match_room.insert(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&self,
|
||||
id: i32,
|
||||
data: UpdateMatchRoomRequest,
|
||||
) -> Result<match_rooms::Model> {
|
||||
let mut match_room: match_rooms::ActiveModel = self.find_by_id(id).await?.into();
|
||||
|
||||
if let Some(room_name) = data.room_name {
|
||||
match_room.room_name = Set(room_name);
|
||||
}
|
||||
|
||||
if let Some(round_number) = data.round_number {
|
||||
match_room.round_number = Set(round_number);
|
||||
}
|
||||
|
||||
if let Some(match_date) = data.match_date {
|
||||
match_room.match_date = Set(match_date);
|
||||
}
|
||||
|
||||
if let Some(match_time) = data.match_time {
|
||||
match_room.match_time = Set(match_time);
|
||||
}
|
||||
|
||||
if let Some(match_number) = data.match_number {
|
||||
match_room.match_number = Set(match_number);
|
||||
}
|
||||
|
||||
if let Some(max_participants) = data.max_participants {
|
||||
match_room.max_participants = Set(Some(max_participants));
|
||||
}
|
||||
|
||||
if let Some(status) = data.status {
|
||||
let status_enum = match status.as_str() {
|
||||
"available" => Status::Available,
|
||||
"scheduled" => Status::Scheduled,
|
||||
"completed" => Status::Completed,
|
||||
_ => return Err(AppError::ValidationError("无效的状态".into())),
|
||||
};
|
||||
match_room.status = Set(Some(status_enum));
|
||||
}
|
||||
|
||||
match_room.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(match_room.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: i32) -> Result<()> {
|
||||
let result = match_rooms::Entity::delete_by_id(id).exec(&self.db).await?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AppError::NotFound(format!("匹配房间 {} 不存在", id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::match_schedules::{
|
||||
CreateMatchScheduleRequest, MatchScheduleQueryParams, UpdateMatchScheduleRequest,
|
||||
},
|
||||
entity::{match_schedules, sea_orm_active_enums::Status},
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
pub struct MatchScheduleService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl MatchScheduleService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
params: MatchScheduleQueryParams,
|
||||
) -> Result<(Vec<match_schedules::Model>, u64)> {
|
||||
let mut query = match_schedules::Entity::find();
|
||||
|
||||
if let Some(room_id) = params.room_id {
|
||||
query = query.filter(match_schedules::Column::RoomId.eq(room_id));
|
||||
}
|
||||
|
||||
if let Some(status) = params.status {
|
||||
let status_enum = match status.as_str() {
|
||||
"available" => Status::Available,
|
||||
"scheduled" => Status::Scheduled,
|
||||
"completed" => Status::Completed,
|
||||
_ => return Err(AppError::ValidationError("无效的状态".into())),
|
||||
};
|
||||
query = query.filter(match_schedules::Column::Status.eq(Some(status_enum)));
|
||||
}
|
||||
|
||||
if let Some(player_osu_id) = params.player_osu_id {
|
||||
query = query.filter(
|
||||
match_schedules::Column::Player1OsuId
|
||||
.eq(&player_osu_id)
|
||||
.or(match_schedules::Column::Player2OsuId.eq(&player_osu_id)),
|
||||
);
|
||||
}
|
||||
|
||||
query = query.order_by_desc(match_schedules::Column::CreatedAt);
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
|
||||
let total = query.clone().count(&self.db).await?;
|
||||
let match_schedules = query
|
||||
.paginate(&self.db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await?;
|
||||
|
||||
Ok((match_schedules, total))
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: i32) -> Result<match_schedules::Model> {
|
||||
match_schedules::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("匹配赛程 {} 不存在", id)))
|
||||
}
|
||||
|
||||
pub async fn create(&self, data: CreateMatchScheduleRequest) -> Result<match_schedules::Model> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let match_schedule = match_schedules::ActiveModel {
|
||||
room_id: Set(data.room_id),
|
||||
player1_osu_id: Set(data.player1_osu_id),
|
||||
player1_username: Set(data.player1_username),
|
||||
player2_osu_id: Set(data.player2_osu_id),
|
||||
player2_username: Set(data.player2_username),
|
||||
status: Set(Some(Status::Available)),
|
||||
created_by: Set(data.created_by),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(match_schedule.insert(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&self,
|
||||
id: i32,
|
||||
data: UpdateMatchScheduleRequest,
|
||||
) -> Result<match_schedules::Model> {
|
||||
let mut match_schedule: match_schedules::ActiveModel = self.find_by_id(id).await?.into();
|
||||
|
||||
if let Some(red_score) = data.red_score {
|
||||
match_schedule.red_score = Set(Some(red_score));
|
||||
}
|
||||
|
||||
if let Some(blue_score) = data.blue_score {
|
||||
match_schedule.blue_score = Set(Some(blue_score));
|
||||
}
|
||||
|
||||
if let Some(status) = data.status {
|
||||
let status_enum = match status.as_str() {
|
||||
"available" => Status::Available,
|
||||
"scheduled" => Status::Scheduled,
|
||||
"completed" => Status::Completed,
|
||||
_ => return Err(AppError::ValidationError("无效的状态".into())),
|
||||
};
|
||||
match_schedule.status = Set(Some(status_enum));
|
||||
}
|
||||
|
||||
if let Some(replay_link) = data.replay_link {
|
||||
match_schedule.replay_link = Set(Some(replay_link));
|
||||
}
|
||||
|
||||
if let Some(match_link) = data.match_link {
|
||||
match_schedule.match_link = Set(Some(match_link));
|
||||
}
|
||||
|
||||
match_schedule.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(match_schedule.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: i32) -> Result<()> {
|
||||
let result = match_schedules::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AppError::NotFound(format!("匹配赛程 {} 不存在", id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::messages::{CreateMessageRequest, MessageQueryParams, UpdateMessageRequest},
|
||||
entity::{
|
||||
messages,
|
||||
sea_orm_active_enums::{Status, Type},
|
||||
},
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
pub struct MessageService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl MessageService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
params: MessageQueryParams,
|
||||
) -> Result<(Vec<messages::Model>, u64)> {
|
||||
let mut query = messages::Entity::find();
|
||||
|
||||
if let Some(sender_osu_id) = params.sender_osu_id {
|
||||
query = query.filter(messages::Column::SenderOsuId.eq(sender_osu_id));
|
||||
}
|
||||
|
||||
if let Some(receiver_osu_id) = params.receiver_osu_id {
|
||||
query = query.filter(messages::Column::ReceiverOsuId.eq(receiver_osu_id));
|
||||
}
|
||||
|
||||
if let Some(status) = params.status {
|
||||
let status_enum = match status.as_str() {
|
||||
"available" => Status::Available,
|
||||
"scheduled" => Status::Scheduled,
|
||||
"completed" => Status::Completed,
|
||||
_ => return Err(AppError::ValidationError("无效的状态".into())),
|
||||
};
|
||||
query = query.filter(messages::Column::Status.eq(Some(status_enum)));
|
||||
}
|
||||
|
||||
query = query.order_by_desc(messages::Column::CreatedAt);
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
|
||||
let total = query.clone().count(&self.db).await?;
|
||||
let messages = query
|
||||
.paginate(&self.db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await?;
|
||||
|
||||
Ok((messages, total))
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: i32) -> Result<messages::Model> {
|
||||
messages::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("消息 {} 不存在", id)))
|
||||
}
|
||||
|
||||
pub async fn create(&self, data: CreateMessageRequest) -> Result<messages::Model> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let message_type = data.r#type.map(|t| match t.as_str() {
|
||||
"system" => Type::System,
|
||||
"match_invitation" => Type::MatchInvitation,
|
||||
"match_response" => Type::MatchResponse,
|
||||
_ => panic!("无效的消息类型"),
|
||||
});
|
||||
|
||||
let message = messages::ActiveModel {
|
||||
sender_osu_id: Set(data.sender_osu_id),
|
||||
sender_username: Set(data.sender_username),
|
||||
receiver_osu_id: Set(data.receiver_osu_id),
|
||||
receiver_username: Set(data.receiver_username),
|
||||
r#type: Set(message_type),
|
||||
title: Set(data.title),
|
||||
content: Set(data.content),
|
||||
related_matchup_id: Set(data.related_matchup_id),
|
||||
status: Set(Some(Status::Available)),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(message.insert(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: i32, data: UpdateMessageRequest) -> Result<messages::Model> {
|
||||
let mut message: messages::ActiveModel = self.find_by_id(id).await?.into();
|
||||
|
||||
if let Some(title) = data.title {
|
||||
message.title = Set(title);
|
||||
}
|
||||
|
||||
if let Some(content) = data.content {
|
||||
message.content = Set(content);
|
||||
}
|
||||
|
||||
if let Some(status) = data.status {
|
||||
let status_enum = match status.as_str() {
|
||||
"available" => Status::Available,
|
||||
"scheduled" => Status::Scheduled,
|
||||
"completed" => Status::Completed,
|
||||
_ => return Err(AppError::ValidationError("无效的状态".into())),
|
||||
};
|
||||
message.status = Set(Some(status_enum));
|
||||
if status == "completed" {
|
||||
message.response_time = Set(Some(chrono::Utc::now()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(response_action) = data.response_action {
|
||||
message.response_action = Set(Some(response_action));
|
||||
}
|
||||
|
||||
message.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(message.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: i32) -> Result<()> {
|
||||
let result = messages::Entity::delete_by_id(id).exec(&self.db).await?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AppError::NotFound(format!("消息 {} 不存在", id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::player_matchups::{
|
||||
CreatePlayerMatchupRequest, PlayerMatchupQueryParams, UpdatePlayerMatchupRequest,
|
||||
},
|
||||
entity::{player_matchups, sea_orm_active_enums::Status},
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
pub struct PlayerMatchupService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl PlayerMatchupService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
params: PlayerMatchupQueryParams,
|
||||
) -> Result<(Vec<player_matchups::Model>, u64)> {
|
||||
let mut query = player_matchups::Entity::find();
|
||||
|
||||
if let Some(status) = params.status {
|
||||
let status_enum = match status.as_str() {
|
||||
"available" => Status::Available,
|
||||
"scheduled" => Status::Scheduled,
|
||||
"completed" => Status::Completed,
|
||||
_ => return Err(AppError::ValidationError("无效的状态".into())),
|
||||
};
|
||||
query = query.filter(player_matchups::Column::Status.eq(Some(status_enum)));
|
||||
}
|
||||
|
||||
if let Some(player_osu_id) = params.player_osu_id {
|
||||
query = query.filter(
|
||||
player_matchups::Column::Player1OsuId
|
||||
.eq(&player_osu_id)
|
||||
.or(player_matchups::Column::Player2OsuId.eq(&player_osu_id)),
|
||||
);
|
||||
}
|
||||
|
||||
query = query.order_by_desc(player_matchups::Column::CreatedAt);
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
|
||||
let total = query.clone().count(&self.db).await?;
|
||||
let player_matchups = query
|
||||
.paginate(&self.db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await?;
|
||||
|
||||
Ok((player_matchups, total))
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: i32) -> Result<player_matchups::Model> {
|
||||
player_matchups::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("玩家对战 {} 不存在", id)))
|
||||
}
|
||||
|
||||
pub async fn create(&self, data: CreatePlayerMatchupRequest) -> Result<player_matchups::Model> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let player_matchup = player_matchups::ActiveModel {
|
||||
player1_osu_id: Set(data.player1_osu_id),
|
||||
player1_username: Set(data.player1_username),
|
||||
player2_osu_id: Set(data.player2_osu_id),
|
||||
player2_username: Set(data.player2_username),
|
||||
status: Set(Some(Status::Available)),
|
||||
created_by: Set(data.created_by),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(player_matchup.insert(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&self,
|
||||
id: i32,
|
||||
data: UpdatePlayerMatchupRequest,
|
||||
) -> Result<player_matchups::Model> {
|
||||
let mut player_matchup: player_matchups::ActiveModel = self.find_by_id(id).await?.into();
|
||||
|
||||
if let Some(status) = data.status {
|
||||
let status_enum = match status.as_str() {
|
||||
"available" => Status::Available,
|
||||
"scheduled" => Status::Scheduled,
|
||||
"completed" => Status::Completed,
|
||||
_ => return Err(AppError::ValidationError("无效的状态".into())),
|
||||
};
|
||||
player_matchup.status = Set(Some(status_enum));
|
||||
}
|
||||
|
||||
player_matchup.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(player_matchup.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: i32) -> Result<()> {
|
||||
let result = player_matchups::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AppError::NotFound(format!("玩家对战 {} 不存在", id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::tournament_settings::{
|
||||
CreateTournamentSettingRequest, TournamentSettingQueryParams,
|
||||
UpdateTournamentSettingRequest,
|
||||
},
|
||||
entity::tournament_settings,
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
pub struct TournamentSettingService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl TournamentSettingService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
params: TournamentSettingQueryParams,
|
||||
) -> Result<(Vec<tournament_settings::Model>, u64)> {
|
||||
let mut query = tournament_settings::Entity::find();
|
||||
|
||||
if let Some(current_season) = params.current_season {
|
||||
query =
|
||||
query.filter(tournament_settings::Column::CurrentSeason.eq(Some(current_season)));
|
||||
}
|
||||
|
||||
query = query.order_by_desc(tournament_settings::Column::CreatedAt);
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
|
||||
let total = query.clone().count(&self.db).await?;
|
||||
let tournament_settings = query
|
||||
.paginate(&self.db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await?;
|
||||
|
||||
Ok((tournament_settings, total))
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: i32) -> Result<tournament_settings::Model> {
|
||||
tournament_settings::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("锦标赛设置 {} 不存在", id)))
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
&self,
|
||||
data: CreateTournamentSettingRequest,
|
||||
) -> Result<tournament_settings::Model> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let tournament_setting = tournament_settings::ActiveModel {
|
||||
tournament_name: Set(data.tournament_name),
|
||||
max_pp_for_registration: Set(data.max_pp_for_registration),
|
||||
min_pp_for_registration: Set(data.min_pp_for_registration),
|
||||
current_season: Set(data.current_season),
|
||||
current_season_stage: Set(data.current_season_stage),
|
||||
mappool_visible: Set(data.mappool_visible),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(tournament_setting.insert(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&self,
|
||||
id: i32,
|
||||
data: UpdateTournamentSettingRequest,
|
||||
) -> Result<tournament_settings::Model> {
|
||||
let mut tournament_setting: tournament_settings::ActiveModel =
|
||||
self.find_by_id(id).await?.into();
|
||||
|
||||
if let Some(tournament_name) = data.tournament_name {
|
||||
tournament_setting.tournament_name = Set(tournament_name);
|
||||
}
|
||||
|
||||
if let Some(max_pp_for_registration) = data.max_pp_for_registration {
|
||||
tournament_setting.max_pp_for_registration = Set(Some(max_pp_for_registration));
|
||||
}
|
||||
|
||||
if let Some(min_pp_for_registration) = data.min_pp_for_registration {
|
||||
tournament_setting.min_pp_for_registration = Set(Some(min_pp_for_registration));
|
||||
}
|
||||
|
||||
if let Some(current_season) = data.current_season {
|
||||
tournament_setting.current_season = Set(Some(current_season));
|
||||
}
|
||||
|
||||
if let Some(current_season_stage) = data.current_season_stage {
|
||||
tournament_setting.current_season_stage = Set(Some(current_season_stage));
|
||||
}
|
||||
|
||||
if let Some(mappool_visible) = data.mappool_visible {
|
||||
tournament_setting.mappool_visible = Set(Some(mappool_visible));
|
||||
}
|
||||
|
||||
tournament_setting.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(tournament_setting.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: i32) -> Result<()> {
|
||||
let result = tournament_settings::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AppError::NotFound(format!("锦标赛设置 {} 不存在", id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_current_settings(&self) -> Result<Option<tournament_settings::Model>> {
|
||||
Ok(tournament_settings::Entity::find()
|
||||
.order_by_desc(tournament_settings::Column::CreatedAt)
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use sea_orm::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::user::{CreateUserRequest, UpdateUserGroupRequest, UpdateUserRequest, UserQueryParams},
|
||||
entity::{sea_orm_active_enums::UserGroup, user},
|
||||
dto::user::{CreateUserRequest, UpdateApprovedRequest, UpdateRegistrationStatusRequest, UpdateSeasonRequest, UpdateUserGroupRequest, UpdateUserRequest, UserQueryParams},
|
||||
entity::{sea_orm_active_enums::{RegistrationStatus, UserGroup}, user},
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
@@ -81,6 +81,7 @@ impl UserService {
|
||||
approved: Set(None),
|
||||
user_group: Set(UserGroup::Player),
|
||||
season: Set(None),
|
||||
registration_status: Set(Some(RegistrationStatus::Pending)),
|
||||
gu_server_user_id: Set(None),
|
||||
gu_server_username: Set(None),
|
||||
created_at: Set(now),
|
||||
@@ -163,4 +164,47 @@ impl UserService {
|
||||
.count(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update_registration_status(&self, id: i32, data: UpdateRegistrationStatusRequest) -> Result<user::Model> {
|
||||
let status_enum = match data.registration_status.as_str() {
|
||||
"pending" => RegistrationStatus::Pending,
|
||||
"confirmed" => RegistrationStatus::Confirmed,
|
||||
"rejected" => RegistrationStatus::Rejected,
|
||||
_ => return Err(AppError::ValidationError("无效的注册状态".into())),
|
||||
};
|
||||
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.registration_status = Set(Some(status_enum));
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update_season(&self, id: i32, data: UpdateSeasonRequest) -> Result<user::Model> {
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.season = Set(Some(data.season));
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update_approved(&self, id: i32, data: UpdateApprovedRequest, is_admin: bool) -> Result<user::Model> {
|
||||
if !is_admin {
|
||||
return Err(AppError::Forbidden("只有管理员可以更新审批状态".into()));
|
||||
}
|
||||
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.approved = Set(Some(data.approved));
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update_season_from_settings(&self, id: i32, season: i32) -> Result<user::Model> {
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.season = Set(Some(season));
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use chrono;
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub sub: String,
|
||||
pub user_group: String,
|
||||
pub exp: usize,
|
||||
}
|
||||
|
||||
/// 生成JWT令牌
|
||||
pub fn generate_token(
|
||||
osu_id: String,
|
||||
user_group: String,
|
||||
) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "secret_key".to_string());
|
||||
let expiration = chrono::Utc::now()
|
||||
.checked_add_signed(chrono::Duration::days(7))
|
||||
.expect("valid timestamp")
|
||||
.timestamp() as usize;
|
||||
|
||||
let claims = Claims {
|
||||
sub: osu_id,
|
||||
user_group,
|
||||
exp: expiration,
|
||||
};
|
||||
|
||||
let header = Header::new(Algorithm::HS256);
|
||||
encode(
|
||||
&header,
|
||||
&claims,
|
||||
&EncodingKey::from_secret(secret.as_bytes()),
|
||||
)
|
||||
}
|
||||
|
||||
/// 验证JWT令牌
|
||||
pub fn verify_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
|
||||
let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "secret_key".to_string());
|
||||
let validation = Validation::new(Algorithm::HS256);
|
||||
decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(secret.as_bytes()),
|
||||
&validation,
|
||||
)
|
||||
.map(|data| data.claims)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user