update
This commit is contained in:
187
Cargo.lock
generated
187
Cargo.lock
generated
@@ -2,6 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
@@ -55,6 +61,15 @@ version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
@@ -106,6 +121,8 @@ dependencies = [
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
@@ -630,6 +647,15 @@ version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
@@ -717,6 +743,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -874,6 +911,16 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"miniz_oxide",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
@@ -1707,6 +1754,26 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
@@ -2552,6 +2619,40 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.114",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
@@ -2998,6 +3099,12 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
@@ -3749,6 +3856,12 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
@@ -3812,6 +3925,48 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui"
|
||||
version = "9.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"utoipa",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.20.0"
|
||||
@@ -4652,8 +4807,40 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
@@ -49,6 +49,8 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.20", features = ["v4", "serde"] }
|
||||
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"] }
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing
|
||||
@@ -63,4 +65,4 @@ debug = true
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
codegen-units = 1
|
||||
212
IMPLEMENTATION_PLAN.md
Normal file
212
IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# AstraCup Backend - CRUD 实施计划
|
||||
|
||||
## 项目概述
|
||||
为AstraCup比赛管理后端生成完整的CRUD API,包括认证、授权和系统初始化功能。
|
||||
|
||||
## 技术栈
|
||||
- **框架**: Axum 0.8.8
|
||||
- **数据库**: SeaORM 1.1.19 + MySQL
|
||||
- **认证**: JWT + osu! OAuth 2.0
|
||||
- **文档**: utoipa (OpenAPI/Swagger)
|
||||
- **验证**: validator 0.20
|
||||
- **配置**: dotenv + config
|
||||
|
||||
## 实施阶段
|
||||
|
||||
### 第一阶段:基础架构搭建 ✅
|
||||
- [x] 创建项目目录结构
|
||||
- [x] 实现数据库连接池
|
||||
- [x] 配置管理系统
|
||||
- [x] 错误处理系统
|
||||
- [x] 基础DTO和Service模板
|
||||
|
||||
### 第二阶段:认证系统 🔄
|
||||
- [ ] JWT认证中间件
|
||||
- [ ] osu! OAuth集成(含refresh token存储)
|
||||
- [ ] 用户注册/登录流程
|
||||
- [ ] 权限中间件(基于UserGroup)
|
||||
|
||||
### 第三阶段:核心CRUD
|
||||
- [ ] User API完整实现
|
||||
- [ ] 管理员设置功能
|
||||
- [ ] 系统初始化逻辑
|
||||
- [ ] TournamentSettings API
|
||||
|
||||
### 第四阶段:其他Entity CRUD
|
||||
- [ ] MatchRooms API
|
||||
- [ ] MatchSchedules API
|
||||
- [ ] PlayerMatchups API
|
||||
- [ ] MapSelections API
|
||||
- [ ] MapComments API
|
||||
- [ ] Messages API
|
||||
|
||||
### 第五阶段:文档和优化
|
||||
- [ ] OpenAPI文档生成
|
||||
- [ ] 测试编写
|
||||
- [ ] 性能优化
|
||||
- [ ] 部署配置
|
||||
|
||||
## 详细任务分解
|
||||
|
||||
### 第一阶段任务
|
||||
1. **目录结构创建**
|
||||
- [x] 创建 `src/api/` 目录和模块
|
||||
- [x] 创建 `src/service/` 目录和模块
|
||||
- [x] 创建 `src/dto/` 目录和模块
|
||||
- [x] 创建 `src/error/` 目录和模块
|
||||
- [x] 创建 `src/config/` 目录和模块
|
||||
- [x] 创建 `src/middleware/` 目录和模块
|
||||
- [x] 创建 `src/utils/` 目录和模块
|
||||
|
||||
2. **数据库连接**
|
||||
- [x] 实现Database配置
|
||||
- [x] 创建连接池管理
|
||||
- [x] 添加数据库迁移支持
|
||||
|
||||
3. **错误处理**
|
||||
- [x] 定义AppError枚举
|
||||
- [x] 实现From trait用于错误转换
|
||||
- [x] 创建统一的错误响应格式
|
||||
|
||||
4. **配置管理**
|
||||
- [x] 从.env读取配置
|
||||
- [x] 创建Config结构体
|
||||
- [x] 实现配置验证
|
||||
|
||||
### 第二阶段任务
|
||||
1. **JWT认证**
|
||||
- [ ] 创建JWT工具函数
|
||||
- [ ] 实现Token生成和验证
|
||||
- [ ] 创建认证中间件
|
||||
|
||||
2. **osu! OAuth集成**
|
||||
- [ ] 添加OAuth相关字段到User entity
|
||||
- [ ] 实现OAuth授权流程
|
||||
- [ ] 处理OAuth回调
|
||||
- [ ] 存储access/refresh token
|
||||
|
||||
3. **权限管理**
|
||||
- [ ] 创建权限检查中间件
|
||||
- [ ] 实现基于UserGroup的权限控制
|
||||
- [ ] 管理员权限特殊处理
|
||||
|
||||
### 第三阶段任务
|
||||
1. **User API**
|
||||
- [ ] 创建User DTOs
|
||||
- [ ] 实现UserService
|
||||
- [ ] 创建User API路由
|
||||
- [ ] 添加数据验证
|
||||
|
||||
2. **系统初始化**
|
||||
- [ ] 检查tournament_settings表
|
||||
- [ ] 创建默认比赛设置
|
||||
- [ ] 管理员用户初始化逻辑
|
||||
|
||||
3. **TournamentSettings API**
|
||||
- [ ] 创建TournamentSettings DTOs
|
||||
- [ ] 实现TournamentSettingsService
|
||||
- [ ] 创建API路由
|
||||
|
||||
### 第四阶段任务
|
||||
1. **MatchRooms API**
|
||||
- [ ] 创建MatchRooms DTOs
|
||||
- [ ] 实现MatchRoomsService
|
||||
- [ ] 创建API路由
|
||||
|
||||
2. **其他Entity API**(类似模式)
|
||||
- [ ] MatchSchedules
|
||||
- [ ] PlayerMatchups
|
||||
- [ ] MapSelections
|
||||
- [ ] MapComments
|
||||
- [ ] Messages
|
||||
|
||||
### 第五阶段任务
|
||||
1. **文档生成**
|
||||
- [ ] 集成utoipa
|
||||
- [ ] 为所有API添加OpenAPI注解
|
||||
- [ ] 设置Swagger UI路由
|
||||
|
||||
2. **测试**
|
||||
- [ ] 单元测试
|
||||
- [ ] 集成测试
|
||||
- [ ] API测试
|
||||
|
||||
3. **优化**
|
||||
- [ ] 性能优化
|
||||
- [ ] 错误处理优化
|
||||
- [ ] 日志优化
|
||||
|
||||
## 数据库变更
|
||||
|
||||
### User表新增字段
|
||||
```sql
|
||||
ALTER TABLE user
|
||||
ADD COLUMN osu_access_token VARCHAR(512) NULL,
|
||||
ADD COLUMN osu_refresh_token VARCHAR(512) NULL,
|
||||
ADD COLUMN osu_token_expires_at TIMESTAMP NULL;
|
||||
```
|
||||
|
||||
## API端点设计
|
||||
|
||||
### 认证相关
|
||||
- `GET /api/auth/osu` - 重定向到osu! OAuth
|
||||
- `GET /api/auth/callback/osu` - OAuth回调
|
||||
- `POST /api/auth/refresh` - 刷新JWT token
|
||||
- `POST /api/auth/logout` - 登出
|
||||
|
||||
### 用户管理
|
||||
- `GET /api/users` - 获取用户列表(管理员)
|
||||
- `GET /api/users/{id}` - 获取用户详情
|
||||
- `PUT /api/users/{id}` - 更新用户信息
|
||||
- `PUT /api/users/{id}/user-group` - 修改用户组(管理员)
|
||||
- `DELETE /api/users/{id}` - 删除用户(管理员)
|
||||
|
||||
### 比赛管理
|
||||
- `GET /api/match-rooms` - 获取比赛房间列表
|
||||
- `POST /api/match-rooms` - 创建比赛房间
|
||||
- `GET /api/match-rooms/{id}` - 获取比赛房间详情
|
||||
- `PUT /api/match-rooms/{id}` - 更新比赛房间
|
||||
- `DELETE /api/match-rooms/{id}` - 删除比赛房间
|
||||
|
||||
## 权限规则
|
||||
|
||||
### 用户角色
|
||||
1. **玩家 (Player)**
|
||||
- 查看公开信息
|
||||
- 管理自己的数据
|
||||
- 参与比赛
|
||||
|
||||
2. **管理员 (Admin)**
|
||||
- 所有玩家权限
|
||||
- 管理所有用户
|
||||
- 修改系统设置
|
||||
- 管理比赛
|
||||
|
||||
### 权限矩阵
|
||||
| 操作 | 玩家 | 管理员 |
|
||||
|------|------|--------|
|
||||
| 查看用户列表 | ❌ | ✅ |
|
||||
| 修改用户组 | ❌ | ✅ |
|
||||
| 创建比赛房间 | ✅ | ✅ |
|
||||
| 删除比赛房间 | 仅自己 | ✅ |
|
||||
| 修改系统设置 | ❌ | ✅ |
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
1. **安全性**
|
||||
- 所有密码/Token加密存储
|
||||
- JWT secret从环境变量读取
|
||||
- 输入验证和清理
|
||||
- SQL注入防护
|
||||
|
||||
2. **性能**
|
||||
- 数据库连接池
|
||||
- 查询优化
|
||||
- 缓存策略
|
||||
|
||||
3. **可维护性**
|
||||
- 清晰的错误信息
|
||||
- 完整的日志记录
|
||||
- 统一的代码风格
|
||||
- 详细的API文档
|
||||
|
||||
25
src/api/auth.rs
Normal file
25
src/api/auth.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new()
|
||||
.route("/osu", get(osu_auth))
|
||||
.route("/callback/osu", get(osu_callback))
|
||||
.route("/refresh", get(refresh_token))
|
||||
.route("/logout", get(logout))
|
||||
}
|
||||
|
||||
async fn osu_auth() -> &'static str {
|
||||
"osu! OAuth 认证"
|
||||
}
|
||||
|
||||
async fn osu_callback() -> &'static str {
|
||||
"osu! OAuth 回调"
|
||||
}
|
||||
|
||||
async fn refresh_token() -> &'static str {
|
||||
"刷新Token"
|
||||
}
|
||||
|
||||
async fn logout() -> &'static str {
|
||||
"登出"
|
||||
}
|
||||
0
src/api/map_comments.rs
Normal file
0
src/api/map_comments.rs
Normal file
0
src/api/map_selections.rs
Normal file
0
src/api/map_selections.rs
Normal file
0
src/api/match_rooms.rs
Normal file
0
src/api/match_rooms.rs
Normal file
0
src/api/match_schedules.rs
Normal file
0
src/api/match_schedules.rs
Normal file
0
src/api/messages.rs
Normal file
0
src/api/messages.rs
Normal file
45
src/api/mod.rs
Normal file
45
src/api/mod.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
pub mod auth;
|
||||
pub mod user;
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
use crate::service::ServiceState;
|
||||
|
||||
pub fn configure_routes() -> Router<ServiceState> {
|
||||
Router::new()
|
||||
// Swagger UI文档
|
||||
.merge(SwaggerUi::new("/api/docs").url("/api-docs/openapi.json", ApiDoc::openapi()))
|
||||
.route("/health", get(|| async { "OK" }))
|
||||
.nest("/api/auth", auth::routes())
|
||||
.nest("/api/users", user::routes())
|
||||
.with_state(())
|
||||
}
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
user::get_users,
|
||||
user::get_user,
|
||||
user::update_user,
|
||||
user::update_user_group,
|
||||
user::delete_user,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
crate::dto::user::UserResponse,
|
||||
crate::dto::user::UpdateUserRequest,
|
||||
crate::dto::user::UpdateUserGroupRequest,
|
||||
crate::dto::user::UserQueryParams,
|
||||
crate::dto::ApiResponse<crate::dto::user::UserResponse>,
|
||||
crate::dto::PaginatedResponse<crate::dto::user::UserResponse>,
|
||||
user::PaginatedUserResponse,
|
||||
)
|
||||
),
|
||||
tags(
|
||||
(name = "auth", description = "认证相关API"),
|
||||
(name = "users", description = "用户管理API"),
|
||||
)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
0
src/api/player_matchups.rs
Normal file
0
src/api/player_matchups.rs
Normal file
0
src/api/tournament_settings.rs
Normal file
0
src/api/tournament_settings.rs
Normal file
183
src/api/user.rs
Normal file
183
src/api/user.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query, State},
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
dto::{
|
||||
ApiResponse, PaginatedResponse,
|
||||
user::{
|
||||
CreateUserRequest, UpdateUserGroupRequest, UpdateUserRequest, UserQueryParams,
|
||||
UserResponse,
|
||||
},
|
||||
},
|
||||
error::Result,
|
||||
service::{ServiceState, user::UserService},
|
||||
};
|
||||
|
||||
pub fn routes() -> Router<ServiceState> {
|
||||
Router::new()
|
||||
.route("/", get(get_users))
|
||||
.route("/", post(create_user))
|
||||
.route("/{id}", get(get_user))
|
||||
.route("/{id}", put(update_user))
|
||||
.route("/{id}", delete(delete_user))
|
||||
.route("/{id}/user-group", put(update_user_group))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/users",
|
||||
params(UserQueryParams),
|
||||
responses(
|
||||
(status = 200, description = "获取用户列表成功", body = PaginatedUserResponse),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn get_users(
|
||||
State(state): State<ServiceState>,
|
||||
Query(params): Query<UserQueryParams>,
|
||||
) -> Result<Json<ApiResponse<PaginatedResponse<UserResponse>>>> {
|
||||
let service = UserService::new(state.db);
|
||||
let (users, total) = service.find_all(params).await?;
|
||||
|
||||
let response = PaginatedResponse::new(
|
||||
users.into_iter().map(UserResponse::from).collect(),
|
||||
total,
|
||||
1,
|
||||
20,
|
||||
);
|
||||
|
||||
Ok(Json(ApiResponse::success(response)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/users/{id}",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "获取用户成功", body = UserResponse),
|
||||
(status = 404, description = "用户不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn get_user(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.find_by_id(id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/users",
|
||||
request_body = CreateUserRequest,
|
||||
responses(
|
||||
(status = 201, description = "创建用户成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn create_user() -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
// 用户创建将通过OAuth回调处理
|
||||
Err(crate::error::AppError::BusinessError(
|
||||
"请通过OAuth注册用户".into(),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
),
|
||||
request_body = UpdateUserRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新用户成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn update_user(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Json(data): Json<UpdateUserRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.update(id, data).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}/user-group",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
),
|
||||
request_body = UpdateUserGroupRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新用户组成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn update_user_group(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Json(data): Json<UpdateUserGroupRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.update_user_group(id, data).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/users/{id}",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "删除用户成功"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn delete_user(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<ApiResponse<()>>> {
|
||||
let service = UserService::new(state.db);
|
||||
service.delete(id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success_with_message((), "用户删除成功")))
|
||||
}
|
||||
|
||||
#[derive(ToSchema)]
|
||||
pub struct PaginatedUserResponse {
|
||||
items: Vec<UserResponse>,
|
||||
total: u64,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
total_pages: u64,
|
||||
}
|
||||
80
src/config/mod.rs
Normal file
80
src/config/mod.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::env;
|
||||
|
||||
use serde::Deserialize;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Validate)]
|
||||
pub struct Config {
|
||||
#[validate(length(min = 1))]
|
||||
pub database_url: String,
|
||||
|
||||
#[validate(range(min = 1, max = 65535))]
|
||||
pub port: u16,
|
||||
|
||||
pub host: String,
|
||||
|
||||
#[validate(length(min = 1))]
|
||||
pub jwt_secret: String,
|
||||
|
||||
pub jwt_expiry_hours: u64,
|
||||
|
||||
pub osu_client_id: String,
|
||||
|
||||
pub osu_client_secret: String,
|
||||
|
||||
pub osu_redirect_uri: String,
|
||||
|
||||
pub cors_origin: String,
|
||||
|
||||
pub log_level: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Result<Self, crate::error::AppError> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let config = Config {
|
||||
database_url: env::var("DATABASE_URL")
|
||||
.map_err(|_| crate::error::AppError::ConfigError("DATABASE_URL未设置".into()))?,
|
||||
|
||||
port: env::var("PORT")
|
||||
.unwrap_or_else(|_| "3000".to_string())
|
||||
.parse()
|
||||
.map_err(|_| crate::error::AppError::ConfigError("PORT格式错误".into()))?,
|
||||
|
||||
host: env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
|
||||
|
||||
jwt_secret: env::var("JWT_SECRET")
|
||||
.map_err(|_| crate::error::AppError::ConfigError("JWT_SECRET未设置".into()))?,
|
||||
|
||||
jwt_expiry_hours: env::var("JWT_EXPIRY_HOURS")
|
||||
.unwrap_or_else(|_| "24".to_string())
|
||||
.parse()
|
||||
.map_err(|_| {
|
||||
crate::error::AppError::ConfigError("JWT_EXPIRY_HOURS格式错误".into())
|
||||
})?,
|
||||
|
||||
osu_client_id: env::var("OSU_CLIENT_ID")
|
||||
.map_err(|_| crate::error::AppError::ConfigError("OSU_CLIENT_ID未设置".into()))?,
|
||||
|
||||
osu_client_secret: env::var("OSU_CLIENT_SECRET").map_err(|_| {
|
||||
crate::error::AppError::ConfigError("OSU_CLIENT_SECRET未设置".into())
|
||||
})?,
|
||||
|
||||
osu_redirect_uri: env::var("OSU_REDIRECT_URI").map_err(|_| {
|
||||
crate::error::AppError::ConfigError("OSU_REDIRECT_URI未设置".into())
|
||||
})?,
|
||||
|
||||
cors_origin: env::var("CORS_ORIGIN")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string()),
|
||||
|
||||
log_level: env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()),
|
||||
};
|
||||
|
||||
config
|
||||
.validate()
|
||||
.map_err(|e| crate::error::AppError::ConfigError(format!("配置验证失败: {}", e)))?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
0
src/dto/auth.rs
Normal file
0
src/dto/auth.rs
Normal file
0
src/dto/map_comments.rs
Normal file
0
src/dto/map_comments.rs
Normal file
0
src/dto/map_selections.rs
Normal file
0
src/dto/map_selections.rs
Normal file
0
src/dto/match_rooms.rs
Normal file
0
src/dto/match_rooms.rs
Normal file
0
src/dto/match_schedules.rs
Normal file
0
src/dto/match_schedules.rs
Normal file
0
src/dto/messages.rs
Normal file
0
src/dto/messages.rs
Normal file
95
src/dto/mod.rs
Normal file
95
src/dto/mod.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
pub mod auth;
|
||||
pub mod map_comments;
|
||||
pub mod map_selections;
|
||||
pub mod match_rooms;
|
||||
pub mod match_schedules;
|
||||
pub mod messages;
|
||||
pub mod player_matchups;
|
||||
pub mod tournament_settings;
|
||||
pub mod user;
|
||||
|
||||
/// 通用响应结构
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct ApiResponse<T: utoipa::ToSchema> {
|
||||
pub success: bool,
|
||||
pub data: Option<T>,
|
||||
pub message: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl<T: utoipa::ToSchema> ApiResponse<T> {
|
||||
pub fn success(data: T) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
data: Some(data),
|
||||
message: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success_with_message(data: T, message: &str) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
data: Some(data),
|
||||
message: Some(message.to_string()),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(error: &str) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
data: None,
|
||||
message: None,
|
||||
error: Some(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error_with_message(error: &str, message: &str) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
data: None,
|
||||
message: Some(message.to_string()),
|
||||
error: Some(error.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 分页请求参数
|
||||
#[derive(Debug, serde::Deserialize, utoipa::IntoParams)]
|
||||
pub struct PaginationParams {
|
||||
#[serde(default = "default_page")]
|
||||
pub page: u64,
|
||||
#[serde(default = "default_page_size")]
|
||||
pub page_size: u64,
|
||||
}
|
||||
|
||||
fn default_page() -> u64 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_page_size() -> u64 {
|
||||
20
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct PaginatedResponse<T: utoipa::ToSchema> {
|
||||
pub items: Vec<T>,
|
||||
pub total: u64,
|
||||
pub page: u64,
|
||||
pub page_size: u64,
|
||||
pub total_pages: u64,
|
||||
}
|
||||
|
||||
impl<T: utoipa::ToSchema> 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 {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
page_size,
|
||||
total_pages,
|
||||
}
|
||||
}
|
||||
}
|
||||
0
src/dto/player_matchups.rs
Normal file
0
src/dto/player_matchups.rs
Normal file
0
src/dto/tournament_settings.rs
Normal file
0
src/dto/tournament_settings.rs
Normal file
119
src/dto/user.rs
Normal file
119
src/dto/user.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::entity::user::Model as UserModel;
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateUserRequest {
|
||||
#[validate(length(min = 3, max = 50))]
|
||||
pub username: String,
|
||||
|
||||
pub avatar_url: Option<String>,
|
||||
|
||||
pub pp: Option<f32>,
|
||||
|
||||
pub global_rank: Option<i32>,
|
||||
|
||||
pub country: Option<String>,
|
||||
|
||||
pub country_rank: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateUserRequest {
|
||||
#[validate(length(min = 3, max = 50))]
|
||||
pub username: Option<String>,
|
||||
|
||||
pub avatar_url: Option<String>,
|
||||
|
||||
pub pp: Option<f32>,
|
||||
|
||||
pub global_rank: Option<i32>,
|
||||
|
||||
pub country: Option<String>,
|
||||
|
||||
pub country_rank: Option<i32>,
|
||||
|
||||
pub approved: Option<i32>,
|
||||
|
||||
pub season: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct UserResponse {
|
||||
pub id: i32,
|
||||
pub osu_id: String,
|
||||
pub username: String,
|
||||
pub registered_at: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub pp: Option<f32>,
|
||||
pub global_rank: Option<i32>,
|
||||
pub country: Option<String>,
|
||||
pub country_rank: Option<i32>,
|
||||
pub approved: Option<i32>,
|
||||
pub user_group: String,
|
||||
pub season: Option<i32>,
|
||||
pub gu_server_user_id: Option<i32>,
|
||||
pub gu_server_username: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<UserModel> for UserResponse {
|
||||
fn from(user: UserModel) -> Self {
|
||||
Self {
|
||||
id: user.id,
|
||||
osu_id: user.osu_id,
|
||||
username: user.username,
|
||||
registered_at: user.registered_at.to_rfc3339(),
|
||||
avatar_url: user.avatar_url,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country: user.country,
|
||||
country_rank: user.country_rank,
|
||||
approved: user.approved,
|
||||
user_group: format!("{:?}", user.user_group),
|
||||
season: user.season,
|
||||
gu_server_user_id: user.gu_server_user_id,
|
||||
gu_server_username: user.gu_server_username,
|
||||
created_at: user.created_at.to_rfc3339(),
|
||||
updated_at: user.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateUserGroupRequest {
|
||||
pub user_group: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, utoipa::IntoParams)]
|
||||
pub struct UserQueryParams {
|
||||
pub username: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub user_group: Option<String>,
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct OsuUserData {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub statistics: Option<OsuStatistics>,
|
||||
pub country: Option<OsuCountry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct OsuStatistics {
|
||||
pub pp: Option<f32>,
|
||||
pub global_rank: Option<i32>,
|
||||
pub country_rank: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct OsuCountry {
|
||||
pub code: String,
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "map_comments")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(column_name = "mapSelectionId")]
|
||||
pub map_selection_id: i32,
|
||||
#[sea_orm(column_name = "osuId")]
|
||||
pub osu_id: String,
|
||||
pub username: String,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub comment: Option<String>,
|
||||
#[sea_orm(column_name = "createdAt")]
|
||||
pub created_at: Option<DateTimeUtc>,
|
||||
#[sea_orm(column_name = "updatedAt")]
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -1,63 +0,0 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "map_selections")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(column_name = "beatmapId")]
|
||||
pub beatmap_id: i32,
|
||||
#[sea_orm(column_name = "beatmapsetId")]
|
||||
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,
|
||||
#[sea_orm(column_name = "starRating", column_type = "Decimal(Some((5, 2)))")]
|
||||
pub star_rating: Decimal,
|
||||
#[sea_orm(column_type = "Decimal(Some((6, 2)))")]
|
||||
pub bpm: Decimal,
|
||||
#[sea_orm(column_name = "totalLength")]
|
||||
pub total_length: i32,
|
||||
#[sea_orm(column_name = "maxCombo")]
|
||||
pub max_combo: i32,
|
||||
#[sea_orm(column_type = "Decimal(Some((4, 2)))")]
|
||||
pub ar: Decimal,
|
||||
#[sea_orm(column_type = "Decimal(Some((4, 2)))")]
|
||||
pub cs: Decimal,
|
||||
#[sea_orm(column_type = "Decimal(Some((4, 2)))")]
|
||||
pub od: Decimal,
|
||||
#[sea_orm(column_type = "Decimal(Some((4, 2)))")]
|
||||
pub hp: Decimal,
|
||||
#[sea_orm(column_name = "selectedMods")]
|
||||
pub selected_mods: String,
|
||||
#[sea_orm(column_name = "modPosition")]
|
||||
pub mod_position: i32,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub comment: Option<String>,
|
||||
#[sea_orm(column_name = "selectedBy")]
|
||||
pub selected_by: String,
|
||||
#[sea_orm(column_name = "selectedAt")]
|
||||
pub selected_at: DateTime,
|
||||
pub season: String,
|
||||
pub category: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub url: String,
|
||||
#[sea_orm(column_name = "coverUrl", column_type = "Text", nullable)]
|
||||
pub cover_url: Option<String>,
|
||||
pub approved: Option<i8>,
|
||||
pub padding: Option<i8>,
|
||||
#[sea_orm(column_name = "createdAt")]
|
||||
pub created_at: Option<DateTimeUtc>,
|
||||
#[sea_orm(column_name = "updatedAt")]
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
37
src/entity/map_comments.rs
Normal file
37
src/entity/map_comments.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "map_comments")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub map_selection_id: i32,
|
||||
pub osu_id: String,
|
||||
pub username: String,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub comment: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::map_selections::Entity",
|
||||
from = "Column::MapSelectionId",
|
||||
to = "super::map_selections::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
MapSelections,
|
||||
}
|
||||
|
||||
impl Related<super::map_selections::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::MapSelections.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
62
src/entity/map_selections.rs
Normal file
62
src/entity/map_selections.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "map_selections")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
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,
|
||||
#[sea_orm(column_type = "Decimal(Some((10, 0)))")]
|
||||
pub star_rating: Decimal,
|
||||
#[sea_orm(column_type = "Decimal(Some((10, 0)))")]
|
||||
pub bpm: Decimal,
|
||||
pub total_length: i32,
|
||||
pub max_combo: i32,
|
||||
#[sea_orm(column_type = "Decimal(Some((10, 0)))")]
|
||||
pub ar: Decimal,
|
||||
#[sea_orm(column_type = "Decimal(Some((10, 0)))")]
|
||||
pub cs: Decimal,
|
||||
#[sea_orm(column_type = "Decimal(Some((10, 0)))")]
|
||||
pub od: Decimal,
|
||||
#[sea_orm(column_type = "Decimal(Some((10, 0)))")]
|
||||
pub hp: Decimal,
|
||||
pub selected_mods: String,
|
||||
pub mod_position: i32,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub comment: Option<String>,
|
||||
pub selected_by: String,
|
||||
pub selected_at: DateTimeUtc,
|
||||
pub season: String,
|
||||
pub category: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub url: String,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub cover_url: Option<String>,
|
||||
pub approved: Option<i32>,
|
||||
pub padding: Option<i32>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::map_comments::Entity")]
|
||||
MapComments,
|
||||
}
|
||||
|
||||
impl Related<super::map_comments::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::MapComments.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -1,9 +1,9 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use super::sea_orm_active_enums::Status;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "match_rooms")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
@@ -16,8 +16,8 @@ pub struct Model {
|
||||
pub max_participants: Option<i32>,
|
||||
pub status: Option<Status>,
|
||||
pub created_by: String,
|
||||
pub created_at: Option<DateTimeUtc>,
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -1,18 +1,16 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use super::sea_orm_active_enums::Status;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "match_schedules")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub room_id: i32,
|
||||
#[sea_orm(column_name = "player1_osuId")]
|
||||
pub player1_osu_id: String,
|
||||
pub player1_username: String,
|
||||
#[sea_orm(column_name = "player2_osuId")]
|
||||
pub player2_osu_id: String,
|
||||
pub player2_username: String,
|
||||
pub red_score: Option<i32>,
|
||||
@@ -23,8 +21,8 @@ pub struct Model {
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub match_link: Option<String>,
|
||||
pub created_by: String,
|
||||
pub created_at: Option<DateTimeUtc>,
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -34,7 +32,7 @@ pub enum Relation {
|
||||
from = "Column::RoomId",
|
||||
to = "super::match_rooms::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
MatchRooms,
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use super::sea_orm_active_enums::Status;
|
||||
use super::sea_orm_active_enums::Type;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "messages")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(column_name = "sender_osuId")]
|
||||
pub sender_osu_id: String,
|
||||
pub sender_username: String,
|
||||
#[sea_orm(column_name = "receiver_osuId")]
|
||||
pub receiver_osu_id: String,
|
||||
pub receiver_username: String,
|
||||
pub r#type: Option<Type>,
|
||||
@@ -22,9 +20,9 @@ pub struct Model {
|
||||
pub related_matchup_id: Option<i32>,
|
||||
pub status: Option<Status>,
|
||||
pub response_action: Option<String>,
|
||||
pub response_time: Option<DateTime>,
|
||||
pub created_at: Option<DateTimeUtc>,
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
pub response_time: Option<DateTimeUtc>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -1,4 +1,4 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use super::sea_orm_active_enums::Status;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "player_matchups")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(column_name = "player1_osuId")]
|
||||
pub player1_osu_id: String,
|
||||
pub player1_username: String,
|
||||
#[sea_orm(column_name = "player2_osuId")]
|
||||
pub player2_osu_id: String,
|
||||
pub player2_username: String,
|
||||
pub status: Option<Status>,
|
||||
pub created_by: String,
|
||||
pub created_at: Option<DateTimeUtc>,
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -1,4 +1,4 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
pub use super::map_comments::Entity as MapComments;
|
||||
pub use super::map_selections::Entity as MapSelections;
|
||||
@@ -1,4 +1,4 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
@@ -23,7 +23,7 @@ pub enum Type {
|
||||
System,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "userGroup")]
|
||||
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "user_group")]
|
||||
pub enum UserGroup {
|
||||
#[sea_orm(string_value = "player")]
|
||||
Player,
|
||||
@@ -1,4 +1,4 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
@@ -14,9 +14,9 @@ pub struct Model {
|
||||
pub min_pp_for_registration: Option<f32>,
|
||||
pub current_season: Option<String>,
|
||||
pub current_season_stage: Option<String>,
|
||||
pub mappool_visible: Option<i8>,
|
||||
pub created_at: Option<DateTimeUtc>,
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
pub mappool_visible: Option<i32>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -1,4 +1,4 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||
|
||||
use super::sea_orm_active_enums::UserGroup;
|
||||
use sea_orm::entity::prelude::*;
|
||||
@@ -8,26 +8,24 @@ use sea_orm::entity::prelude::*;
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(column_name = "osuId", unique)]
|
||||
#[sea_orm(unique)]
|
||||
pub osu_id: String,
|
||||
#[sea_orm(unique)]
|
||||
pub username: String,
|
||||
#[sea_orm(column_name = "registeredAt")]
|
||||
pub registered_at: DateTime,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub registered_at: DateTimeUtc,
|
||||
pub avatar_url: Option<String>,
|
||||
#[sea_orm(column_type = "Float", nullable)]
|
||||
pub pp: Option<f32>,
|
||||
pub global_rank: Option<i32>,
|
||||
pub country: Option<String>,
|
||||
pub country_rank: Option<i32>,
|
||||
pub approved: Option<i8>,
|
||||
#[sea_orm(column_name = "userGroup")]
|
||||
pub approved: Option<i32>,
|
||||
pub user_group: UserGroup,
|
||||
pub season: Option<i32>,
|
||||
#[sea_orm(column_name = "createdAt")]
|
||||
pub created_at: Option<DateTimeUtc>,
|
||||
#[sea_orm(column_name = "updatedAt")]
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
pub gu_server_user_id: Option<i32>,
|
||||
pub gu_server_username: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
105
src/error/mod.rs
Normal file
105
src/error/mod.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use sea_orm::DbErr;
|
||||
use serde_json::json;
|
||||
use thiserror::Error;
|
||||
|
||||
/// 应用程序错误枚举
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("数据库错误: {0}")]
|
||||
DatabaseError(#[from] DbErr),
|
||||
|
||||
#[error("未找到资源: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("认证失败: {0}")]
|
||||
Unauthorized(String),
|
||||
|
||||
#[error("权限不足: {0}")]
|
||||
Forbidden(String),
|
||||
|
||||
#[error("验证失败: {0}")]
|
||||
ValidationError(String),
|
||||
|
||||
#[error("业务错误: {0}")]
|
||||
BusinessError(String),
|
||||
|
||||
#[error("配置错误: {0}")]
|
||||
ConfigError(String),
|
||||
|
||||
#[error("外部API错误: {0}")]
|
||||
ExternalApiError(String),
|
||||
|
||||
#[error("内部服务器错误: {0}")]
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
AppError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
|
||||
AppError::Forbidden(_) => StatusCode::FORBIDDEN,
|
||||
AppError::ValidationError(_) => StatusCode::BAD_REQUEST,
|
||||
AppError::BusinessError(_) => StatusCode::BAD_REQUEST,
|
||||
AppError::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AppError::ExternalApiError(_) => StatusCode::BAD_GATEWAY,
|
||||
AppError::DatabaseError(_) | AppError::InternalError(_) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error_message(&self) -> String {
|
||||
match self {
|
||||
AppError::DatabaseError(err) => format!("数据库错误: {}", err),
|
||||
AppError::NotFound(msg) => format!("未找到资源: {}", msg),
|
||||
AppError::Unauthorized(msg) => format!("认证失败: {}", msg),
|
||||
AppError::Forbidden(msg) => format!("权限不足: {}", msg),
|
||||
AppError::ValidationError(msg) => format!("验证失败: {}", msg),
|
||||
AppError::BusinessError(msg) => format!("业务错误: {}", msg),
|
||||
AppError::ConfigError(msg) => format!("配置错误: {}", msg),
|
||||
AppError::ExternalApiError(msg) => format!("外部API错误: {}", msg),
|
||||
AppError::InternalError(msg) => format!("内部服务器错误: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let status = self.status_code();
|
||||
let error_response = json!({
|
||||
"success": false,
|
||||
"error": self.error_message(),
|
||||
"status_code": status.as_u16(),
|
||||
});
|
||||
|
||||
(status, Json(error_response)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AppError>;
|
||||
|
||||
pub fn not_found(msg: &str) -> AppError {
|
||||
AppError::NotFound(msg.to_string())
|
||||
}
|
||||
|
||||
pub fn unauthorized(msg: &str) -> AppError {
|
||||
AppError::Unauthorized(msg.to_string())
|
||||
}
|
||||
|
||||
pub fn forbidden(msg: &str) -> AppError {
|
||||
AppError::Forbidden(msg.to_string())
|
||||
}
|
||||
|
||||
pub fn validation_error(msg: &str) -> AppError {
|
||||
AppError::ValidationError(msg.to_string())
|
||||
}
|
||||
|
||||
pub fn business_error(msg: &str) -> AppError {
|
||||
AppError::BusinessError(msg.to_string())
|
||||
}
|
||||
94
src/main.rs
94
src/main.rs
@@ -1,12 +1,90 @@
|
||||
use axum::{Router, routing::get};
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use config::Config;
|
||||
use sea_orm::Database;
|
||||
use service::ServiceState;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
mod api;
|
||||
mod config;
|
||||
mod dto;
|
||||
mod entity;
|
||||
mod error;
|
||||
mod middleware;
|
||||
mod service;
|
||||
mod utils;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// build our application with a single route
|
||||
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 初始化日志
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(
|
||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
|
||||
))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
// run our app with hyper, listening globally on port 3000
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
println!("Server listening on http://localhost:3000");
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
// 加载配置
|
||||
let config = Config::from_env()?;
|
||||
|
||||
print_banner();
|
||||
println!("启动 AstraCup 后端服务...");
|
||||
println!("配置加载成功");
|
||||
println!("监听地址: {}:{}", config.host, config.port);
|
||||
println!("数据库: {}", mask_database_url(&config.database_url));
|
||||
|
||||
println!("连接数据库...");
|
||||
let db = Database::connect(&config.database_url).await?;
|
||||
println!("数据库连接成功");
|
||||
|
||||
// 创建服务状态
|
||||
let service_state = Arc::new(ServiceState::new(db));
|
||||
|
||||
// 配置应用
|
||||
let app = api::configure_routes()
|
||||
.layer(middleware::configure_middleware())
|
||||
.with_state(service_state);
|
||||
|
||||
// 启动服务器
|
||||
let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.host, config.port))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("服务器启动成功");
|
||||
println!("API文档: http://{}:{}/api/docs", config.host, config.port);
|
||||
println!("健康检查: http://{}:{}/health", config.host, config.port);
|
||||
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_banner() {
|
||||
println!(r" _ _ ____ ");
|
||||
println!(r" / \ ___| |_ __ _ _ __ / ____ _ _ __ ");
|
||||
println!(r" / _ \ / __| __/ _` | '__| | | | | | '_ \ ");
|
||||
println!(r" / ___ \\__ \ || (_| | | | |__| |_| | |_) |");
|
||||
println!(r" /_/ \_\___/\__\__,_|_| \____\__,_| .__/ ");
|
||||
println!(r" |_| ");
|
||||
println!(r"==============================================");
|
||||
}
|
||||
|
||||
fn mask_database_url(url: &str) -> String {
|
||||
if let Some(pos) = url.find('@') {
|
||||
let mut masked = url.to_string();
|
||||
if let Some(start) = url.find("://") {
|
||||
let end = pos;
|
||||
if end > start + 3 {
|
||||
let credentials = &url[start + 3..end];
|
||||
if let Some(colon_pos) = credentials.find(':') {
|
||||
let username = &credentials[..colon_pos];
|
||||
masked.replace_range(start + 3..end, &format!("{}:******", username));
|
||||
}
|
||||
}
|
||||
}
|
||||
masked
|
||||
} else {
|
||||
url.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
0
src/middleware/auth.rs
Normal file
0
src/middleware/auth.rs
Normal file
8
src/middleware/mod.rs
Normal file
8
src/middleware/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub mod auth;
|
||||
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
pub fn configure_middleware() -> impl tower::Layer<axum::Router> + Clone {
|
||||
ServiceBuilder::new().layer(TraceLayer::new_for_http())
|
||||
}
|
||||
0
src/service/auth.rs
Normal file
0
src/service/auth.rs
Normal file
0
src/service/map_comments.rs
Normal file
0
src/service/map_comments.rs
Normal file
0
src/service/map_selections.rs
Normal file
0
src/service/map_selections.rs
Normal file
0
src/service/match_rooms.rs
Normal file
0
src/service/match_rooms.rs
Normal file
0
src/service/match_schedules.rs
Normal file
0
src/service/match_schedules.rs
Normal file
0
src/service/match_schedulesrs
Normal file
0
src/service/match_schedulesrs
Normal file
0
src/service/messages.rs
Normal file
0
src/service/messages.rs
Normal file
24
src/service/mod.rs
Normal file
24
src/service/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
pub mod auth;
|
||||
pub mod map_comments;
|
||||
pub mod map_selections;
|
||||
pub mod match_rooms;
|
||||
pub mod match_schedules;
|
||||
pub mod messages;
|
||||
pub mod player_matchups;
|
||||
pub mod tournament_settings;
|
||||
pub mod user;
|
||||
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
/// 服务层共享状态
|
||||
#[derive(Clone)]
|
||||
pub struct ServiceState {
|
||||
pub db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl ServiceState {
|
||||
/// 创建新的服务状态
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
}
|
||||
0
src/service/player_matchups.rs
Normal file
0
src/service/player_matchups.rs
Normal file
0
src/service/tournament_settings.rs
Normal file
0
src/service/tournament_settings.rs
Normal file
166
src/service/user.rs
Normal file
166
src/service/user.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::user::{CreateUserRequest, UpdateUserGroupRequest, UpdateUserRequest, UserQueryParams},
|
||||
entity::{sea_orm_active_enums::UserGroup, user},
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
pub struct UserService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl UserService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(&self, params: UserQueryParams) -> Result<(Vec<user::Model>, u64)> {
|
||||
let mut query = user::Entity::find();
|
||||
|
||||
if let Some(username) = params.username {
|
||||
query = query.filter(user::Column::Username.contains(username));
|
||||
}
|
||||
|
||||
if let Some(country) = params.country {
|
||||
query = query.filter(user::Column::Country.eq(country));
|
||||
}
|
||||
|
||||
if let Some(user_group) = params.user_group {
|
||||
let group_enum = match user_group.as_str() {
|
||||
"player" => UserGroup::Player,
|
||||
"admin" => UserGroup::Admin,
|
||||
_ => return Err(AppError::ValidationError("无效的用户组".into())),
|
||||
};
|
||||
query = query.filter(user::Column::UserGroup.eq(group_enum));
|
||||
}
|
||||
|
||||
query = query.order_by_asc(user::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 users = query
|
||||
.paginate(&self.db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await?;
|
||||
|
||||
Ok((users, total))
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: i32) -> Result<user::Model> {
|
||||
user::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("用户 {} 不存在", id)))
|
||||
}
|
||||
|
||||
pub async fn find_by_osu_id(&self, osu_id: &str) -> Result<Option<user::Model>> {
|
||||
Ok(user::Entity::find()
|
||||
.filter(user::Column::OsuId.eq(osu_id))
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn create(&self, osu_id: String, data: CreateUserRequest) -> Result<user::Model> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let user = user::ActiveModel {
|
||||
osu_id: Set(osu_id),
|
||||
username: Set(data.username),
|
||||
registered_at: Set(now),
|
||||
avatar_url: Set(data.avatar_url),
|
||||
pp: Set(data.pp),
|
||||
global_rank: Set(data.global_rank),
|
||||
country: Set(data.country),
|
||||
country_rank: Set(data.country_rank),
|
||||
approved: Set(None),
|
||||
user_group: Set(UserGroup::Player),
|
||||
season: Set(None),
|
||||
gu_server_user_id: Set(None),
|
||||
gu_server_username: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(user.insert(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: i32, data: UpdateUserRequest) -> Result<user::Model> {
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
|
||||
if let Some(username) = data.username {
|
||||
user.username = Set(username);
|
||||
}
|
||||
|
||||
if let Some(avatar_url) = data.avatar_url {
|
||||
user.avatar_url = Set(Some(avatar_url));
|
||||
}
|
||||
|
||||
if let Some(pp) = data.pp {
|
||||
user.pp = Set(Some(pp));
|
||||
}
|
||||
|
||||
if let Some(global_rank) = data.global_rank {
|
||||
user.global_rank = Set(Some(global_rank));
|
||||
}
|
||||
|
||||
if let Some(country) = data.country {
|
||||
user.country = Set(Some(country));
|
||||
}
|
||||
|
||||
if let Some(country_rank) = data.country_rank {
|
||||
user.country_rank = Set(Some(country_rank));
|
||||
}
|
||||
|
||||
if let Some(approved) = data.approved {
|
||||
user.approved = Set(Some(approved));
|
||||
}
|
||||
|
||||
if let Some(season) = data.season {
|
||||
user.season = Set(Some(season));
|
||||
}
|
||||
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update_user_group(
|
||||
&self,
|
||||
id: i32,
|
||||
data: UpdateUserGroupRequest,
|
||||
) -> Result<user::Model> {
|
||||
let group_enum = match data.user_group.as_str() {
|
||||
"player" => UserGroup::Player,
|
||||
"admin" => UserGroup::Admin,
|
||||
_ => return Err(AppError::ValidationError("无效的用户组".into())),
|
||||
};
|
||||
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.user_group = Set(group_enum);
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: i32) -> Result<()> {
|
||||
let result = user::Entity::delete_by_id(id).exec(&self.db).await?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AppError::NotFound(format!("用户 {} 不存在", id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn count_admins(&self) -> Result<u64> {
|
||||
Ok(user::Entity::find()
|
||||
.filter(user::Column::UserGroup.eq(UserGroup::Admin))
|
||||
.count(&self.db)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
0
src/utils/jwt.rs
Normal file
0
src/utils/jwt.rs
Normal file
3
src/utils/mod.rs
Normal file
3
src/utils/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod jwt;
|
||||
pub mod password;
|
||||
pub mod validation;
|
||||
0
src/utils/password.rs
Normal file
0
src/utils/password.rs
Normal file
0
src/utils/validation.rs
Normal file
0
src/utils/validation.rs
Normal file
Reference in New Issue
Block a user