diff --git a/.env b/.env new file mode 100644 index 0000000..56dee4f --- /dev/null +++ b/.env @@ -0,0 +1,22 @@ +# Database Configuration +DATABASE_URL=mysql://root:114514@localhost:3306/lbw + +# OAuth Configuration +OSU_CLIENT_ID = 47367 +OSU_CLIENT_SECRET = gZ1JunP2o3ZYDkGCrZOQ2dZGeo9YecL6O2k8pO81 +OSU_REDIRECT_URI = http://localhost:3000/api/auth/callback/osu + +# JWT Configuration +JWT_SECRET=457ceac798afbc2e305c4833844a2736 +JWT_EXPIRY_HOURS=24 + +# Server Configuration +PORT=3001 +HOST=0.0.0.0 +LOG_LEVEL=info + +# CORS Configuration +CORS_ORIGIN=http://localhost:3000 + +# Redis Configuration (Optional) +REDIS_URL=redis://localhost:6379 \ No newline at end of file diff --git a/.env.example b/.env.example index 2d79173..59ae220 100644 --- a/.env.example +++ b/.env.example @@ -1,22 +1,22 @@ -# Database Configuration -DATABASE_URL=mysql://username:password@localhost:3306/astracup - -# OAuth Configuration -OSU_CLIENT_ID=your_osu_client_id -OSU_CLIENT_SECRET=your_osu_client_secret -OSU_REDIRECT_URI=http://localhost:3000/api/auth/callback/osu - -# JWT Configuration -JWT_SECRET=your_jwt_secret_key_here_change_this_to_something_secure -JWT_EXPIRY_HOURS=24 - -# Server Configuration -PORT=3001 -HOST=0.0.0.0 -LOG_LEVEL=info - -# CORS Configuration -CORS_ORIGIN=http://localhost:3000 - -# Redis Configuration (Optional) +# Database Configuration +DATABASE_URL=mysql://username:password@localhost:3306/astracup + +# OAuth Configuration +OSU_CLIENT_ID=your_osu_client_id +OSU_CLIENT_SECRET=your_osu_client_secret +OSU_REDIRECT_URI=http://localhost:3000/api/auth/callback/osu + +# JWT Configuration +JWT_SECRET=your_jwt_secret_key_here_change_this_to_something_secure +JWT_EXPIRY_HOURS=24 + +# Server Configuration +PORT=3001 +HOST=0.0.0.0 +LOG_LEVEL=info + +# CORS Configuration +CORS_ORIGIN=http://localhost:3000 + +# Redis Configuration (Optional) REDIS_URL=redis://localhost:6379 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 12aeedd..b217cbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,7 +86,8 @@ version = "0.1.0" dependencies = [ "anyhow", "argon2", - "axum", + "axum 0.7.9", + "axum-extra", "bcrypt", "chrono", "config", @@ -171,7 +172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "axum-macros", "bytes", "futures-util", @@ -181,7 +182,7 @@ dependencies = [ "hyper 1.8.1", "hyper-util", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", @@ -199,6 +200,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core 0.5.6", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper 1.0.2", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-core" version = "0.4.5" @@ -220,6 +247,46 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" +dependencies = [ + "axum 0.8.8", + "axum-core 0.5.6", + "bytes", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-macros" version = "0.4.2" @@ -1481,6 +1548,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" diff --git a/Cargo.toml b/Cargo.toml index a50c532..69b3732 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ validator = { version = "0.16", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1.0", features = ["v4", "serde"] } futures = "0.3" +axum-extra = "0.12.5" [dev-dependencies] # Testing @@ -59,7 +60,7 @@ mockall = "0.11" sqlx = { version = "0.7", features = ["mysql", "runtime-tokio-native-tls"] } # SeaORM CLI for code generation -sea-orm-cli = { version = "0.12", features = ["sqlx-mysql"] } +# sea-orm-cli = { version = "0.12.15", features = ["sqlx-mysql"] } [profile.dev] opt-level = 0 @@ -68,4 +69,4 @@ debug = true [profile.release] opt-level = 3 lto = true -codegen-units = 1 \ No newline at end of file +codegen-units = 1 diff --git a/README.md b/README.md index 865b853..47234ea 100644 --- a/README.md +++ b/README.md @@ -1,289 +1,289 @@ -# AstraCup Backend - -基于Rust + Axum + SeaORM的osu!比赛管理后端系统。 - -## 功能特性 - -- ✅ osu! OAuth2认证 -- ✅ JWT令牌管理 -- ✅ MySQL数据库集成 -- ✅ RESTful API设计 -- ✅ 错误处理中间件 -- ✅ CORS支持 -- ✅ 结构化日志 - -## 技术栈 - -- **Web框架**: Axum 0.7 -- **ORM**: SeaORM 0.12 -- **数据库**: MySQL 8.0+ -- **认证**: JWT + osu! OAuth2 -- **序列化**: serde + serde_json -- **配置管理**: dotenv -- **日志**: tracing + tracing-subscriber -- **HTTP客户端**: reqwest - -## 项目结构 - -``` -astracup-backend/ -├── src/ -│ ├── main.rs # 应用入口点 -│ ├── config/ # 配置管理 -│ │ ├── mod.rs -│ │ └── settings.rs -│ ├── database/ # 数据库连接 -│ │ ├── mod.rs -│ │ └── connection.rs -│ ├── entities/ # SeaORM实体(待生成) -│ ├── migrations/ # 数据库迁移(待生成) -│ ├── services/ # 业务逻辑层 -│ │ └── auth/ # 认证服务 -│ ├── handlers/ # 请求处理器 -│ │ └── auth/ # 认证处理器 -│ ├── routes/ # 路由定义 -│ │ ├── mod.rs -│ │ └── api.rs -│ ├── utils/ # 工具函数 -│ │ ├── mod.rs -│ │ ├── error.rs # 错误处理 -│ │ └── response.rs # 响应格式 -│ └── middleware/ # 中间件(待实现) -├── .env.example # 环境变量示例 -├── Cargo.toml # 依赖配置 -└── README.md # 项目说明 -``` - -## 快速开始 - -### 1. 环境要求 - -- Rust 1.70+ ([安装指南](https://www.rust-lang.org/tools/install)) -- MySQL 8.0+ -- osu! API客户端ID和密钥 - -### 2. 配置环境变量 - -复制环境变量模板: - -```bash -cp .env.example .env -``` - -编辑 `.env` 文件: - -```env -# 数据库配置 -DATABASE_URL=mysql://username:password@localhost:3306/astracup - -# osu! OAuth配置 -OSU_CLIENT_ID=your_client_id -OSU_CLIENT_SECRET=your_client_secret -OSU_REDIRECT_URI=http://localhost:3000/api/auth/callback/osu - -# JWT配置 -JWT_SECRET=your_jwt_secret_key_here_change_this_to_something_secure -JWT_EXPIRY_HOURS=24 - -# 服务器配置 -PORT=3001 -HOST=0.0.0.0 -LOG_LEVEL=info - -# CORS配置 -CORS_ORIGIN=http://localhost:3000 -``` - -### 3. 初始化数据库 - -确保MySQL服务运行,并创建数据库: - -```sql -CREATE DATABASE IF NOT EXISTS astracup CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -``` - -### 4. 构建和运行 - -```bash -# 安装依赖 -cargo build - -# 运行开发服务器 -cargo run - -# 或使用cargo watch自动重启 -cargo install cargo-watch -cargo watch -x run -``` - -### 5. 测试API - -服务器启动后,可以测试以下端点: - -```bash -# 获取osu!认证URL -curl http://localhost:3001/api/auth/url - -# 验证服务器状态 -curl http://localhost:3001/health -``` - -## API文档 - -### 认证相关 - -#### 获取osu!认证URL -``` -GET /api/auth/url -``` - -响应: -```json -{ - "success": true, - "auth_url": "https://osu.ppy.sh/oauth/authorize?client_id=..." -} -``` - -#### osu!认证回调 -``` -GET /api/auth/callback/osu?code={authorization_code} -``` - -响应: -```json -{ - "success": true, - "token": "jwt_token_here", - "user": { - "id": 123456, - "username": "PlayerName", - "avatar_url": "https://...", - "cover_url": "https://...", - "country_code": "CN", - "statistics": { - "pp": 4500.5, - "global_rank": 1000, - "country_rank": 50 - } - } -} -``` - -#### 登出 -``` -POST /api/auth/logout -``` - -响应: -```json -{ - "success": true, - "message": "Logout successful. Please remove the token on the client side." -} -``` - -#### 验证令牌 -``` -GET /api/auth/verify -Authorization: Bearer {jwt_token} -``` - -响应: -```json -{ - "success": true, - "valid": true, - "user_id": "123456", - "username": "PlayerName" -} -``` - -## 开发指南 - -### 添加新的API端点 - -1. 在 `services/` 目录下创建业务逻辑 -2. 在 `handlers/` 目录下创建请求处理器 -3. 在 `routes/api.rs` 中注册路由 - -### 数据库操作 - -使用SeaORM进行数据库操作: - -```rust -use sea_orm::{EntityTrait, QueryFilter, ColumnTrait}; - -// 查询示例 -let users = User::find() - .filter(UserColumn::Username.contains("test")) - .all(&db.connection) - .await?; -``` - -### 错误处理 - -使用 `AppError` 枚举处理错误: - -```rust -use crate::utils::AppError; - -async fn some_handler() -> Result, AppError> { - // 业务逻辑 - if something_wrong { - return Err(AppError::NotFound("Resource not found".to_string())); - } - Ok(Json(response)) -} -``` - -## 部署 - -### 生产环境构建 - -```bash -# 发布构建 -cargo build --release - -# 运行生产服务器 -./target/release/astracup-backend -``` - -### Docker部署 - -```dockerfile -FROM rust:1.70 as builder -WORKDIR /usr/src/app -COPY . . -RUN cargo build --release - -FROM debian:bullseye-slim -RUN apt-get update && apt-get install -y libssl-dev && rm -rf /var/lib/apt/lists/* -COPY --from=builder /usr/src/app/target/release/astracup-backend /usr/local/bin/ -COPY .env.production .env -EXPOSE 3001 -CMD ["astracup-backend"] -``` - -## 迁移计划 - -本项目是从现有的Next.js API后端迁移而来。完整的迁移计划包括: - -1. ✅ 项目初始化和基础架构 -2. ⏳ 数据库模型和迁移 -3. ⏳ 核心业务逻辑实现 -4. ⏳ 剩余API端点迁移 -5. ⏳ 测试和优化 -6. ⏳ 部署和文档 - -## 贡献指南 - -1. Fork 项目 -2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 开启 Pull Request - -## 许可证 - +# AstraCup Backend + +基于Rust + Axum + SeaORM的osu!比赛管理后端系统。 + +## 功能特性 + +- ✅ osu! OAuth2认证 +- ✅ JWT令牌管理 +- ✅ MySQL数据库集成 +- ✅ RESTful API设计 +- ✅ 错误处理中间件 +- ✅ CORS支持 +- ✅ 结构化日志 + +## 技术栈 + +- **Web框架**: Axum 0.7 +- **ORM**: SeaORM 0.12 +- **数据库**: MySQL 8.0+ +- **认证**: JWT + osu! OAuth2 +- **序列化**: serde + serde_json +- **配置管理**: dotenv +- **日志**: tracing + tracing-subscriber +- **HTTP客户端**: reqwest + +## 项目结构 + +``` +astracup-backend/ +├── src/ +│ ├── main.rs # 应用入口点 +│ ├── config/ # 配置管理 +│ │ ├── mod.rs +│ │ └── settings.rs +│ ├── database/ # 数据库连接 +│ │ ├── mod.rs +│ │ └── connection.rs +│ ├── entities/ # SeaORM实体(待生成) +│ ├── migrations/ # 数据库迁移(待生成) +│ ├── services/ # 业务逻辑层 +│ │ └── auth/ # 认证服务 +│ ├── handlers/ # 请求处理器 +│ │ └── auth/ # 认证处理器 +│ ├── routes/ # 路由定义 +│ │ ├── mod.rs +│ │ └── api.rs +│ ├── utils/ # 工具函数 +│ │ ├── mod.rs +│ │ ├── error.rs # 错误处理 +│ │ └── response.rs # 响应格式 +│ └── middleware/ # 中间件(待实现) +├── .env.example # 环境变量示例 +├── Cargo.toml # 依赖配置 +└── README.md # 项目说明 +``` + +## 快速开始 + +### 1. 环境要求 + +- Rust 1.70+ ([安装指南](https://www.rust-lang.org/tools/install)) +- MySQL 8.0+ +- osu! API客户端ID和密钥 + +### 2. 配置环境变量 + +复制环境变量模板: + +```bash +cp .env.example .env +``` + +编辑 `.env` 文件: + +```env +# 数据库配置 +DATABASE_URL=mysql://username:password@localhost:3306/astracup + +# osu! OAuth配置 +OSU_CLIENT_ID=your_client_id +OSU_CLIENT_SECRET=your_client_secret +OSU_REDIRECT_URI=http://localhost:3000/api/auth/callback/osu + +# JWT配置 +JWT_SECRET=your_jwt_secret_key_here_change_this_to_something_secure +JWT_EXPIRY_HOURS=24 + +# 服务器配置 +PORT=3001 +HOST=0.0.0.0 +LOG_LEVEL=info + +# CORS配置 +CORS_ORIGIN=http://localhost:3000 +``` + +### 3. 初始化数据库 + +确保MySQL服务运行,并创建数据库: + +```sql +CREATE DATABASE IF NOT EXISTS astracup CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +### 4. 构建和运行 + +```bash +# 安装依赖 +cargo build + +# 运行开发服务器 +cargo run + +# 或使用cargo watch自动重启 +cargo install cargo-watch +cargo watch -x run +``` + +### 5. 测试API + +服务器启动后,可以测试以下端点: + +```bash +# 获取osu!认证URL +curl http://localhost:3001/api/auth/url + +# 验证服务器状态 +curl http://localhost:3001/health +``` + +## API文档 + +### 认证相关 + +#### 获取osu!认证URL +``` +GET /api/auth/url +``` + +响应: +```json +{ + "success": true, + "auth_url": "https://osu.ppy.sh/oauth/authorize?client_id=..." +} +``` + +#### osu!认证回调 +``` +GET /api/auth/callback/osu?code={authorization_code} +``` + +响应: +```json +{ + "success": true, + "token": "jwt_token_here", + "user": { + "id": 123456, + "username": "PlayerName", + "avatar_url": "https://...", + "cover_url": "https://...", + "country_code": "CN", + "statistics": { + "pp": 4500.5, + "global_rank": 1000, + "country_rank": 50 + } + } +} +``` + +#### 登出 +``` +POST /api/auth/logout +``` + +响应: +```json +{ + "success": true, + "message": "Logout successful. Please remove the token on the client side." +} +``` + +#### 验证令牌 +``` +GET /api/auth/verify +Authorization: Bearer {jwt_token} +``` + +响应: +```json +{ + "success": true, + "valid": true, + "user_id": "123456", + "username": "PlayerName" +} +``` + +## 开发指南 + +### 添加新的API端点 + +1. 在 `services/` 目录下创建业务逻辑 +2. 在 `handlers/` 目录下创建请求处理器 +3. 在 `routes/api.rs` 中注册路由 + +### 数据库操作 + +使用SeaORM进行数据库操作: + +```rust +use sea_orm::{EntityTrait, QueryFilter, ColumnTrait}; + +// 查询示例 +let users = User::find() + .filter(UserColumn::Username.contains("test")) + .all(&db.connection) + .await?; +``` + +### 错误处理 + +使用 `AppError` 枚举处理错误: + +```rust +use crate::utils::AppError; + +async fn some_handler() -> Result, AppError> { + // 业务逻辑 + if something_wrong { + return Err(AppError::NotFound("Resource not found".to_string())); + } + Ok(Json(response)) +} +``` + +## 部署 + +### 生产环境构建 + +```bash +# 发布构建 +cargo build --release + +# 运行生产服务器 +./target/release/astracup-backend +``` + +### Docker部署 + +```dockerfile +FROM rust:1.70 as builder +WORKDIR /usr/src/app +COPY . . +RUN cargo build --release + +FROM debian:bullseye-slim +RUN apt-get update && apt-get install -y libssl-dev && rm -rf /var/lib/apt/lists/* +COPY --from=builder /usr/src/app/target/release/astracup-backend /usr/local/bin/ +COPY .env.production .env +EXPOSE 3001 +CMD ["astracup-backend"] +``` + +## 迁移计划 + +本项目是从现有的Next.js API后端迁移而来。完整的迁移计划包括: + +1. ✅ 项目初始化和基础架构 +2. ⏳ 数据库模型和迁移 +3. ⏳ 核心业务逻辑实现 +4. ⏳ 剩余API端点迁移 +5. ⏳ 测试和优化 +6. ⏳ 部署和文档 + +## 贡献指南 + +1. Fork 项目 +2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 开启 Pull Request + +## 许可证 + MIT License - 详见 LICENSE 文件 \ No newline at end of file diff --git a/RUST_MIGRATION_PLAN.md b/RUST_MIGRATION_PLAN.md index f5d57df..caa3007 100644 --- a/RUST_MIGRATION_PLAN.md +++ b/RUST_MIGRATION_PLAN.md @@ -1,304 +1,304 @@ -# AstraCup Web 后端迁移计划 - -## 项目概述 -将现有的Next.js API后端迁移到Rust + Axum + SeaORM架构。 - -## 现有API分析 - -### 主要功能模块 -1. **认证模块** - osu! OAuth2认证 -2. **报名管理** - 玩家报名、审核、管理 -3. **比赛房间** - 比赛房间创建、管理 -4. **比赛预约** - 比赛时间安排 -5. **玩家对战** - 对战列表管理 -6. **消息通知** - 系统消息通知 -7. **比赛设置** - 锦标赛配置 -8. **分数管理** - 比赛分数保存 -9. **文件处理** - 谱面下载、回放处理 - -### 数据库表结构 -基于 `src/lib/mysql-registrations.ts` 分析: - -1. **registrations** - 玩家报名表 -2. **match_rooms** - 比赛房间表 -3. **match_schedules** - 比赛预约表 -4. **player_matchups** - 玩家对战表 -5. **messages** - 消息通知表 -6. **staff_room_assignments** - Staff分配表 -7. **tournament_settings** - 比赛设置表 -8. **match_scores** - 比赛分数表 - -## Rust后端架构设计 - -### 技术栈 -- **Web框架**: Axum -- **ORM**: SeaORM -- **数据库**: MySQL (与现有兼容) -- **认证**: JWT + osu! OAuth2 -- **配置管理**: dotenv + config -- **日志**: tracing -- **序列化**: serde -- **验证**: validator - -### 项目结构 -``` -astracup-backend/ -├── Cargo.toml -├── .env.example -├── .gitignore -├── README.md -├── src/ -│ ├── main.rs -│ ├── config/ -│ │ ├── mod.rs -│ │ └── settings.rs -│ ├── database/ -│ │ ├── mod.rs -│ │ ├── connection.rs -│ │ └── models/ -│ │ ├── mod.rs -│ │ ├── registration.rs -│ │ ├── match_room.rs -│ │ ├── match_schedule.rs -│ │ ├── player_matchup.rs -│ │ ├── message.rs -│ │ ├── staff_assignment.rs -│ │ ├── tournament_setting.rs -│ │ └── match_score.rs -│ ├── entities/ -│ │ ├── mod.rs -│ │ └── *.rs (SeaORM生成的实体) -│ ├── migrations/ -│ │ ├── mod.rs -│ │ └── *.rs (数据库迁移文件) -│ ├── services/ -│ │ ├── mod.rs -│ │ ├── auth/ -│ │ │ ├── mod.rs -│ │ │ ├── service.rs -│ │ │ └── middleware.rs -│ │ ├── registration/ -│ │ ├── match_room/ -│ │ ├── match_schedule/ -│ │ ├── player_matchup/ -│ │ ├── message/ -│ │ ├── staff_assignment/ -│ │ ├── tournament_setting/ -│ │ └── match_score/ -│ ├── handlers/ -│ │ ├── mod.rs -│ │ ├── auth/ -│ │ ├── registration/ -│ │ ├── match_room/ -│ │ ├── match_schedule/ -│ │ ├── player_matchup/ -│ │ ├── message/ -│ │ ├── staff_assignment/ -│ │ ├── tournament_setting/ -│ │ └── match_score/ -│ ├── routes/ -│ │ ├── mod.rs -│ │ └── api.rs -│ ├── utils/ -│ │ ├── mod.rs -│ │ ├── error.rs -│ │ ├── response.rs -│ │ └── validation.rs -│ └── middleware/ -│ ├── mod.rs -│ ├── auth.rs -│ └── logging.rs -└── tests/ - ├── integration/ - └── unit/ -``` - -### API端点映射 - -#### 认证相关 -- `GET /api/auth/url` → `GET /api/auth/url` -- `GET /api/auth/callback/osu` → `GET /api/auth/callback/osu` -- `POST /api/auth/logout` → `POST /api/auth/logout` - -#### 报名管理 -- `GET /api/register` → `GET /api/registrations` -- `POST /api/register` → `POST /api/registrations` -- `GET /api/approved-players` → `GET /api/registrations/approved` -- `POST /api/admin/approve-registration` → `POST /api/admin/registrations/{id}/approve` -- `POST /api/admin/delete-registration` → `DELETE /api/admin/registrations/{id}` - -#### 比赛房间 -- `GET /api/match-rooms` → `GET /api/match-rooms` -- `POST /api/match-rooms/create` → `POST /api/match-rooms` -- `DELETE /api/match-rooms/delete` → `DELETE /api/match-rooms/{id}` - -#### 比赛预约 -- `GET /api/match-schedules` → `GET /api/match-schedules` -- `POST /api/match-schedules/create` → `POST /api/match-schedules` -- `PUT /api/match-schedules/update` → `PUT /api/match-schedules/{id}` -- `PUT /api/match-schedules/update-details` → `PATCH /api/match-schedules/{id}/details` - -#### 玩家对战 -- `GET /api/player-matchups` → `GET /api/player-matchups` -- `POST /api/player-matchups/create` → `POST /api/player-matchups` -- `DELETE /api/player-matchups/delete` → `DELETE /api/player-matchups/{id}` - -#### 消息通知 -- `GET /api/messages` → `GET /api/messages` -- `POST /api/messages/send` → `POST /api/messages` - -#### Staff分配 -- `GET /api/staff-room-assignments` → `GET /api/staff-assignments` -- `GET /api/available-rooms-for-staff` → `GET /api/staff-assignments/available-rooms` -- `POST /api/staff-room-assignments` → `POST /api/staff-assignments` -- `PUT /api/staff-room-assignments/{id}` → `PUT /api/staff-assignments/{id}` - -#### 比赛设置 -- `GET /api/tournament-settings` → `GET /api/tournament-settings` -- `PUT /api/tournament-settings` → `PUT /api/tournament-settings` - -#### 分数管理 -- `POST /api/match-scores/save` → `POST /api/match-scores` -- `PUT /api/match-scores/update` → `PUT /api/match-scores/{id}` -- `GET /api/multiplayer-scores` → `GET /api/match-scores` - -## 迁移步骤 - -### 第一阶段:项目初始化 (已完成) -1. ✅ 创建Rust项目结构 -2. ✅ 配置Cargo.toml依赖 -3. ✅ 设置数据库连接和SeaORM -4. ✅ 创建环境配置 - -### 第二阶段:数据库模型和迁移 (已完成) -1. ✅ 根据现有表结构创建SeaORM实体 -2. ✅ 编写数据库迁移脚本 -3. ✅ 创建模型定义和关系 -4. ✅ 测试数据库连接和操作 - -### 第三阶段:核心服务层 (已完成) -1. ✅ 实现认证服务(osu! OAuth2集成) -2. ✅ 实现报名管理服务 -3. ✅ 实现比赛房间服务 -4. ✅ 实现消息通知服务 -5. ✅ 实现比赛设置服务 - -### 第四阶段:API路由和处理器 (已完成) -1. ✅ 实现Axum路由配置 -2. ✅ 创建请求/响应DTO -3. ✅ 实现错误处理中间件 -4. ✅ 实现认证中间件 - -### 第五阶段:剩余功能模块 (进行中) -1. ⏳ 比赛预约服务 -2. ⏳ 玩家对战管理 -3. ⏳ 分数管理系统 -4. ⏳ 文件处理模块 - -### 第六阶段:测试和优化 (待开始) -1. ⏳ 编写单元测试 -2. ⏳ 编写集成测试 -3. ⏳ 性能优化 -4. ⏳ 错误处理完善 - -### 第七阶段:部署和文档 (待开始) -1. ⏳ 创建部署配置 -2. ⏳ 编写部署文档 -3. ⏳ 创建API文档 -4. ⏳ 迁移指南 - -## 当前完成状态 - -### 已完成的模块 -1. ✅ **认证模块** - osu! OAuth2认证、JWT令牌管理 -2. ✅ **报名管理** - 玩家报名、审核、管理 -3. ✅ **比赛房间** - 比赛房间创建、管理 -4. ✅ **消息通知** - 系统消息通知 -5. ✅ **比赛设置** - 锦标赛配置 -6. ✅ **用户管理** - 用户信息管理 -7. ✅ **团队管理** - 团队信息管理 - -### 待完成的模块 -1. ⏳ **比赛预约** - 比赛时间安排 -2. ⏳ **玩家对战** - 对战列表管理 -3. ⏳ **分数管理** - 比赛分数保存 -4. ⏳ **文件处理** - 谱面下载、回放处理 - -### API端点完成情况 -- ✅ 认证相关API (4个端点) -- ✅ 用户管理API (4个端点) -- ✅ 报名管理API (5个端点) -- ✅ 比赛房间API (6个端点) -- ✅ 消息通知API (7个端点) -- ✅ 比赛设置API (7个端点) -- ✅ 团队管理API (3个端点) -- ⏳ 比赛预约API (待实现) -- ⏳ 玩家对战API (待实现) -- ⏳ 分数管理API (待实现) - -**总计:已完成 36个API端点,待完成 约15个API端点** - -## 技术细节 - -### 数据库连接池 -使用SeaORM的连接池,配置与现有MySQL兼容: -```rust -let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); -let pool = Database::connect(&database_url).await?; -``` - -### 认证流程 -1. 前端调用 `/api/auth/url` 获取osu! OAuth2授权URL -2. 用户授权后重定向到 `/api/auth/callback/osu` -3. 后端使用授权码交换访问令牌 -4. 获取用户信息并创建JWT -5. 返回JWT给前端用于后续请求 - -### 错误处理 -统一错误响应格式: -```json -{ - "success": false, - "error": "错误消息", - "code": "ERROR_CODE" -} -``` - -### 环境变量 -``` -DATABASE_URL=mysql://user:password@localhost:3306/astracup -OSU_CLIENT_ID=your_client_id -OSU_CLIENT_SECRET=your_client_secret -JWT_SECRET=your_jwt_secret -REDIS_URL=redis://localhost:6379 (可选,用于会话缓存) -``` - -## 兼容性考虑 - -### 数据迁移 -1. 保持现有数据库表结构不变 -2. 新Rust后端直接使用现有数据库 -3. 逐步迁移API端点,保持前端兼容 - -### API兼容性 -1. 保持相同的请求/响应格式 -2. 保持相同的错误码 -3. 逐步替换,可以并行运行 - -## 性能优势 -1. **内存效率**: Rust无GC,内存占用更低 -2. **并发性能**: 异步处理,高并发支持 -3. **启动速度**: 编译为原生二进制,启动更快 -4. **资源消耗**: CPU和内存使用更少 - -## 风险评估 -1. **学习曲线**: Rust有一定学习成本 -2. **迁移时间**: 需要2-3周完成迁移 -3. **兼容性问题**: 需要确保API完全兼容 -4. **测试覆盖**: 需要充分测试确保稳定性 - -## 下一步行动 -1. 创建Rust项目 -2. 配置基础依赖 -3. 创建数据库模型 +# AstraCup Web 后端迁移计划 + +## 项目概述 +将现有的Next.js API后端迁移到Rust + Axum + SeaORM架构。 + +## 现有API分析 + +### 主要功能模块 +1. **认证模块** - osu! OAuth2认证 +2. **报名管理** - 玩家报名、审核、管理 +3. **比赛房间** - 比赛房间创建、管理 +4. **比赛预约** - 比赛时间安排 +5. **玩家对战** - 对战列表管理 +6. **消息通知** - 系统消息通知 +7. **比赛设置** - 锦标赛配置 +8. **分数管理** - 比赛分数保存 +9. **文件处理** - 谱面下载、回放处理 + +### 数据库表结构 +基于 `src/lib/mysql-registrations.ts` 分析: + +1. **registrations** - 玩家报名表 +2. **match_rooms** - 比赛房间表 +3. **match_schedules** - 比赛预约表 +4. **player_matchups** - 玩家对战表 +5. **messages** - 消息通知表 +6. **staff_room_assignments** - Staff分配表 +7. **tournament_settings** - 比赛设置表 +8. **match_scores** - 比赛分数表 + +## Rust后端架构设计 + +### 技术栈 +- **Web框架**: Axum +- **ORM**: SeaORM +- **数据库**: MySQL (与现有兼容) +- **认证**: JWT + osu! OAuth2 +- **配置管理**: dotenv + config +- **日志**: tracing +- **序列化**: serde +- **验证**: validator + +### 项目结构 +``` +astracup-backend/ +├── Cargo.toml +├── .env.example +├── .gitignore +├── README.md +├── src/ +│ ├── main.rs +│ ├── config/ +│ │ ├── mod.rs +│ │ └── settings.rs +│ ├── database/ +│ │ ├── mod.rs +│ │ ├── connection.rs +│ │ └── models/ +│ │ ├── mod.rs +│ │ ├── registration.rs +│ │ ├── match_room.rs +│ │ ├── match_schedule.rs +│ │ ├── player_matchup.rs +│ │ ├── message.rs +│ │ ├── staff_assignment.rs +│ │ ├── tournament_setting.rs +│ │ └── match_score.rs +│ ├── entities/ +│ │ ├── mod.rs +│ │ └── *.rs (SeaORM生成的实体) +│ ├── migrations/ +│ │ ├── mod.rs +│ │ └── *.rs (数据库迁移文件) +│ ├── services/ +│ │ ├── mod.rs +│ │ ├── auth/ +│ │ │ ├── mod.rs +│ │ │ ├── service.rs +│ │ │ └── middleware.rs +│ │ ├── registration/ +│ │ ├── match_room/ +│ │ ├── match_schedule/ +│ │ ├── player_matchup/ +│ │ ├── message/ +│ │ ├── staff_assignment/ +│ │ ├── tournament_setting/ +│ │ └── match_score/ +│ ├── handlers/ +│ │ ├── mod.rs +│ │ ├── auth/ +│ │ ├── registration/ +│ │ ├── match_room/ +│ │ ├── match_schedule/ +│ │ ├── player_matchup/ +│ │ ├── message/ +│ │ ├── staff_assignment/ +│ │ ├── tournament_setting/ +│ │ └── match_score/ +│ ├── routes/ +│ │ ├── mod.rs +│ │ └── api.rs +│ ├── utils/ +│ │ ├── mod.rs +│ │ ├── error.rs +│ │ ├── response.rs +│ │ └── validation.rs +│ └── middleware/ +│ ├── mod.rs +│ ├── auth.rs +│ └── logging.rs +└── tests/ + ├── integration/ + └── unit/ +``` + +### API端点映射 + +#### 认证相关 +- `GET /api/auth/url` → `GET /api/auth/url` +- `GET /api/auth/callback/osu` → `GET /api/auth/callback/osu` +- `POST /api/auth/logout` → `POST /api/auth/logout` + +#### 报名管理 +- `GET /api/register` → `GET /api/registrations` +- `POST /api/register` → `POST /api/registrations` +- `GET /api/approved-players` → `GET /api/registrations/approved` +- `POST /api/admin/approve-registration` → `POST /api/admin/registrations/{id}/approve` +- `POST /api/admin/delete-registration` → `DELETE /api/admin/registrations/{id}` + +#### 比赛房间 +- `GET /api/match-rooms` → `GET /api/match-rooms` +- `POST /api/match-rooms/create` → `POST /api/match-rooms` +- `DELETE /api/match-rooms/delete` → `DELETE /api/match-rooms/{id}` + +#### 比赛预约 +- `GET /api/match-schedules` → `GET /api/match-schedules` +- `POST /api/match-schedules/create` → `POST /api/match-schedules` +- `PUT /api/match-schedules/update` → `PUT /api/match-schedules/{id}` +- `PUT /api/match-schedules/update-details` → `PATCH /api/match-schedules/{id}/details` + +#### 玩家对战 +- `GET /api/player-matchups` → `GET /api/player-matchups` +- `POST /api/player-matchups/create` → `POST /api/player-matchups` +- `DELETE /api/player-matchups/delete` → `DELETE /api/player-matchups/{id}` + +#### 消息通知 +- `GET /api/messages` → `GET /api/messages` +- `POST /api/messages/send` → `POST /api/messages` + +#### Staff分配 +- `GET /api/staff-room-assignments` → `GET /api/staff-assignments` +- `GET /api/available-rooms-for-staff` → `GET /api/staff-assignments/available-rooms` +- `POST /api/staff-room-assignments` → `POST /api/staff-assignments` +- `PUT /api/staff-room-assignments/{id}` → `PUT /api/staff-assignments/{id}` + +#### 比赛设置 +- `GET /api/tournament-settings` → `GET /api/tournament-settings` +- `PUT /api/tournament-settings` → `PUT /api/tournament-settings` + +#### 分数管理 +- `POST /api/match-scores/save` → `POST /api/match-scores` +- `PUT /api/match-scores/update` → `PUT /api/match-scores/{id}` +- `GET /api/multiplayer-scores` → `GET /api/match-scores` + +## 迁移步骤 + +### 第一阶段:项目初始化 (已完成) +1. ✅ 创建Rust项目结构 +2. ✅ 配置Cargo.toml依赖 +3. ✅ 设置数据库连接和SeaORM +4. ✅ 创建环境配置 + +### 第二阶段:数据库模型和迁移 (已完成) +1. ✅ 根据现有表结构创建SeaORM实体 +2. ✅ 编写数据库迁移脚本 +3. ✅ 创建模型定义和关系 +4. ✅ 测试数据库连接和操作 + +### 第三阶段:核心服务层 (已完成) +1. ✅ 实现认证服务(osu! OAuth2集成) +2. ✅ 实现报名管理服务 +3. ✅ 实现比赛房间服务 +4. ✅ 实现消息通知服务 +5. ✅ 实现比赛设置服务 + +### 第四阶段:API路由和处理器 (已完成) +1. ✅ 实现Axum路由配置 +2. ✅ 创建请求/响应DTO +3. ✅ 实现错误处理中间件 +4. ✅ 实现认证中间件 + +### 第五阶段:剩余功能模块 (进行中) +1. ⏳ 比赛预约服务 +2. ⏳ 玩家对战管理 +3. ⏳ 分数管理系统 +4. ⏳ 文件处理模块 + +### 第六阶段:测试和优化 (待开始) +1. ⏳ 编写单元测试 +2. ⏳ 编写集成测试 +3. ⏳ 性能优化 +4. ⏳ 错误处理完善 + +### 第七阶段:部署和文档 (待开始) +1. ⏳ 创建部署配置 +2. ⏳ 编写部署文档 +3. ⏳ 创建API文档 +4. ⏳ 迁移指南 + +## 当前完成状态 + +### 已完成的模块 +1. ✅ **认证模块** - osu! OAuth2认证、JWT令牌管理 +2. ✅ **报名管理** - 玩家报名、审核、管理 +3. ✅ **比赛房间** - 比赛房间创建、管理 +4. ✅ **消息通知** - 系统消息通知 +5. ✅ **比赛设置** - 锦标赛配置 +6. ✅ **用户管理** - 用户信息管理 +7. ✅ **团队管理** - 团队信息管理 + +### 待完成的模块 +1. ⏳ **比赛预约** - 比赛时间安排 +2. ⏳ **玩家对战** - 对战列表管理 +3. ⏳ **分数管理** - 比赛分数保存 +4. ⏳ **文件处理** - 谱面下载、回放处理 + +### API端点完成情况 +- ✅ 认证相关API (4个端点) +- ✅ 用户管理API (4个端点) +- ✅ 报名管理API (5个端点) +- ✅ 比赛房间API (6个端点) +- ✅ 消息通知API (7个端点) +- ✅ 比赛设置API (7个端点) +- ✅ 团队管理API (3个端点) +- ⏳ 比赛预约API (待实现) +- ⏳ 玩家对战API (待实现) +- ⏳ 分数管理API (待实现) + +**总计:已完成 36个API端点,待完成 约15个API端点** + +## 技术细节 + +### 数据库连接池 +使用SeaORM的连接池,配置与现有MySQL兼容: +```rust +let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); +let pool = Database::connect(&database_url).await?; +``` + +### 认证流程 +1. 前端调用 `/api/auth/url` 获取osu! OAuth2授权URL +2. 用户授权后重定向到 `/api/auth/callback/osu` +3. 后端使用授权码交换访问令牌 +4. 获取用户信息并创建JWT +5. 返回JWT给前端用于后续请求 + +### 错误处理 +统一错误响应格式: +```json +{ + "success": false, + "error": "错误消息", + "code": "ERROR_CODE" +} +``` + +### 环境变量 +``` +DATABASE_URL=mysql://user:password@localhost:3306/astracup +OSU_CLIENT_ID=your_client_id +OSU_CLIENT_SECRET=your_client_secret +JWT_SECRET=your_jwt_secret +REDIS_URL=redis://localhost:6379 (可选,用于会话缓存) +``` + +## 兼容性考虑 + +### 数据迁移 +1. 保持现有数据库表结构不变 +2. 新Rust后端直接使用现有数据库 +3. 逐步迁移API端点,保持前端兼容 + +### API兼容性 +1. 保持相同的请求/响应格式 +2. 保持相同的错误码 +3. 逐步替换,可以并行运行 + +## 性能优势 +1. **内存效率**: Rust无GC,内存占用更低 +2. **并发性能**: 异步处理,高并发支持 +3. **启动速度**: 编译为原生二进制,启动更快 +4. **资源消耗**: CPU和内存使用更少 + +## 风险评估 +1. **学习曲线**: Rust有一定学习成本 +2. **迁移时间**: 需要2-3周完成迁移 +3. **兼容性问题**: 需要确保API完全兼容 +4. **测试覆盖**: 需要充分测试确保稳定性 + +## 下一步行动 +1. 创建Rust项目 +2. 配置基础依赖 +3. 创建数据库模型 4. 实现第一个API端点作为示例 \ No newline at end of file diff --git a/migration/Cargo.toml b/migration/Cargo.toml index a5a4466..debe934 100644 --- a/migration/Cargo.toml +++ b/migration/Cargo.toml @@ -1,14 +1,14 @@ -[package] -name = "migration" -version = "0.1.0" -edition = "2024" - -[[bin]] -name = "migration" -path = "src/main.rs" - -[dependencies] -sea-orm-migration = "0.12" -async-std = "1.12" -dotenv = "0.15" +[package] +name = "migration" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "migration" +path = "src/main.rs" + +[dependencies] +sea-orm-migration = "0.12" +async-std = "1.12" +dotenv = "0.15" tokio = { version = "1.0", features = ["full"] } \ No newline at end of file diff --git a/migration/src/lib.rs b/migration/src/lib.rs index e3ff754..c86b1cd 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -1,14 +1,14 @@ -pub use sea_orm_migration::prelude::*; - -mod m20250210_000001_create_tables; - -pub struct Migrator; - -#[async_trait::async_trait] -impl MigratorTrait for Migrator { - fn migrations() -> Vec> { - vec![ - Box::new(m20250210_000001_create_tables::Migration), - ] - } +pub use sea_orm_migration::prelude::*; + +mod m20250210_000001_create_tables; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20250210_000001_create_tables::Migration), + ] + } } \ No newline at end of file diff --git a/migration/src/m20250210_000001_create_tables.rs b/migration/src/m20250210_000001_create_tables.rs index 457d0a9..fa99c2e 100644 --- a/migration/src/m20250210_000001_create_tables.rs +++ b/migration/src/m20250210_000001_create_tables.rs @@ -1,734 +1,734 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - // 创建枚举类型 - manager - .create_type( - Type::create() - .as_enum(BeatmapSelectMod::Table) - .values([ - BeatmapSelectMod::Nm, - BeatmapSelectMod::Hd, - BeatmapSelectMod::Hr, - BeatmapSelectMod::Dt, - BeatmapSelectMod::Lz, - BeatmapSelectMod::Tb, - ]) - .to_owned(), - ) - .await?; - - manager - .create_type( - Type::create() - .as_enum(MultiplayerRoomType::Table) - .values([ - MultiplayerRoomType::Solo, - MultiplayerRoomType::TeamVs, - ]) - .to_owned(), - ) - .await?; - - manager - .create_type( - Type::create() - .as_enum(TeamColor::Table) - .values([ - TeamColor::BlueTeam, - TeamColor::RedTeam, - ]) - .to_owned(), - ) - .await?; - - manager - .create_type( - Type::create() - .as_enum(TeamState::Table) - .values([ - TeamState::Active, - TeamState::Unapproved, - TeamState::Approved, - TeamState::Banned, - ]) - .to_owned(), - ) - .await?; - - manager - .create_type( - Type::create() - .as_enum(UserState::Table) - .values([ - UserState::Active, - UserState::Registered, - UserState::Abandoned, - UserState::Banned, - ]) - .to_owned(), - ) - .await?; - - manager - .create_type( - Type::create() - .as_enum(UserGroupType::Table) - .values([ - UserGroupType::Host, - UserGroupType::Admin, - UserGroupType::Pooler, - UserGroupType::Streamer, - UserGroupType::Tester, - UserGroupType::Gfx, - UserGroupType::Sheeter, - UserGroupType::Commentator, - UserGroupType::Mapper, - UserGroupType::Referee, - UserGroupType::Player, - ]) - .to_owned(), - ) - .await?; - - manager - .create_type( - Type::create() - .as_enum(Season::Table) - .values([ - Season::S1, - Season::S2, - ]) - .to_owned(), - ) - .await?; - - manager - .create_type( - Type::create() - .as_enum(Category::Table) - .values([ - Category::Qua, - Category::Ro16, - Category::Qf, - Category::Sf, - Category::F, - Category::Gf, - ]) - .to_owned(), - ) - .await?; - - // 创建用户表 - manager - .create_table( - Table::create() - .table(User::Table) - .if_not_exists() - .col( - ColumnDef::new(User::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(User::UserState).custom(UserState::Table).not_null().default("ACTIVE")) - .col(ColumnDef::new(User::Osuid).integer().not_null().unique_key()) - .col(ColumnDef::new(User::Username).string().not_null().unique_key()) - .col(ColumnDef::new(User::AvatarUrl).string().null()) - .col(ColumnDef::new(User::CoverUrl).string().null()) - .col(ColumnDef::new(User::CountryCode).string().not_null()) - .col(ColumnDef::new(User::Pp).double().not_null().default(0.0)) - .col(ColumnDef::new(User::GlobalRank).integer().not_null().default(0)) - .col(ColumnDef::new(User::CountryRank).integer().not_null().default(0)) - .col(ColumnDef::new(User::Approved).boolean().not_null().default(false)) - .col(ColumnDef::new(User::Seed).integer().not_null().default(0)) - .col(ColumnDef::new(User::Seasonal).custom(Season::Table).not_null().default("S1")) - .col(ColumnDef::new(User::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .col(ColumnDef::new(User::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .to_owned(), - ) - .await?; - - // 创建队伍表 - manager - .create_table( - Table::create() - .table(Team::Table) - .if_not_exists() - .col( - ColumnDef::new(Team::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Team::Name).string().not_null()) - .col(ColumnDef::new(Team::BannerUrl).string().not_null()) - .col(ColumnDef::new(Team::TeamColor).custom(TeamColor::Table).not_null()) - .col(ColumnDef::new(Team::TeamState).custom(TeamState::Table).not_null().default("ACTIVE")) - .col(ColumnDef::new(Team::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .col(ColumnDef::new(Team::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .to_owned(), - ) - .await?; - - // 创建比赛配置表 - manager - .create_table( - Table::create() - .table(TournamentConfig::Table) - .if_not_exists() - .col( - ColumnDef::new(TournamentConfig::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(TournamentConfig::TournamentName).string().not_null().default("AstarCup")) - .col(ColumnDef::new(TournamentConfig::MaxPpForRegistration).double().not_null().default(0.0)) - .col(ColumnDef::new(TournamentConfig::MinPpForRegistration).double().not_null().default(0.0)) - .col(ColumnDef::new(TournamentConfig::CurrentSeasonal).custom(Season::Table).not_null().default("S1")) - .col(ColumnDef::new(TournamentConfig::CurrentCategory).custom(Category::Table).not_null().default("QUA")) - .col(ColumnDef::new(TournamentConfig::CanRegister).boolean().not_null().default(false)) - .col(ColumnDef::new(TournamentConfig::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .col(ColumnDef::new(TournamentConfig::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .to_owned(), - ) - .await?; - - // 创建多人游戏房间表 - manager - .create_table( - Table::create() - .table(MultiplayerRoom::Table) - .if_not_exists() - .col( - ColumnDef::new(MultiplayerRoom::RoomId) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(MultiplayerRoom::Type).custom(MultiplayerRoomType::Table).not_null().default("SOLO")) - .col(ColumnDef::new(MultiplayerRoom::Season).custom(Season::Table).not_null().default("S1")) - .col(ColumnDef::new(MultiplayerRoom::Category).custom(Category::Table).not_null().default("QUA")) - .col(ColumnDef::new(MultiplayerRoom::MultiplayerSoloRoomRoomId).integer().null()) - .col(ColumnDef::new(MultiplayerRoom::MultiplayerTeamvsRoomRoomId).integer().null()) - .col(ColumnDef::new(MultiplayerRoom::ScoreRed).integer().not_null().default(0)) - .col(ColumnDef::new(MultiplayerRoom::ScoreBlue).integer().not_null().default(0)) - .col(ColumnDef::new(MultiplayerRoom::StartTime).timestamp().not_null()) - .col(ColumnDef::new(MultiplayerRoom::EndTime).timestamp().not_null()) - .col(ColumnDef::new(MultiplayerRoom::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .col(ColumnDef::new(MultiplayerRoom::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .to_owned(), - ) - .await?; - - // 创建单人游戏房间表 - manager - .create_table( - Table::create() - .table(MultiplayerSoloRoom::Table) - .if_not_exists() - .col( - ColumnDef::new(MultiplayerSoloRoom::RoomId) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(MultiplayerSoloRoom::PlayerRedId).integer().not_null().unique_key()) - .col(ColumnDef::new(MultiplayerSoloRoom::PlayerBlueId).integer().not_null().unique_key()) - .to_owned(), - ) - .await?; - - // 创建团队对战房间表 - manager - .create_table( - Table::create() - .table(MultiplayerTeamvsRoom::Table) - .if_not_exists() - .col( - ColumnDef::new(MultiplayerTeamvsRoom::RoomId) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(MultiplayerTeamvsRoom::TeamRedId).integer().not_null().unique_key()) - .col(ColumnDef::new(MultiplayerTeamvsRoom::TeamBlueId).integer().not_null().unique_key()) - .to_owned(), - ) - .await?; - - // 创建消息表 - manager - .create_table( - Table::create() - .table(Message::Table) - .if_not_exists() - .col( - ColumnDef::new(Message::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Message::Title).string().not_null()) - .col(ColumnDef::new(Message::Content).text().not_null()) - .col(ColumnDef::new(Message::SenderId).integer().not_null()) - .col(ColumnDef::new(Message::ReceiverId).integer().not_null()) - .col(ColumnDef::new(Message::RoomId).integer().null()) - .col(ColumnDef::new(Message::IsRead).boolean().not_null().default(false)) - .col(ColumnDef::new(Message::IsDeleted).boolean().not_null().default(false)) - .col(ColumnDef::new(Message::IsSystemMessage).boolean().not_null().default(false)) - .col(ColumnDef::new(Message::IsAccepted).boolean().not_null().default(false)) - .col(ColumnDef::new(Message::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .col(ColumnDef::new(Message::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) - .to_owned(), - ) - .await?; - - // 创建外键约束 - manager - .create_foreign_key( - ForeignKey::create() - .name("fk_multiplayer_room_solo_room") - .from(MultiplayerRoom::Table, MultiplayerRoom::MultiplayerSoloRoomRoomId) - .to(MultiplayerSoloRoom::Table, MultiplayerSoloRoom::RoomId) - .on_delete(ForeignKeyAction::SetNull) - .on_update(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await?; - - manager - .create_foreign_key( - ForeignKey::create() - .name("fk_multiplayer_room_teamvs_room") - .from(MultiplayerRoom::Table, MultiplayerRoom::MultiplayerTeamvsRoomRoomId) - .to(MultiplayerTeamvsRoom::Table, MultiplayerTeamvsRoom::RoomId) - .on_delete(ForeignKeyAction::SetNull) - .on_update(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await?; - - manager - .create_foreign_key( - ForeignKey::create() - .name("fk_multiplayer_solo_room_player_red") - .from(MultiplayerSoloRoom::Table, MultiplayerSoloRoom::PlayerRedId) - .to(User::Table, User::Osuid) - .on_delete(ForeignKeyAction::Restrict) - .on_update(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await?; - - manager - .create_foreign_key( - ForeignKey::create() - .name("fk_multiplayer_solo_room_player_blue") - .from(MultiplayerSoloRoom::Table, MultiplayerSoloRoom::PlayerBlueId) - .to(User::Table, User::Osuid) - .on_delete(ForeignKeyAction::Restrict) - .on_update(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await?; - - manager - .create_foreign_key( - ForeignKey::create() - .name("fk_multiplayer_teamvs_room_team_red") - .from(MultiplayerTeamvsRoom::Table, MultiplayerTeamvsRoom::TeamRedId) - .to(Team::Table, Team::Id) - .on_delete(ForeignKeyAction::Restrict) - .on_update(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await?; - - manager - .create_foreign_key( - ForeignKey::create() - .name("fk_multiplayer_teamvs_room_team_blue") - .from(MultiplayerTeamvsRoom::Table, MultiplayerTeamvsRoom::TeamBlueId) - .to(Team::Table, Team::Id) - .on_delete(ForeignKeyAction::Restrict) - .on_update(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await?; - - manager - .create_foreign_key( - ForeignKey::create() - .name("fk_message_sender") - .from(Message::Table, Message::SenderId) - .to(User::Table, User::Osuid) - .on_delete(ForeignKeyAction::Restrict) - .on_update(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await?; - - manager - .create_foreign_key( - ForeignKey::create() - .name("fk_message_receiver") - .from(Message::Table, Message::ReceiverId) - .to(User::Table, User::Osuid) - .on_delete(ForeignKeyAction::Restrict) - .on_update(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await?; - - manager - .create_foreign_key( - ForeignKey::create() - .name("fk_message_room") - .from(Message::Table, Message::RoomId) - .to(MultiplayerRoom::Table, MultiplayerRoom::RoomId) - .on_delete(ForeignKeyAction::SetNull) - .on_update(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - // 删除外键约束 - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk_message_room") - .table(Message::Table) - .to_owned(), - ) - .await?; - - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk_message_receiver") - .table(Message::Table) - .to_owned(), - ) - .await?; - - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk_message_sender") - .table(Message::Table) - .to_owned(), - ) - .await?; - - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk_multiplayer_teamvs_room_team_blue") - .table(MultiplayerTeamvsRoom::Table) - .to_owned(), - ) - .await?; - - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk_multiplayer_teamvs_room_team_red") - .table(MultiplayerTeamvsRoom::Table) - .to_owned(), - ) - .await?; - - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk_multiplayer_solo_room_player_blue") - .table(MultiplayerSoloRoom::Table) - .to_owned(), - ) - .await?; - - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk_multiplayer_solo_room_player_red") - .table(MultiplayerSoloRoom::Table) - .to_owned(), - ) - .await?; - - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk_multiplayer_room_teamvs_room") - .table(MultiplayerRoom::Table) - .to_owned(), - ) - .await?; - - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk_multiplayer_room_solo_room") - .table(MultiplayerRoom::Table) - .to_owned(), - ) - .await?; - - // 删除表 - manager - .drop_table(Table::drop().table(Message::Table).to_owned()) - .await?; - - manager - .drop_table(Table::drop().table(MultiplayerTeamvsRoom::Table).to_owned()) - .await?; - - manager - .drop_table(Table::drop().table(MultiplayerSoloRoom::Table).to_owned()) - .await?; - - manager - .drop_table(Table::drop().table(MultiplayerRoom::Table).to_owned()) - .await?; - - manager - .drop_table(Table::drop().table(TournamentConfig::Table).to_owned()) - .await?; - - manager - .drop_table(Table::drop().table(Team::Table).to_owned()) - .await?; - - manager - .drop_table(Table::drop().table(User::Table).to_owned()) - .await?; - - // 删除枚举类型 - manager - .drop_type(Type::drop().name(Category::Table).to_owned()) - .await?; - - manager - .drop_type(Type::drop().name(Season::Table).to_owned()) - .await?; - - manager - .drop_type(Type::drop().name(UserGroupType::Table).to_owned()) - .await?; - - manager - .drop_type(Type::drop().name(UserState::Table).to_owned()) - .await?; - - manager - .drop_type(Type::drop().name(TeamState::Table).to_owned()) - .await?; - - manager - .drop_type(Type::drop().name(TeamColor::Table).to_owned()) - .await?; - - manager - .drop_type(Type::drop().name(MultiplayerRoomType::Table).to_owned()) - .await?; - - manager - .drop_type(Type::drop().name(BeatmapSelectMod::Table).to_owned()) - .await?; - - Ok(()) - } -} - -// 定义表名枚举 -#[derive(Iden)] -enum User { - Table, - Id, - UserState, - Osuid, - Username, - AvatarUrl, - CoverUrl, - CountryCode, - Pp, - GlobalRank, - CountryRank, - Approved, - Seed, - Seasonal, - CreatedAt, - UpdatedAt, -} - -#[derive(Iden)] -enum Team { - Table, - Id, - Name, - BannerUrl, - TeamColor, - TeamState, - CreatedAt, - UpdatedAt, -} - -#[derive(Iden)] -enum TournamentConfig { - Table, - Id, - TournamentName, - MaxPpForRegistration, - MinPpForRegistration, - CurrentSeasonal, - CurrentCategory, - CanRegister, - CreatedAt, - UpdatedAt, -} - -#[derive(Iden)] -enum MultiplayerRoom { - Table, - RoomId, - Type, - Season, - Category, - MultiplayerSoloRoomRoomId, - MultiplayerTeamvsRoomRoomId, - ScoreRed, - ScoreBlue, - StartTime, - EndTime, - CreatedAt, - UpdatedAt, -} - -#[derive(Iden)] -enum MultiplayerSoloRoom { - Table, - RoomId, - PlayerRedId, - PlayerBlueId, -} - -#[derive(Iden)] -enum MultiplayerTeamvsRoom { - Table, - RoomId, - TeamRedId, - TeamBlueId, -} - -#[derive(Iden)] -enum Message { - Table, - Id, - Title, - Content, - SenderId, - ReceiverId, - RoomId, - IsRead, - IsDeleted, - IsSystemMessage, - IsAccepted, - CreatedAt, - UpdatedAt, -} - -// 定义枚举类型名 -#[derive(Iden)] -enum BeatmapSelectMod { - Table, - Nm, - Hd, - Hr, - Dt, - Lz, - Tb, -} - -#[derive(Iden)] -enum MultiplayerRoomType { - Table, - Solo, - TeamVs, -} - -#[derive(Iden)] -enum TeamColor { - Table, - BlueTeam, - RedTeam, -} - -#[derive(Iden)] -enum TeamState { - Table, - Active, - Unapproved, - Approved, - Banned, -} - -#[derive(Iden)] -enum UserState { - Table, - Active, - Registered, - Abandoned, - Banned, -} - -#[derive(Iden)] -enum UserGroupType { - Table, - Host, - Admin, - Pooler, - Streamer, - Tester, - Gfx, - Sheeter, - Commentator, - Mapper, - Referee, - Player, -} - -#[derive(Iden)] -enum Season { - Table, - S1, - S2, -} - -#[derive(Iden)] -enum Category { - Table, - Qua, - Ro16, - Qf, - Sf, - F, - Gf, -} +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // 创建枚举类型 + manager + .create_type( + Type::create() + .as_enum(BeatmapSelectMod::Table) + .values([ + BeatmapSelectMod::Nm, + BeatmapSelectMod::Hd, + BeatmapSelectMod::Hr, + BeatmapSelectMod::Dt, + BeatmapSelectMod::Lz, + BeatmapSelectMod::Tb, + ]) + .to_owned(), + ) + .await?; + + manager + .create_type( + Type::create() + .as_enum(MultiplayerRoomType::Table) + .values([ + MultiplayerRoomType::Solo, + MultiplayerRoomType::TeamVs, + ]) + .to_owned(), + ) + .await?; + + manager + .create_type( + Type::create() + .as_enum(TeamColor::Table) + .values([ + TeamColor::BlueTeam, + TeamColor::RedTeam, + ]) + .to_owned(), + ) + .await?; + + manager + .create_type( + Type::create() + .as_enum(TeamState::Table) + .values([ + TeamState::Active, + TeamState::Unapproved, + TeamState::Approved, + TeamState::Banned, + ]) + .to_owned(), + ) + .await?; + + manager + .create_type( + Type::create() + .as_enum(UserState::Table) + .values([ + UserState::Active, + UserState::Registered, + UserState::Abandoned, + UserState::Banned, + ]) + .to_owned(), + ) + .await?; + + manager + .create_type( + Type::create() + .as_enum(UserGroupType::Table) + .values([ + UserGroupType::Host, + UserGroupType::Admin, + UserGroupType::Pooler, + UserGroupType::Streamer, + UserGroupType::Tester, + UserGroupType::Gfx, + UserGroupType::Sheeter, + UserGroupType::Commentator, + UserGroupType::Mapper, + UserGroupType::Referee, + UserGroupType::Player, + ]) + .to_owned(), + ) + .await?; + + manager + .create_type( + Type::create() + .as_enum(Season::Table) + .values([ + Season::S1, + Season::S2, + ]) + .to_owned(), + ) + .await?; + + manager + .create_type( + Type::create() + .as_enum(Category::Table) + .values([ + Category::Qua, + Category::Ro16, + Category::Qf, + Category::Sf, + Category::F, + Category::Gf, + ]) + .to_owned(), + ) + .await?; + + // 创建用户表 + manager + .create_table( + Table::create() + .table(User::Table) + .if_not_exists() + .col( + ColumnDef::new(User::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(User::UserState).custom(UserState::Table).not_null().default("ACTIVE")) + .col(ColumnDef::new(User::Osuid).integer().not_null().unique_key()) + .col(ColumnDef::new(User::Username).string().not_null().unique_key()) + .col(ColumnDef::new(User::AvatarUrl).string().null()) + .col(ColumnDef::new(User::CoverUrl).string().null()) + .col(ColumnDef::new(User::CountryCode).string().not_null()) + .col(ColumnDef::new(User::Pp).double().not_null().default(0.0)) + .col(ColumnDef::new(User::GlobalRank).integer().not_null().default(0)) + .col(ColumnDef::new(User::CountryRank).integer().not_null().default(0)) + .col(ColumnDef::new(User::Approved).boolean().not_null().default(false)) + .col(ColumnDef::new(User::Seed).integer().not_null().default(0)) + .col(ColumnDef::new(User::Seasonal).custom(Season::Table).not_null().default("S1")) + .col(ColumnDef::new(User::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(User::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .to_owned(), + ) + .await?; + + // 创建队伍表 + manager + .create_table( + Table::create() + .table(Team::Table) + .if_not_exists() + .col( + ColumnDef::new(Team::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Team::Name).string().not_null()) + .col(ColumnDef::new(Team::BannerUrl).string().not_null()) + .col(ColumnDef::new(Team::TeamColor).custom(TeamColor::Table).not_null()) + .col(ColumnDef::new(Team::TeamState).custom(TeamState::Table).not_null().default("ACTIVE")) + .col(ColumnDef::new(Team::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(Team::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .to_owned(), + ) + .await?; + + // 创建比赛配置表 + manager + .create_table( + Table::create() + .table(TournamentConfig::Table) + .if_not_exists() + .col( + ColumnDef::new(TournamentConfig::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(TournamentConfig::TournamentName).string().not_null().default("AstarCup")) + .col(ColumnDef::new(TournamentConfig::MaxPpForRegistration).double().not_null().default(0.0)) + .col(ColumnDef::new(TournamentConfig::MinPpForRegistration).double().not_null().default(0.0)) + .col(ColumnDef::new(TournamentConfig::CurrentSeasonal).custom(Season::Table).not_null().default("S1")) + .col(ColumnDef::new(TournamentConfig::CurrentCategory).custom(Category::Table).not_null().default("QUA")) + .col(ColumnDef::new(TournamentConfig::CanRegister).boolean().not_null().default(false)) + .col(ColumnDef::new(TournamentConfig::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(TournamentConfig::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .to_owned(), + ) + .await?; + + // 创建多人游戏房间表 + manager + .create_table( + Table::create() + .table(MultiplayerRoom::Table) + .if_not_exists() + .col( + ColumnDef::new(MultiplayerRoom::RoomId) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(MultiplayerRoom::Type).custom(MultiplayerRoomType::Table).not_null().default("SOLO")) + .col(ColumnDef::new(MultiplayerRoom::Season).custom(Season::Table).not_null().default("S1")) + .col(ColumnDef::new(MultiplayerRoom::Category).custom(Category::Table).not_null().default("QUA")) + .col(ColumnDef::new(MultiplayerRoom::MultiplayerSoloRoomRoomId).integer().null()) + .col(ColumnDef::new(MultiplayerRoom::MultiplayerTeamvsRoomRoomId).integer().null()) + .col(ColumnDef::new(MultiplayerRoom::ScoreRed).integer().not_null().default(0)) + .col(ColumnDef::new(MultiplayerRoom::ScoreBlue).integer().not_null().default(0)) + .col(ColumnDef::new(MultiplayerRoom::StartTime).timestamp().not_null()) + .col(ColumnDef::new(MultiplayerRoom::EndTime).timestamp().not_null()) + .col(ColumnDef::new(MultiplayerRoom::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(MultiplayerRoom::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .to_owned(), + ) + .await?; + + // 创建单人游戏房间表 + manager + .create_table( + Table::create() + .table(MultiplayerSoloRoom::Table) + .if_not_exists() + .col( + ColumnDef::new(MultiplayerSoloRoom::RoomId) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(MultiplayerSoloRoom::PlayerRedId).integer().not_null().unique_key()) + .col(ColumnDef::new(MultiplayerSoloRoom::PlayerBlueId).integer().not_null().unique_key()) + .to_owned(), + ) + .await?; + + // 创建团队对战房间表 + manager + .create_table( + Table::create() + .table(MultiplayerTeamvsRoom::Table) + .if_not_exists() + .col( + ColumnDef::new(MultiplayerTeamvsRoom::RoomId) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(MultiplayerTeamvsRoom::TeamRedId).integer().not_null().unique_key()) + .col(ColumnDef::new(MultiplayerTeamvsRoom::TeamBlueId).integer().not_null().unique_key()) + .to_owned(), + ) + .await?; + + // 创建消息表 + manager + .create_table( + Table::create() + .table(Message::Table) + .if_not_exists() + .col( + ColumnDef::new(Message::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Message::Title).string().not_null()) + .col(ColumnDef::new(Message::Content).text().not_null()) + .col(ColumnDef::new(Message::SenderId).integer().not_null()) + .col(ColumnDef::new(Message::ReceiverId).integer().not_null()) + .col(ColumnDef::new(Message::RoomId).integer().null()) + .col(ColumnDef::new(Message::IsRead).boolean().not_null().default(false)) + .col(ColumnDef::new(Message::IsDeleted).boolean().not_null().default(false)) + .col(ColumnDef::new(Message::IsSystemMessage).boolean().not_null().default(false)) + .col(ColumnDef::new(Message::IsAccepted).boolean().not_null().default(false)) + .col(ColumnDef::new(Message::CreatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(Message::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp())) + .to_owned(), + ) + .await?; + + // 创建外键约束 + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_multiplayer_room_solo_room") + .from(MultiplayerRoom::Table, MultiplayerRoom::MultiplayerSoloRoomRoomId) + .to(MultiplayerSoloRoom::Table, MultiplayerSoloRoom::RoomId) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_multiplayer_room_teamvs_room") + .from(MultiplayerRoom::Table, MultiplayerRoom::MultiplayerTeamvsRoomRoomId) + .to(MultiplayerTeamvsRoom::Table, MultiplayerTeamvsRoom::RoomId) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_multiplayer_solo_room_player_red") + .from(MultiplayerSoloRoom::Table, MultiplayerSoloRoom::PlayerRedId) + .to(User::Table, User::Osuid) + .on_delete(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_multiplayer_solo_room_player_blue") + .from(MultiplayerSoloRoom::Table, MultiplayerSoloRoom::PlayerBlueId) + .to(User::Table, User::Osuid) + .on_delete(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_multiplayer_teamvs_room_team_red") + .from(MultiplayerTeamvsRoom::Table, MultiplayerTeamvsRoom::TeamRedId) + .to(Team::Table, Team::Id) + .on_delete(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_multiplayer_teamvs_room_team_blue") + .from(MultiplayerTeamvsRoom::Table, MultiplayerTeamvsRoom::TeamBlueId) + .to(Team::Table, Team::Id) + .on_delete(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_message_sender") + .from(Message::Table, Message::SenderId) + .to(User::Table, User::Osuid) + .on_delete(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_message_receiver") + .from(Message::Table, Message::ReceiverId) + .to(User::Table, User::Osuid) + .on_delete(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_message_room") + .from(Message::Table, Message::RoomId) + .to(MultiplayerRoom::Table, MultiplayerRoom::RoomId) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // 删除外键约束 + manager + .drop_foreign_key( + ForeignKey::drop() + .name("fk_message_room") + .table(Message::Table) + .to_owned(), + ) + .await?; + + manager + .drop_foreign_key( + ForeignKey::drop() + .name("fk_message_receiver") + .table(Message::Table) + .to_owned(), + ) + .await?; + + manager + .drop_foreign_key( + ForeignKey::drop() + .name("fk_message_sender") + .table(Message::Table) + .to_owned(), + ) + .await?; + + manager + .drop_foreign_key( + ForeignKey::drop() + .name("fk_multiplayer_teamvs_room_team_blue") + .table(MultiplayerTeamvsRoom::Table) + .to_owned(), + ) + .await?; + + manager + .drop_foreign_key( + ForeignKey::drop() + .name("fk_multiplayer_teamvs_room_team_red") + .table(MultiplayerTeamvsRoom::Table) + .to_owned(), + ) + .await?; + + manager + .drop_foreign_key( + ForeignKey::drop() + .name("fk_multiplayer_solo_room_player_blue") + .table(MultiplayerSoloRoom::Table) + .to_owned(), + ) + .await?; + + manager + .drop_foreign_key( + ForeignKey::drop() + .name("fk_multiplayer_solo_room_player_red") + .table(MultiplayerSoloRoom::Table) + .to_owned(), + ) + .await?; + + manager + .drop_foreign_key( + ForeignKey::drop() + .name("fk_multiplayer_room_teamvs_room") + .table(MultiplayerRoom::Table) + .to_owned(), + ) + .await?; + + manager + .drop_foreign_key( + ForeignKey::drop() + .name("fk_multiplayer_room_solo_room") + .table(MultiplayerRoom::Table) + .to_owned(), + ) + .await?; + + // 删除表 + manager + .drop_table(Table::drop().table(Message::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(MultiplayerTeamvsRoom::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(MultiplayerSoloRoom::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(MultiplayerRoom::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(TournamentConfig::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(Team::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(User::Table).to_owned()) + .await?; + + // 删除枚举类型 + manager + .drop_type(Type::drop().name(Category::Table).to_owned()) + .await?; + + manager + .drop_type(Type::drop().name(Season::Table).to_owned()) + .await?; + + manager + .drop_type(Type::drop().name(UserGroupType::Table).to_owned()) + .await?; + + manager + .drop_type(Type::drop().name(UserState::Table).to_owned()) + .await?; + + manager + .drop_type(Type::drop().name(TeamState::Table).to_owned()) + .await?; + + manager + .drop_type(Type::drop().name(TeamColor::Table).to_owned()) + .await?; + + manager + .drop_type(Type::drop().name(MultiplayerRoomType::Table).to_owned()) + .await?; + + manager + .drop_type(Type::drop().name(BeatmapSelectMod::Table).to_owned()) + .await?; + + Ok(()) + } +} + +// 定义表名枚举 +#[derive(Iden)] +enum User { + Table, + Id, + UserState, + Osuid, + Username, + AvatarUrl, + CoverUrl, + CountryCode, + Pp, + GlobalRank, + CountryRank, + Approved, + Seed, + Seasonal, + CreatedAt, + UpdatedAt, +} + +#[derive(Iden)] +enum Team { + Table, + Id, + Name, + BannerUrl, + TeamColor, + TeamState, + CreatedAt, + UpdatedAt, +} + +#[derive(Iden)] +enum TournamentConfig { + Table, + Id, + TournamentName, + MaxPpForRegistration, + MinPpForRegistration, + CurrentSeasonal, + CurrentCategory, + CanRegister, + CreatedAt, + UpdatedAt, +} + +#[derive(Iden)] +enum MultiplayerRoom { + Table, + RoomId, + Type, + Season, + Category, + MultiplayerSoloRoomRoomId, + MultiplayerTeamvsRoomRoomId, + ScoreRed, + ScoreBlue, + StartTime, + EndTime, + CreatedAt, + UpdatedAt, +} + +#[derive(Iden)] +enum MultiplayerSoloRoom { + Table, + RoomId, + PlayerRedId, + PlayerBlueId, +} + +#[derive(Iden)] +enum MultiplayerTeamvsRoom { + Table, + RoomId, + TeamRedId, + TeamBlueId, +} + +#[derive(Iden)] +enum Message { + Table, + Id, + Title, + Content, + SenderId, + ReceiverId, + RoomId, + IsRead, + IsDeleted, + IsSystemMessage, + IsAccepted, + CreatedAt, + UpdatedAt, +} + +// 定义枚举类型名 +#[derive(Iden)] +enum BeatmapSelectMod { + Table, + Nm, + Hd, + Hr, + Dt, + Lz, + Tb, +} + +#[derive(Iden)] +enum MultiplayerRoomType { + Table, + Solo, + TeamVs, +} + +#[derive(Iden)] +enum TeamColor { + Table, + BlueTeam, + RedTeam, +} + +#[derive(Iden)] +enum TeamState { + Table, + Active, + Unapproved, + Approved, + Banned, +} + +#[derive(Iden)] +enum UserState { + Table, + Active, + Registered, + Abandoned, + Banned, +} + +#[derive(Iden)] +enum UserGroupType { + Table, + Host, + Admin, + Pooler, + Streamer, + Tester, + Gfx, + Sheeter, + Commentator, + Mapper, + Referee, + Player, +} + +#[derive(Iden)] +enum Season { + Table, + S1, + S2, +} + +#[derive(Iden)] +enum Category { + Table, + Qua, + Ro16, + Qf, + Sf, + F, + Gf, +} diff --git a/migration/src/main.rs b/migration/src/main.rs index c81d0e7..c62b906 100644 --- a/migration/src/main.rs +++ b/migration/src/main.rs @@ -1,6 +1,6 @@ -use sea_orm_migration::prelude::*; - -#[async_std::main] -async fn main() { - cli::run_cli(migration::Migrator).await; +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + cli::run_cli(migration::Migrator).await; } \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs index d192388..78d4e64 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,3 +1,3 @@ -pub mod settings; - +pub mod settings; + pub use settings::Settings; \ No newline at end of file diff --git a/src/config/settings.rs b/src/config/settings.rs index c58efee..79f2f8d 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -1,102 +1,102 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize, Clone)] -pub struct DatabaseConfig { - pub url: String, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct OAuthConfig { - pub client_id: String, - pub client_secret: String, - pub redirect_uri: String, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct JwtConfig { - pub secret: String, - pub expiry_hours: i64, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct ServerConfig { - pub port: u16, - pub host: String, - pub log_level: String, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct CorsConfig { - pub origin: String, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct RedisConfig { - pub url: Option, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct Settings { - pub database: DatabaseConfig, - pub oauth: OAuthConfig, - pub jwt: JwtConfig, - pub server: ServerConfig, - pub cors: CorsConfig, - pub redis: RedisConfig, -} - -impl Settings { - pub fn from_env() -> Result> { - dotenv::dotenv().ok(); - - let settings = Settings { - database: DatabaseConfig { - url: std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "mysql://root:@localhost:3306/astracup".to_string()), - }, - oauth: OAuthConfig { - client_id: std::env::var("OSU_CLIENT_ID").unwrap_or_default(), - client_secret: std::env::var("OSU_CLIENT_SECRET").unwrap_or_default(), - redirect_uri: std::env::var("OSU_REDIRECT_URI") - .unwrap_or_else(|_| "http://localhost:3000/api/auth/callback/osu".to_string()), - }, - jwt: JwtConfig { - secret: std::env::var("JWT_SECRET") - .unwrap_or_else(|_| "default_secret_change_in_production".to_string()), - expiry_hours: std::env::var("JWT_EXPIRY_HOURS") - .unwrap_or_else(|_| "24".to_string()) - .parse() - .unwrap_or(24), - }, - server: ServerConfig { - port: std::env::var("PORT") - .unwrap_or_else(|_| "3001".to_string()) - .parse() - .unwrap_or(3001), - host: std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()), - log_level: std::env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()), - }, - cors: CorsConfig { - origin: std::env::var("CORS_ORIGIN") - .unwrap_or_else(|_| "http://localhost:3000".to_string()), - }, - redis: RedisConfig { - url: std::env::var("REDIS_URL").ok(), - }, - }; - - Ok(settings) - } - - pub fn database_url(&self) -> &str { - &self.database.url - } - - pub fn jwt_secret(&self) -> &str { - &self.jwt.secret - } - - pub fn server_address(&self) -> String { - format!("{}:{}", self.server.host, self.server.port) - } +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct DatabaseConfig { + pub url: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct OAuthConfig { + pub client_id: String, + pub client_secret: String, + pub redirect_uri: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct JwtConfig { + pub secret: String, + pub expiry_hours: i64, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ServerConfig { + pub port: u16, + pub host: String, + pub log_level: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct CorsConfig { + pub origin: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct RedisConfig { + pub url: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Settings { + pub database: DatabaseConfig, + pub oauth: OAuthConfig, + pub jwt: JwtConfig, + pub server: ServerConfig, + pub cors: CorsConfig, + pub redis: RedisConfig, +} + +impl Settings { + pub fn from_env() -> Result> { + dotenv::dotenv().ok(); + + let settings = Settings { + database: DatabaseConfig { + url: std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "mysql://root:@localhost:3306/astracup".to_string()), + }, + oauth: OAuthConfig { + client_id: std::env::var("OSU_CLIENT_ID").unwrap_or_default(), + client_secret: std::env::var("OSU_CLIENT_SECRET").unwrap_or_default(), + redirect_uri: std::env::var("OSU_REDIRECT_URI") + .unwrap_or_else(|_| "http://localhost:3000/api/auth/callback/osu".to_string()), + }, + jwt: JwtConfig { + secret: std::env::var("JWT_SECRET") + .unwrap_or_else(|_| "default_secret_change_in_production".to_string()), + expiry_hours: std::env::var("JWT_EXPIRY_HOURS") + .unwrap_or_else(|_| "24".to_string()) + .parse() + .unwrap_or(24), + }, + server: ServerConfig { + port: std::env::var("PORT") + .unwrap_or_else(|_| "3001".to_string()) + .parse() + .unwrap_or(3001), + host: std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()), + log_level: std::env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()), + }, + cors: CorsConfig { + origin: std::env::var("CORS_ORIGIN") + .unwrap_or_else(|_| "http://localhost:3000".to_string()), + }, + redis: RedisConfig { + url: std::env::var("REDIS_URL").ok(), + }, + }; + + Ok(settings) + } + + pub fn database_url(&self) -> &str { + &self.database.url + } + + pub fn jwt_secret(&self) -> &str { + &self.jwt.secret + } + + pub fn server_address(&self) -> String { + format!("{}:{}", self.server.host, self.server.port) + } } \ No newline at end of file diff --git a/src/database/connection.rs b/src/database/connection.rs index 364cb59..0628fba 100644 --- a/src/database/connection.rs +++ b/src/database/connection.rs @@ -1,39 +1,39 @@ -use sea_orm::{Database as SeaDatabase, DatabaseConnection, DbErr}; -use tracing::info; - -pub struct Database { - pub connection: DatabaseConnection, -} - -impl Database { - pub async fn new(database_url: &str) -> Result { - info!("Connecting to database: {}", database_url); - - let connection = SeaDatabase::connect(database_url).await?; - - info!("Database connection established successfully"); - - Ok(Self { connection }) - } - - pub fn get_connection(&self) -> &DatabaseConnection { - &self.connection - } - - pub async fn test_connection(&self) -> Result<(), DbErr> { - info!("Testing database connection..."); - - // Simple query to test connection - let _: Option<(i32,)> = sea_orm::QuerySelect::query( - &mut sea_orm::Statement::from_string( - sea_orm::DatabaseBackend::MySql, - "SELECT 1".to_string(), - ), - ) - .one(&self.connection) - .await?; - - info!("Database connection test passed"); - Ok(()) - } +use sea_orm::{Database as SeaDatabase, DatabaseConnection, DbErr}; +use tracing::info; + +pub struct Database { + pub connection: DatabaseConnection, +} + +impl Database { + pub async fn new(database_url: &str) -> Result { + info!("Connecting to database: {}", database_url); + + let connection = SeaDatabase::connect(database_url).await?; + + info!("Database connection established successfully"); + + Ok(Self { connection }) + } + + pub fn get_connection(&self) -> &DatabaseConnection { + &self.connection + } + + pub async fn test_connection(&self) -> Result<(), DbErr> { + info!("Testing database connection..."); + + // Simple query to test connection + let _: Option<(i32,)> = sea_orm::QuerySelect::query( + &mut sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::MySql, + "SELECT 1".to_string(), + ), + ) + .one(&self.connection) + .await?; + + info!("Database connection test passed"); + Ok(()) + } } \ No newline at end of file diff --git a/src/database/mod.rs b/src/database/mod.rs index 265d069..b657207 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,3 +1,3 @@ -pub mod connection; - +pub mod connection; + pub use connection::Database; \ No newline at end of file diff --git a/src/entities/message.rs b/src/entities/message.rs index bf493f3..18590aa 100644 --- a/src/entities/message.rs +++ b/src/entities/message.rs @@ -1,76 +1,76 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] -#[sea_orm(table_name = "messages")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - - pub title: String, - - #[sea_orm(column_type = "Text")] - pub content: String, - - pub sender_id: i32, - - pub receiver_id: i32, - - pub room_id: Option, - - pub is_read: bool, - - pub is_deleted: bool, - - pub is_system_message: bool, - - pub is_accepted: bool, - - pub created_at: DateTime, - - pub updated_at: DateTime, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::SenderId", - to = "super::user::Column::Osuid", - on_update = "Cascade", - on_delete = "Restrict" - )] - Sender, - - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::ReceiverId", - to = "super::user::Column::Osuid", - on_update = "Cascade", - on_delete = "Restrict" - )] - Receiver, - - #[sea_orm( - belongs_to = "super::multiplayer_room::Entity", - from = "Column::RoomId", - to = "super::multiplayer_room::Column::RoomId", - on_update = "Cascade", - on_delete = "SetNull" - )] - Room, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Sender.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "messages")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + + pub title: String, + + #[sea_orm(column_type = "Text")] + pub content: String, + + pub sender_id: i32, + + pub receiver_id: i32, + + pub room_id: Option, + + pub is_read: bool, + + pub is_deleted: bool, + + pub is_system_message: bool, + + pub is_accepted: bool, + + pub created_at: DateTime, + + pub updated_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::SenderId", + to = "super::user::Column::Osuid", + on_update = "Cascade", + on_delete = "Restrict" + )] + Sender, + + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::ReceiverId", + to = "super::user::Column::Osuid", + on_update = "Cascade", + on_delete = "Restrict" + )] + Receiver, + + #[sea_orm( + belongs_to = "super::multiplayer_room::Entity", + from = "Column::RoomId", + to = "super::multiplayer_room::Column::RoomId", + on_update = "Cascade", + on_delete = "SetNull" + )] + Room, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Sender.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Room.def() + } +} + impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 2547feb..51160f9 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,17 +1,16 @@ -pub mod prelude; - -pub use prelude::*; - -// 实体定义 - 基于Prisma模型(完全迁移) -pub mod user; -pub mod team; -pub mod tournament_config; -pub mod multiplayer_room; -pub mod multiplayer_solo_room; -pub mod multiplayer_teamvs_room; -pub mod beatmap; -pub mod beatmap_comment; -pub mod score; -pub mod user_group; -pub mod message; -pub mod matchup; +pub mod prelude; + +pub use prelude::*; + +pub mod user; +pub mod team; +pub mod tournament_config; +pub mod multiplayer_room; +pub mod multiplayer_solo_room; +pub mod multiplayer_teamvs_room; +pub mod beatmap; +pub mod beatmap_comment; +pub mod score; +pub mod user_group; +pub mod message; +pub mod matchup; diff --git a/src/entities/multiplayer_room.rs b/src/entities/multiplayer_room.rs index 29a2da5..2ea5a12 100644 --- a/src/entities/multiplayer_room.rs +++ b/src/entities/multiplayer_room.rs @@ -1,90 +1,90 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] -#[sea_orm(table_name = "multiplayer_rooms")] -pub struct Model { - #[sea_orm(primary_key)] - pub room_id: i32, - - #[sea_orm(column_type = "String(Some(10))")] - pub r#type: MultiplayerRoomType, - - #[sea_orm(column_type = "String(Some(2))")] - pub season: Season, - - #[sea_orm(column_type = "String(Some(4))")] - pub category: Category, - - pub multiplayer_solo_room_room_id: Option, - - pub multiplayer_teamvs_room_room_id: Option, - - pub score_red: i32, - - pub score_blue: i32, - - pub start_time: DateTime, - - pub end_time: DateTime, - - pub created_at: DateTime, - - pub updated_at: DateTime, -} - -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "multiplayer_room_type")] -pub enum MultiplayerRoomType { - #[sea_orm(string_value = "SOLO")] - Solo, - #[sea_orm(string_value = "TEAM_VS")] - TeamVs, -} - -// 使用与tournament_config相同的Season和Category枚举 -pub use super::tournament_config::{Season, Category}; - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::multiplayer_solo_room::Entity", - from = "Column::MultiplayerSoloRoomRoomId", - to = "super::multiplayer_solo_room::Column::RoomId", - on_update = "Cascade", - on_delete = "SetNull" - )] - MultiplayerSoloRoom, - - #[sea_orm( - belongs_to = "super::multiplayer_teamvs_room::Entity", - from = "Column::MultiplayerTeamvsRoomRoomId", - to = "super::multiplayer_teamvs_room::Column::RoomId", - on_update = "Cascade", - on_delete = "SetNull" - )] - MultiplayerTeamvsRoom, - - #[sea_orm(has_many = "super::message::Entity")] - Messages, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MultiplayerSoloRoom.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MultiplayerTeamvsRoom.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Messages.def() - } -} - +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "multiplayer_rooms")] +pub struct Model { + #[sea_orm(primary_key)] + pub room_id: i32, + + #[sea_orm(column_type = "String(Some(10))")] + pub r#type: MultiplayerRoomType, + + #[sea_orm(column_type = "String(Some(2))")] + pub season: Season, + + #[sea_orm(column_type = "String(Some(4))")] + pub category: Category, + + pub multiplayer_solo_room_room_id: Option, + + pub multiplayer_teamvs_room_room_id: Option, + + pub score_red: i32, + + pub score_blue: i32, + + pub start_time: DateTime, + + pub end_time: DateTime, + + pub created_at: DateTime, + + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "multiplayer_room_type")] +pub enum MultiplayerRoomType { + #[sea_orm(string_value = "SOLO")] + Solo, + #[sea_orm(string_value = "TEAM_VS")] + TeamVs, +} + +// 使用与tournament_config相同的Season和Category枚举 +pub use super::tournament_config::{Season, Category}; + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::multiplayer_solo_room::Entity", + from = "Column::MultiplayerSoloRoomRoomId", + to = "super::multiplayer_solo_room::Column::RoomId", + on_update = "Cascade", + on_delete = "SetNull" + )] + MultiplayerSoloRoom, + + #[sea_orm( + belongs_to = "super::multiplayer_teamvs_room::Entity", + from = "Column::MultiplayerTeamvsRoomRoomId", + to = "super::multiplayer_teamvs_room::Column::RoomId", + on_update = "Cascade", + on_delete = "SetNull" + )] + MultiplayerTeamvsRoom, + + #[sea_orm(has_many = "super::message::Entity")] + Messages, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MultiplayerSoloRoom.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MultiplayerTeamvsRoom.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Messages.def() + } +} + impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/src/entities/multiplayer_solo_room.rs b/src/entities/multiplayer_solo_room.rs index f8da268..e6aee4c 100644 --- a/src/entities/multiplayer_solo_room.rs +++ b/src/entities/multiplayer_solo_room.rs @@ -1,53 +1,53 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] -#[sea_orm(table_name = "multiplayer_solo_rooms")] -pub struct Model { - #[sea_orm(primary_key)] - pub room_id: i32, - - #[sea_orm(unique)] - pub player_red_id: i32, - - #[sea_orm(unique)] - pub player_blue_id: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::PlayerRedId", - to = "super::user::Column::Osuid", - on_update = "Cascade", - on_delete = "Restrict" - )] - RedPlayer, - - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::PlayerBlueId", - to = "super::user::Column::Osuid", - on_update = "Cascade", - on_delete = "Restrict" - )] - BluePlayer, - - #[sea_orm(has_many = "super::multiplayer_room::Entity")] - MultiplayerRooms, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::RedPlayer.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MultiplayerRooms.def() - } -} - +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "multiplayer_solo_rooms")] +pub struct Model { + #[sea_orm(primary_key)] + pub room_id: i32, + + #[sea_orm(unique)] + pub player_red_id: i32, + + #[sea_orm(unique)] + pub player_blue_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::PlayerRedId", + to = "super::user::Column::Osuid", + on_update = "Cascade", + on_delete = "Restrict" + )] + RedPlayer, + + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::PlayerBlueId", + to = "super::user::Column::Osuid", + on_update = "Cascade", + on_delete = "Restrict" + )] + BluePlayer, + + #[sea_orm(has_many = "super::multiplayer_room::Entity")] + MultiplayerRooms, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::RedPlayer.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MultiplayerRooms.def() + } +} + impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/src/entities/multiplayer_teamvs_room.rs b/src/entities/multiplayer_teamvs_room.rs index 1e8e9a0..817d781 100644 --- a/src/entities/multiplayer_teamvs_room.rs +++ b/src/entities/multiplayer_teamvs_room.rs @@ -1,53 +1,53 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] -#[sea_orm(table_name = "multiplayer_teamvs_rooms")] -pub struct Model { - #[sea_orm(primary_key)] - pub room_id: i32, - - #[sea_orm(unique)] - pub team_red_id: i32, - - #[sea_orm(unique)] - pub team_blue_id: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::team::Entity", - from = "Column::TeamRedId", - to = "super::team::Column::Id", - on_update = "Cascade", - on_delete = "Restrict" - )] - RedTeam, - - #[sea_orm( - belongs_to = "super::team::Entity", - from = "Column::TeamBlueId", - to = "super::team::Column::Id", - on_update = "Cascade", - on_delete = "Restrict" - )] - BlueTeam, - - #[sea_orm(has_many = "super::multiplayer_room::Entity")] - MultiplayerRooms, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::RedTeam.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MultiplayerRooms.def() - } -} - +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "multiplayer_teamvs_rooms")] +pub struct Model { + #[sea_orm(primary_key)] + pub room_id: i32, + + #[sea_orm(unique)] + pub team_red_id: i32, + + #[sea_orm(unique)] + pub team_blue_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::team::Entity", + from = "Column::TeamRedId", + to = "super::team::Column::Id", + on_update = "Cascade", + on_delete = "Restrict" + )] + RedTeam, + + #[sea_orm( + belongs_to = "super::team::Entity", + from = "Column::TeamBlueId", + to = "super::team::Column::Id", + on_update = "Cascade", + on_delete = "Restrict" + )] + BlueTeam, + + #[sea_orm(has_many = "super::multiplayer_room::Entity")] + MultiplayerRooms, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::RedTeam.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MultiplayerRooms.def() + } +} + impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/src/entities/prelude.rs b/src/entities/prelude.rs index c92a1a5..0b2bab7 100644 --- a/src/entities/prelude.rs +++ b/src/entities/prelude.rs @@ -1,17 +1,17 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.12.15 - -pub use super::{ - // Prisma模型实体 - user::Entity as User, - team::Entity as Team, - tournament_config::Entity as TournamentConfig, - multiplayer_room::Entity as MultiplayerRoom, - multiplayer_solo_room::Entity as MultiplayerSoloRoom, - multiplayer_teamvs_room::Entity as MultiplayerTeamvsRoom, - beatmap::Entity as Beatmap, - beatmap_comment::Entity as BeatmapComment, - score::Entity as Score, - user_group::Entity as UserGroup, - message::Entity as Message, - matchup::Entity as Matchup, -}; +//! SeaORM Entity. Generated by sea-orm-codegen 0.12.15 + +pub use super::{ + // Prisma模型实体 + user::Entity as User, + team::Entity as Team, + tournament_config::Entity as TournamentConfig, + multiplayer_room::Entity as MultiplayerRoom, + multiplayer_solo_room::Entity as MultiplayerSoloRoom, + multiplayer_teamvs_room::Entity as MultiplayerTeamvsRoom, + beatmap::Entity as Beatmap, + beatmap_comment::Entity as BeatmapComment, + score::Entity as Score, + user_group::Entity as UserGroup, + message::Entity as Message, + matchup::Entity as Matchup, +}; diff --git a/src/entities/team.rs b/src/entities/team.rs index 897a2eb..3ed385d 100644 --- a/src/entities/team.rs +++ b/src/entities/team.rs @@ -1,66 +1,66 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] -#[sea_orm(table_name = "teams")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - - pub name: String, - - #[sea_orm(column_type = "Text")] - pub banner_url: String, - - #[sea_orm(column_type = "String(Some(10))")] - pub team_color: TeamColor, - - #[sea_orm(column_type = "String(Some(15))")] - pub team_state: TeamState, - - pub created_at: DateTime, - - pub updated_at: DateTime, -} - -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "team_color")] -pub enum TeamColor { - #[sea_orm(string_value = "blue_team")] - BlueTeam, - #[sea_orm(string_value = "red_team")] - RedTeam, -} - -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "team_state")] -pub enum TeamState { - #[sea_orm(string_value = "ACTIVE")] - Active, - #[sea_orm(string_value = "UNAPPROVED")] - Unapproved, - #[sea_orm(string_value = "APPROVED")] - Approved, - #[sea_orm(string_value = "BANNED")] - Banned, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::user::Entity")] - Members, - - #[sea_orm(has_many = "super::multiplayer_teamvs_room::Entity", via = "super::multiplayer_teamvs_room::Relation::RedTeam")] - RedTeamRooms, - - #[sea_orm(has_many = "super::multiplayer_teamvs_room::Entity", via = "super::multiplayer_teamvs_room::Relation::BlueTeam")] - BlueTeamRooms, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Members.def() - } -} - +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "teams")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + + pub name: String, + + #[sea_orm(column_type = "Text")] + pub banner_url: String, + + #[sea_orm(column_type = "String(Some(10))")] + pub team_color: TeamColor, + + #[sea_orm(column_type = "String(Some(15))")] + pub team_state: TeamState, + + pub created_at: DateTime, + + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "team_color")] +pub enum TeamColor { + #[sea_orm(string_value = "blue_team")] + BlueTeam, + #[sea_orm(string_value = "red_team")] + RedTeam, +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "team_state")] +pub enum TeamState { + #[sea_orm(string_value = "ACTIVE")] + Active, + #[sea_orm(string_value = "UNAPPROVED")] + Unapproved, + #[sea_orm(string_value = "APPROVED")] + Approved, + #[sea_orm(string_value = "BANNED")] + Banned, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::user::Entity")] + Members, + + #[sea_orm(has_many = "super::multiplayer_teamvs_room::Entity", via = "super::multiplayer_teamvs_room::Relation::RedTeam")] + RedTeamRooms, + + #[sea_orm(has_many = "super::multiplayer_teamvs_room::Entity", via = "super::multiplayer_teamvs_room::Relation::BlueTeam")] + BlueTeamRooms, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Members.def() + } +} + impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/src/entities/tournament_config.rs b/src/entities/tournament_config.rs index 3b9ccdc..d7b4fe1 100644 --- a/src/entities/tournament_config.rs +++ b/src/entities/tournament_config.rs @@ -1,142 +1,142 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] -#[sea_orm(table_name = "tournament_configs")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - - pub tournament_name: String, - - #[sea_orm(column_type = "Double")] - pub max_pp_for_registration: f64, - - #[sea_orm(column_type = "Double")] - pub min_pp_for_registration: f64, - - #[sea_orm(column_type = "String(Some(2))")] - pub current_seasonal: Season, - - #[sea_orm(column_type = "String(Some(4))")] - pub current_category: Category, - - pub can_register: bool, - - pub created_at: DateTime, - - pub updated_at: DateTime, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum Season { - S1, - S2, - S3, - S4, - S5, - S6, - S7, - S8, - S9, - S10, - Custom(String), -} - -impl From for Season { - fn from(value: String) -> Self { - match value.as_str() { - "S1" => Season::S1, - "S2" => Season::S2, - "S3" => Season::S3, - "S4" => Season::S4, - "S5" => Season::S5, - "S6" => Season::S6, - "S7" => Season::S7, - "S8" => Season::S8, - "S9" => Season::S9, - "S10" => Season::S10, - _ => Season::Custom(value), - } - } -} - -impl From for String { - fn from(season: Season) -> Self { - match season { - Season::S1 => "S1".to_string(), - Season::S2 => "S2".to_string(), - Season::S3 => "S3".to_string(), - Season::S4 => "S4".to_string(), - Season::S5 => "S5".to_string(), - Season::S6 => "S6".to_string(), - Season::S7 => "S7".to_string(), - Season::S8 => "S8".to_string(), - Season::S9 => "S9".to_string(), - Season::S10 => "S10".to_string(), - Season::Custom(value) => value, - } - } -} - -impl sea_orm::TryGetable for Season { - fn try_get_by(res: &sea_orm::QueryResult, index: I) -> Result { - let value: String = res.try_get_by(index)?; - Ok(Season::from(value)) - } -} - -impl sea_orm::sea_query::ValueType for Season { - fn try_from(v: sea_orm::Value) -> Result { - match v { - sea_orm::Value::String(Some(s)) => Ok(Season::from(s)), - _ => Err(sea_orm::sea_query::ValueTypeErr), - } - } - - fn type_name() -> String { - "String".to_string() - } - - fn column_type() -> sea_orm::sea_query::ColumnType { - sea_orm::sea_query::ColumnType::String(Some(10)) - } - - fn array_type() -> sea_orm::sea_query::ArrayType { - sea_orm::sea_query::ArrayType::String - } -} - -impl sea_orm::sea_query::Nullable for Season { - fn null() -> sea_orm::Value { - sea_orm::Value::String(None) - } -} - -impl sea_orm::sea_query::ValueTypeErr for Season { - fn err() -> sea_orm::sea_query::ValueTypeErr { - sea_orm::sea_query::ValueTypeErr - } -} - -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "category")] -pub enum Category { - #[sea_orm(string_value = "QUA")] - Qua, - #[sea_orm(string_value = "RO16")] - Ro16, - #[sea_orm(string_value = "QF")] - Qf, - #[sea_orm(string_value = "SF")] - Sf, - #[sea_orm(string_value = "F")] - F, - #[sea_orm(string_value = "GF")] - Gf, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "tournament_configs")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + + pub tournament_name: String, + + #[sea_orm(column_type = "Double")] + pub max_pp_for_registration: f64, + + #[sea_orm(column_type = "Double")] + pub min_pp_for_registration: f64, + + #[sea_orm(column_type = "String(Some(2))")] + pub current_seasonal: Season, + + #[sea_orm(column_type = "String(Some(4))")] + pub current_category: Category, + + pub can_register: bool, + + pub created_at: DateTime, + + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Season { + S1, + S2, + S3, + S4, + S5, + S6, + S7, + S8, + S9, + S10, + Custom(String), +} + +impl From for Season { + fn from(value: String) -> Self { + match value.as_str() { + "S1" => Season::S1, + "S2" => Season::S2, + "S3" => Season::S3, + "S4" => Season::S4, + "S5" => Season::S5, + "S6" => Season::S6, + "S7" => Season::S7, + "S8" => Season::S8, + "S9" => Season::S9, + "S10" => Season::S10, + _ => Season::Custom(value), + } + } +} + +impl From for String { + fn from(season: Season) -> Self { + match season { + Season::S1 => "S1".to_string(), + Season::S2 => "S2".to_string(), + Season::S3 => "S3".to_string(), + Season::S4 => "S4".to_string(), + Season::S5 => "S5".to_string(), + Season::S6 => "S6".to_string(), + Season::S7 => "S7".to_string(), + Season::S8 => "S8".to_string(), + Season::S9 => "S9".to_string(), + Season::S10 => "S10".to_string(), + Season::Custom(value) => value, + } + } +} + +impl sea_orm::TryGetable for Season { + fn try_get_by(res: &sea_orm::QueryResult, index: I) -> Result { + let value: String = res.try_get_by(index)?; + Ok(Season::from(value)) + } +} + +impl sea_orm::sea_query::ValueType for Season { + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::String(Some(s)) => Ok(Season::from(s)), + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + "String".to_string() + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::String(Some(10)) + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::String + } +} + +impl sea_orm::sea_query::Nullable for Season { + fn null() -> sea_orm::Value { + sea_orm::Value::String(None) + } +} + +impl sea_orm::sea_query::ValueTypeErr for Season { + fn err() -> sea_orm::sea_query::ValueTypeErr { + sea_orm::sea_query::ValueTypeErr + } +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "category")] +pub enum Category { + #[sea_orm(string_value = "QUA")] + Qua, + #[sea_orm(string_value = "RO16")] + Ro16, + #[sea_orm(string_value = "QF")] + Qf, + #[sea_orm(string_value = "SF")] + Sf, + #[sea_orm(string_value = "F")] + F, + #[sea_orm(string_value = "GF")] + Gf, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/src/entities/user.rs b/src/entities/user.rs index a32f977..36a91b2 100644 --- a/src/entities/user.rs +++ b/src/entities/user.rs @@ -1,106 +1,106 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] -#[sea_orm(table_name = "users")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - - #[sea_orm(unique)] - pub osuid: i32, - - #[sea_orm(unique)] - pub username: String, - - #[sea_orm(column_type = "Text", nullable)] - pub avatar_url: Option, - - #[sea_orm(column_type = "Text", nullable)] - pub cover_url: Option, - - pub country_code: String, - - #[sea_orm(column_type = "Double")] - pub pp: f64, - - pub global_rank: i32, - - pub country_rank: i32, - - pub approved: bool, - - pub seed: i32, - - #[sea_orm(column_type = "String(Some(2))")] - pub seasonal: Season, - - #[sea_orm(column_type = "String(Some(10))")] - pub user_state: UserState, - - pub created_at: DateTime, - - pub updated_at: DateTime, -} - -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "user_state")] -pub enum UserState { - #[sea_orm(string_value = "ACTIVE")] - Active, - #[sea_orm(string_value = "REGISTERED")] - Registered, - #[sea_orm(string_value = "ABANDONED")] - Abandoned, - #[sea_orm(string_value = "BANNED")] - Banned, -} - -pub use super::tournament_config::Season; - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::team::Entity")] - Teams, - - #[sea_orm(has_many = "super::message::Entity", via = "super::message::Relation::Sender")] - SentMessages, - - #[sea_orm(has_many = "super::message::Entity", via = "super::message::Relation::Receiver")] - ReceivedMessages, - - #[sea_orm(has_many = "super::beatmap::Entity")] - Beatmaps, - - #[sea_orm(has_many = "super::beatmap_comment::Entity")] - BeatmapComments, - - #[sea_orm(has_many = "super::multiplayer_solo_room::Entity", via = "super::multiplayer_solo_room::Relation::RedPlayer")] - SoloRedRooms, - - #[sea_orm(has_many = "super::multiplayer_solo_room::Entity", via = "super::multiplayer_solo_room::Relation::BluePlayer")] - SoloBlueRooms, - - #[sea_orm(has_many = "super::user_group::Entity")] - UserGroups, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Teams.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::SentMessages.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Beatmaps.def() - } -} - +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + + #[sea_orm(unique)] + pub osuid: i32, + + #[sea_orm(unique)] + pub username: String, + + #[sea_orm(column_type = "Text", nullable)] + pub avatar_url: Option, + + #[sea_orm(column_type = "Text", nullable)] + pub cover_url: Option, + + pub country_code: String, + + #[sea_orm(column_type = "Double")] + pub pp: f64, + + pub global_rank: i32, + + pub country_rank: i32, + + pub approved: bool, + + pub seed: i32, + + #[sea_orm(column_type = "String(Some(2))")] + pub seasonal: Season, + + #[sea_orm(column_type = "String(Some(10))")] + pub user_state: UserState, + + pub created_at: DateTime, + + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "user_state")] +pub enum UserState { + #[sea_orm(string_value = "ACTIVE")] + Active, + #[sea_orm(string_value = "REGISTERED")] + Registered, + #[sea_orm(string_value = "ABANDONED")] + Abandoned, + #[sea_orm(string_value = "BANNED")] + Banned, +} + +pub use super::tournament_config::Season; + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::team::Entity")] + Teams, + + #[sea_orm(has_many = "super::message::Entity", via = "super::message::Relation::Sender")] + SentMessages, + + #[sea_orm(has_many = "super::message::Entity", via = "super::message::Relation::Receiver")] + ReceivedMessages, + + #[sea_orm(has_many = "super::beatmap::Entity")] + Beatmaps, + + #[sea_orm(has_many = "super::beatmap_comment::Entity")] + BeatmapComments, + + #[sea_orm(has_many = "super::multiplayer_solo_room::Entity", via = "super::multiplayer_solo_room::Relation::RedPlayer")] + SoloRedRooms, + + #[sea_orm(has_many = "super::multiplayer_solo_room::Entity", via = "super::multiplayer_solo_room::Relation::BluePlayer")] + SoloBlueRooms, + + #[sea_orm(has_many = "super::user_group::Entity")] + UserGroups, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Teams.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SentMessages.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Beatmaps.def() + } +} + impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/src/entities/user_group.rs b/src/entities/user_group.rs index 2439ec5..3080364 100644 --- a/src/entities/user_group.rs +++ b/src/entities/user_group.rs @@ -1,65 +1,65 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] -#[sea_orm(table_name = "user_groups")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - - pub user_id: i32, - - #[sea_orm(column_type = "String(Some(15))")] - pub group_type: UserGroupType, - - pub created_at: DateTime, - - pub updated_at: DateTime, -} - -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "user_group_type")] -pub enum UserGroupType { - #[sea_orm(string_value = "HOST")] - Host, - #[sea_orm(string_value = "ADMIN")] - Admin, - #[sea_orm(string_value = "POOLER")] - Pooler, - #[sea_orm(string_value = "STREAMER")] - Streamer, - #[sea_orm(string_value = "TESTER")] - Tester, - #[sea_orm(string_value = "GFX")] - Gfx, - #[sea_orm(string_value = "SHEETER")] - Sheeter, - #[sea_orm(string_value = "COMMENTATOR")] - Commentator, - #[sea_orm(string_value = "MAPPER")] - Mapper, - #[sea_orm(string_value = "REFEREE")] - Referee, - #[sea_orm(string_value = "PLAYER")] - Player, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Osuid", - on_update = "Cascade", - on_delete = "Cascade" - )] - User, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "user_groups")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + + pub user_id: i32, + + #[sea_orm(column_type = "String(Some(15))")] + pub group_type: UserGroupType, + + pub created_at: DateTime, + + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "user_group_type")] +pub enum UserGroupType { + #[sea_orm(string_value = "HOST")] + Host, + #[sea_orm(string_value = "ADMIN")] + Admin, + #[sea_orm(string_value = "POOLER")] + Pooler, + #[sea_orm(string_value = "STREAMER")] + Streamer, + #[sea_orm(string_value = "TESTER")] + Tester, + #[sea_orm(string_value = "GFX")] + Gfx, + #[sea_orm(string_value = "SHEETER")] + Sheeter, + #[sea_orm(string_value = "COMMENTATOR")] + Commentator, + #[sea_orm(string_value = "MAPPER")] + Mapper, + #[sea_orm(string_value = "REFEREE")] + Referee, + #[sea_orm(string_value = "PLAYER")] + Player, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Osuid", + on_update = "Cascade", + on_delete = "Cascade" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/src/handlers/auth/handlers.rs b/src/handlers/auth/handlers.rs index 8c56449..b0d3a9e 100644 --- a/src/handlers/auth/handlers.rs +++ b/src/handlers/auth/handlers.rs @@ -1,113 +1,113 @@ -use axum::{ - extract::{Query, State}, - Json, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - services::auth::{AuthService, OsuUserInfo}, - utils::{response::ok, AppError}, -}; - -#[derive(Debug, Deserialize)] -pub struct AuthUrlQuery { - pub redirect_uri: Option, -} - -#[derive(Debug, Serialize)] -pub struct AuthUrlResponse { - pub success: bool, - pub auth_url: String, -} - -#[derive(Debug, Deserialize)] -pub struct AuthCallbackQuery { - pub code: String, -} - -#[derive(Debug, Serialize)] -pub struct AuthCallbackResponse { - pub success: bool, - pub token: String, - pub user: OsuUserInfo, -} - -pub async fn get_auth_url( - State(auth_service): State, - Query(query): Query, -) -> Result, AppError> { - // If a custom redirect_uri is provided, we could update the auth service - // For now, we'll use the configured one - let auth_url = auth_service.get_osu_auth_url(); - - Ok(Json(AuthUrlResponse { - success: true, - auth_url, - })) -} - -pub async fn auth_callback( - State(auth_service): State, - Query(query): Query, -) -> Result, AppError> { - // Exchange code for token - let token_data = auth_service.exchange_code_for_token(&query.code).await?; - - // Get user info from osu! API - let user_info = auth_service - .get_osu_user_info(&token_data.access_token) - .await?; - - // Generate JWT token - let jwt_token = auth_service.generate_jwt(&user_info)?; - - Ok(Json(AuthCallbackResponse { - success: true, - token: jwt_token, - user: user_info, - })) -} - -#[derive(Debug, Serialize)] -pub struct LogoutResponse { - pub success: bool, - pub message: String, -} - -pub async fn logout() -> Result, AppError> { - // In a stateless JWT system, logout is handled on the client side - // by removing the token. We could implement token blacklisting - // if using Redis or similar. - Ok(Json(LogoutResponse { - success: true, - message: "Logout successful. Please remove the token on the client side.".to_string(), - })) -} - -#[derive(Debug, Serialize)] -pub struct VerifyTokenResponse { - pub success: bool, - pub valid: bool, - pub user_id: Option, - pub username: Option, -} - -pub async fn verify_token( - State(auth_service): State, - token: String, -) -> Result, AppError> { - match auth_service.verify_jwt(&token) { - Ok(claims) => Ok(Json(VerifyTokenResponse { - success: true, - valid: true, - user_id: Some(claims.sub), - username: Some(claims.username), - })), - Err(_) => Ok(Json(VerifyTokenResponse { - success: true, - valid: false, - user_id: None, - username: None, - })), - } +use axum::{ + extract::{Query, State}, + Json, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + services::auth::{AuthService, OsuUserInfo}, + utils::{response::ok, AppError}, +}; + +#[derive(Debug, Deserialize)] +pub struct AuthUrlQuery { + pub redirect_uri: Option, +} + +#[derive(Debug, Serialize)] +pub struct AuthUrlResponse { + pub success: bool, + pub auth_url: String, +} + +#[derive(Debug, Deserialize)] +pub struct AuthCallbackQuery { + pub code: String, +} + +#[derive(Debug, Serialize)] +pub struct AuthCallbackResponse { + pub success: bool, + pub token: String, + pub user: OsuUserInfo, +} + +pub async fn get_auth_url( + State(auth_service): State, + Query(query): Query, +) -> Result, AppError> { + // If a custom redirect_uri is provided, we could update the auth service + // For now, we'll use the configured one + let auth_url = auth_service.get_osu_auth_url(); + + Ok(Json(AuthUrlResponse { + success: true, + auth_url, + })) +} + +pub async fn auth_callback( + State(auth_service): State, + Query(query): Query, +) -> Result, AppError> { + // Exchange code for token + let token_data = auth_service.exchange_code_for_token(&query.code).await?; + + // Get user info from osu! API + let user_info = auth_service + .get_osu_user_info(&token_data.access_token) + .await?; + + // Generate JWT token + let jwt_token = auth_service.generate_jwt(&user_info)?; + + Ok(Json(AuthCallbackResponse { + success: true, + token: jwt_token, + user: user_info, + })) +} + +#[derive(Debug, Serialize)] +pub struct LogoutResponse { + pub success: bool, + pub message: String, +} + +pub async fn logout() -> Result, AppError> { + // In a stateless JWT system, logout is handled on the client side + // by removing the token. We could implement token blacklisting + // if using Redis or similar. + Ok(Json(LogoutResponse { + success: true, + message: "Logout successful. Please remove the token on the client side.".to_string(), + })) +} + +#[derive(Debug, Serialize)] +pub struct VerifyTokenResponse { + pub success: bool, + pub valid: bool, + pub user_id: Option, + pub username: Option, +} + +pub async fn verify_token( + State(auth_service): State, + token: String, +) -> Result, AppError> { + match auth_service.verify_jwt(&token) { + Ok(claims) => Ok(Json(VerifyTokenResponse { + success: true, + valid: true, + user_id: Some(claims.sub), + username: Some(claims.username), + })), + Err(_) => Ok(Json(VerifyTokenResponse { + success: true, + valid: false, + user_id: None, + username: None, + })), + } } \ No newline at end of file diff --git a/src/handlers/auth/mod.rs b/src/handlers/auth/mod.rs index f3a0c88..6433603 100644 --- a/src/handlers/auth/mod.rs +++ b/src/handlers/auth/mod.rs @@ -1,3 +1,3 @@ -pub mod handlers; - +pub mod handlers; + pub use handlers::*; \ No newline at end of file diff --git a/src/handlers/health.rs b/src/handlers/health.rs index 90be8ed..ae7050a 100644 --- a/src/handlers/health.rs +++ b/src/handlers/health.rs @@ -1,19 +1,19 @@ -use axum::{Json, response::IntoResponse}; -use serde::Serialize; - -#[derive(Debug, Serialize)] -pub struct HealthResponse { - pub status: String, - pub version: String, - pub timestamp: String, -} - -pub async fn health_check() -> impl IntoResponse { - let response = HealthResponse { - status: "ok".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - timestamp: chrono::Utc::now().to_rfc3339(), - }; - - Json(response) +use axum::{Json, response::IntoResponse}; +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct HealthResponse { + pub status: String, + pub version: String, + pub timestamp: String, +} + +pub async fn health_check() -> impl IntoResponse { + let response = HealthResponse { + status: "ok".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + timestamp: chrono::Utc::now().to_rfc3339(), + }; + + Json(response) } \ No newline at end of file diff --git a/src/handlers/match_room.rs b/src/handlers/match_room.rs index 99382f8..ef2b811 100644 --- a/src/handlers/match_room.rs +++ b/src/handlers/match_room.rs @@ -1,294 +1,294 @@ -use axum::{ - Json, - extract::{Path, Query, State}, -}; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, - QueryOrder, Set, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - database::Database, - entities::{ - multiplayer_room, multiplayer_solo_room, multiplayer_teamvs_room, prelude::*, team, user, - }, - utils::response::{ApiResponse, ApiResult}, -}; - -#[derive(Debug, Deserialize)] -pub struct GetMatchRoomsQuery { - page: Option, - limit: Option, - room_type: Option, - season: Option, - category: Option, -} - -#[derive(Debug, Serialize)] -pub struct MatchRoomResponse { - room_id: i32, - room_type: String, - season: String, - category: String, - multiplayer_solo_room_room_id: Option, - multiplayer_teamvs_room_room_id: Option, - score_red: i32, - score_blue: i32, - start_time: chrono::DateTime, - end_time: chrono::DateTime, - created_at: chrono::DateTime, - updated_at: chrono::DateTime, -} - -impl From for MatchRoomResponse { - fn from(model: multiplayer_room::Model) -> Self { - Self { - room_id: model.room_id, - room_type: model.r#type.to_string(), - season: model.season.to_string(), - category: model.category.to_string(), - multiplayer_solo_room_room_id: model.multiplayer_solo_room_room_id, - multiplayer_teamvs_room_room_id: model.multiplayer_teamvs_room_room_id, - score_red: model.score_red, - score_blue: model.score_blue, - start_time: model.start_time, - end_time: model.end_time, - created_at: model.created_at, - updated_at: model.updated_at, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct CreateMatchRoomRequest { - room_type: String, - season: String, - category: String, - multiplayer_solo_room_room_id: Option, - multiplayer_teamvs_room_room_id: Option, - score_red: i32, - score_blue: i32, - start_time: chrono::DateTime, - end_time: chrono::DateTime, -} - -#[derive(Debug, Deserialize)] -pub struct UpdateMatchRoomRequest { - score_red: Option, - score_blue: Option, - start_time: Option>, - end_time: Option>, -} - -/// 获取比赛房间列表 -pub async fn get_match_rooms( - State(db): State, - Query(query): Query, -) -> ApiResult> { - let conn = db.get_connection(); - - let page = query.page.unwrap_or(1); - let limit = query.limit.unwrap_or(20); - let offset = (page - 1) * limit; - - let mut query = MultiplayerRoom::find().order_by_desc(multiplayer_room::Column::CreatedAt); - - if let Some(room_type) = query.room_type { - query = query.filter(multiplayer_room::Column::Type.eq(room_type)); - } - - if let Some(season) = query.season { - query = query.filter(multiplayer_room::Column::Season.eq(season)); - } - - if let Some(category) = query.category { - query = query.filter(multiplayer_room::Column::Category.eq(category)); - } - - let rooms = query - .offset(offset) - .limit(limit) - .all(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch match rooms: {}", e); - ApiResponse::internal_server_error("Failed to fetch match rooms") - })?; - - let response = rooms.into_iter().map(MatchRoomResponse::from).collect(); - Ok(ApiResponse::success(response)) -} - -/// 获取单个比赛房间 -pub async fn get_match_room( - State(db): State, - Path(room_id): Path, -) -> ApiResult { - let conn = db.get_connection(); - - let room = MultiplayerRoom::find_by_id(room_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch match room: {}", e); - ApiResponse::internal_server_error("Failed to fetch match room") - })? - .ok_or_else(|| ApiResponse::not_found("Match room not found"))?; - - Ok(ApiResponse::success(MatchRoomResponse::from(room))) -} - -/// 创建比赛房间 -pub async fn create_match_room( - State(db): State, - Json(payload): Json, -) -> ApiResult { - let conn = db.get_connection(); - - // 验证房间类型 - let room_type = match payload.room_type.as_str() { - "SOLO" => multiplayer_room::MultiplayerRoomType::Solo, - "TEAM_VS" => multiplayer_room::MultiplayerRoomType::TeamVs, - _ => return Err(ApiResponse::bad_request("Invalid room type")), - }; - - // 验证赛季 - let season = match payload.season.as_str() { - "S1" => multiplayer_room::Season::S1, - "S2" => multiplayer_room::Season::S2, - _ => return Err(ApiResponse::bad_request("Invalid season")), - }; - - // 验证类别 - let category = match payload.category.as_str() { - "QUA" => multiplayer_room::Category::Qua, - "RO16" => multiplayer_room::Category::Ro16, - "QF" => multiplayer_room::Category::Qf, - "SF" => multiplayer_room::Category::Sf, - "F" => multiplayer_room::Category::F, - "GF" => multiplayer_room::Category::Gf, - _ => return Err(ApiResponse::bad_request("Invalid category")), - }; - - // 创建房间 - let room = multiplayer_room::ActiveModel { - r#type: Set(room_type), - season: Set(season), - category: Set(category), - multiplayer_solo_room_room_id: Set(payload.multiplayer_solo_room_room_id), - multiplayer_teamvs_room_room_id: Set(payload.multiplayer_teamvs_room_room_id), - score_red: Set(payload.score_red), - score_blue: Set(payload.score_blue), - start_time: Set(payload.start_time), - end_time: Set(payload.end_time), - ..Default::default() - }; - - let room = room.insert(conn).await.map_err(|e| { - tracing::error!("Failed to create match room: {}", e); - ApiResponse::internal_server_error("Failed to create match room") - })?; - - Ok(ApiResponse::success(MatchRoomResponse::from(room))) -} - -/// 更新比赛房间 -pub async fn update_match_room( - State(db): State, - Path(room_id): Path, - Json(payload): Json, -) -> ApiResult { - let conn = db.get_connection(); - - let mut room: multiplayer_room::ActiveModel = MultiplayerRoom::find_by_id(room_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch match room: {}", e); - ApiResponse::internal_server_error("Failed to fetch match room") - })? - .ok_or_else(|| ApiResponse::not_found("Match room not found"))? - .into(); - - if let Some(score_red) = payload.score_red { - room.score_red = Set(score_red); - } - - if let Some(score_blue) = payload.score_blue { - room.score_blue = Set(score_blue); - } - - if let Some(start_time) = payload.start_time { - room.start_time = Set(start_time); - } - - if let Some(end_time) = payload.end_time { - room.end_time = Set(end_time); - } - - let room = room.update(conn).await.map_err(|e| { - tracing::error!("Failed to update match room: {}", e); - ApiResponse::internal_server_error("Failed to update match room") - })?; - - Ok(ApiResponse::success(MatchRoomResponse::from(room))) -} - -/// 删除比赛房间 -pub async fn delete_match_room( - State(db): State, - Path(room_id): Path, -) -> ApiResult<()> { - let conn = db.get_connection(); - - let room = MultiplayerRoom::find_by_id(room_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch match room: {}", e); - ApiResponse::internal_server_error("Failed to fetch match room") - })? - .ok_or_else(|| ApiResponse::not_found("Match room not found"))?; - - MultiplayerRoom::delete_by_id(room.room_id) - .exec(conn) - .await - .map_err(|e| { - tracing::error!("Failed to delete match room: {}", e); - ApiResponse::internal_server_error("Failed to delete match room") - })?; - - Ok(ApiResponse::success_message( - "Match room deleted successfully", - )) -} - -/// 获取比赛房间总数 -pub async fn get_match_rooms_count( - State(db): State, - Query(query): Query, -) -> ApiResult { - let conn = db.get_connection(); - - let mut query = MultiplayerRoom::find(); - - if let Some(room_type) = query.room_type { - query = query.filter(multiplayer_room::Column::Type.eq(room_type)); - } - - if let Some(season) = query.season { - query = query.filter(multiplayer_room::Column::Season.eq(season)); - } - - if let Some(category) = query.category { - query = query.filter(multiplayer_room::Column::Category.eq(category)); - } - - let count = query.count(conn).await.map_err(|e| { - tracing::error!("Failed to count match rooms: {}", e); - ApiResponse::internal_server_error("Failed to count match rooms") - })?; - - Ok(ApiResponse::success(count)) -} +use axum::{ + Json, + extract::{Path, Query, State}, +}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, + QueryOrder, Set, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + database::Database, + entities::{ + multiplayer_room, multiplayer_solo_room, multiplayer_teamvs_room, prelude::*, team, user, + }, + utils::response::{ApiResponse, ApiResult}, +}; + +#[derive(Debug, Deserialize)] +pub struct GetMatchRoomsQuery { + page: Option, + limit: Option, + room_type: Option, + season: Option, + category: Option, +} + +#[derive(Debug, Serialize)] +pub struct MatchRoomResponse { + room_id: i32, + room_type: String, + season: String, + category: String, + multiplayer_solo_room_room_id: Option, + multiplayer_teamvs_room_room_id: Option, + score_red: i32, + score_blue: i32, + start_time: chrono::DateTime, + end_time: chrono::DateTime, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, +} + +impl From for MatchRoomResponse { + fn from(model: multiplayer_room::Model) -> Self { + Self { + room_id: model.room_id, + room_type: model.r#type.to_string(), + season: model.season.to_string(), + category: model.category.to_string(), + multiplayer_solo_room_room_id: model.multiplayer_solo_room_room_id, + multiplayer_teamvs_room_room_id: model.multiplayer_teamvs_room_room_id, + score_red: model.score_red, + score_blue: model.score_blue, + start_time: model.start_time, + end_time: model.end_time, + created_at: model.created_at, + updated_at: model.updated_at, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct CreateMatchRoomRequest { + room_type: String, + season: String, + category: String, + multiplayer_solo_room_room_id: Option, + multiplayer_teamvs_room_room_id: Option, + score_red: i32, + score_blue: i32, + start_time: chrono::DateTime, + end_time: chrono::DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateMatchRoomRequest { + score_red: Option, + score_blue: Option, + start_time: Option>, + end_time: Option>, +} + +/// 获取比赛房间列表 +pub async fn get_match_rooms( + State(db): State, + Query(query): Query, +) -> ApiResult> { + let conn = db.get_connection(); + + let page = query.page.unwrap_or(1); + let limit = query.limit.unwrap_or(20); + let offset = (page - 1) * limit; + + let mut query = MultiplayerRoom::find().order_by_desc(multiplayer_room::Column::CreatedAt); + + if let Some(room_type) = query.room_type { + query = query.filter(multiplayer_room::Column::Type.eq(room_type)); + } + + if let Some(season) = query.season { + query = query.filter(multiplayer_room::Column::Season.eq(season)); + } + + if let Some(category) = query.category { + query = query.filter(multiplayer_room::Column::Category.eq(category)); + } + + let rooms = query + .offset(offset) + .limit(limit) + .all(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch match rooms: {}", e); + ApiResponse::internal_server_error("Failed to fetch match rooms") + })?; + + let response = rooms.into_iter().map(MatchRoomResponse::from).collect(); + Ok(ApiResponse::success(response)) +} + +/// 获取单个比赛房间 +pub async fn get_match_room( + State(db): State, + Path(room_id): Path, +) -> ApiResult { + let conn = db.get_connection(); + + let room = MultiplayerRoom::find_by_id(room_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch match room: {}", e); + ApiResponse::internal_server_error("Failed to fetch match room") + })? + .ok_or_else(|| ApiResponse::not_found("Match room not found"))?; + + Ok(ApiResponse::success(MatchRoomResponse::from(room))) +} + +/// 创建比赛房间 +pub async fn create_match_room( + State(db): State, + Json(payload): Json, +) -> ApiResult { + let conn = db.get_connection(); + + // 验证房间类型 + let room_type = match payload.room_type.as_str() { + "SOLO" => multiplayer_room::MultiplayerRoomType::Solo, + "TEAM_VS" => multiplayer_room::MultiplayerRoomType::TeamVs, + _ => return Err(ApiResponse::bad_request("Invalid room type")), + }; + + // 验证赛季 + let season = match payload.season.as_str() { + "S1" => multiplayer_room::Season::S1, + "S2" => multiplayer_room::Season::S2, + _ => return Err(ApiResponse::bad_request("Invalid season")), + }; + + // 验证类别 + let category = match payload.category.as_str() { + "QUA" => multiplayer_room::Category::Qua, + "RO16" => multiplayer_room::Category::Ro16, + "QF" => multiplayer_room::Category::Qf, + "SF" => multiplayer_room::Category::Sf, + "F" => multiplayer_room::Category::F, + "GF" => multiplayer_room::Category::Gf, + _ => return Err(ApiResponse::bad_request("Invalid category")), + }; + + // 创建房间 + let room = multiplayer_room::ActiveModel { + r#type: Set(room_type), + season: Set(season), + category: Set(category), + multiplayer_solo_room_room_id: Set(payload.multiplayer_solo_room_room_id), + multiplayer_teamvs_room_room_id: Set(payload.multiplayer_teamvs_room_room_id), + score_red: Set(payload.score_red), + score_blue: Set(payload.score_blue), + start_time: Set(payload.start_time), + end_time: Set(payload.end_time), + ..Default::default() + }; + + let room = room.insert(conn).await.map_err(|e| { + tracing::error!("Failed to create match room: {}", e); + ApiResponse::internal_server_error("Failed to create match room") + })?; + + Ok(ApiResponse::success(MatchRoomResponse::from(room))) +} + +/// 更新比赛房间 +pub async fn update_match_room( + State(db): State, + Path(room_id): Path, + Json(payload): Json, +) -> ApiResult { + let conn = db.get_connection(); + + let mut room: multiplayer_room::ActiveModel = MultiplayerRoom::find_by_id(room_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch match room: {}", e); + ApiResponse::internal_server_error("Failed to fetch match room") + })? + .ok_or_else(|| ApiResponse::not_found("Match room not found"))? + .into(); + + if let Some(score_red) = payload.score_red { + room.score_red = Set(score_red); + } + + if let Some(score_blue) = payload.score_blue { + room.score_blue = Set(score_blue); + } + + if let Some(start_time) = payload.start_time { + room.start_time = Set(start_time); + } + + if let Some(end_time) = payload.end_time { + room.end_time = Set(end_time); + } + + let room = room.update(conn).await.map_err(|e| { + tracing::error!("Failed to update match room: {}", e); + ApiResponse::internal_server_error("Failed to update match room") + })?; + + Ok(ApiResponse::success(MatchRoomResponse::from(room))) +} + +/// 删除比赛房间 +pub async fn delete_match_room( + State(db): State, + Path(room_id): Path, +) -> ApiResult<()> { + let conn = db.get_connection(); + + let room = MultiplayerRoom::find_by_id(room_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch match room: {}", e); + ApiResponse::internal_server_error("Failed to fetch match room") + })? + .ok_or_else(|| ApiResponse::not_found("Match room not found"))?; + + MultiplayerRoom::delete_by_id(room.room_id) + .exec(conn) + .await + .map_err(|e| { + tracing::error!("Failed to delete match room: {}", e); + ApiResponse::internal_server_error("Failed to delete match room") + })?; + + Ok(ApiResponse::success_message( + "Match room deleted successfully", + )) +} + +/// 获取比赛房间总数 +pub async fn get_match_rooms_count( + State(db): State, + Query(query): Query, +) -> ApiResult { + let conn = db.get_connection(); + + let mut query = MultiplayerRoom::find(); + + if let Some(room_type) = query.room_type { + query = query.filter(multiplayer_room::Column::Type.eq(room_type)); + } + + if let Some(season) = query.season { + query = query.filter(multiplayer_room::Column::Season.eq(season)); + } + + if let Some(category) = query.category { + query = query.filter(multiplayer_room::Column::Category.eq(category)); + } + + let count = query.count(conn).await.map_err(|e| { + tracing::error!("Failed to count match rooms: {}", e); + ApiResponse::internal_server_error("Failed to count match rooms") + })?; + + Ok(ApiResponse::success(count)) +} diff --git a/src/handlers/message.rs b/src/handlers/message.rs index acc05be..23f8984 100644 --- a/src/handlers/message.rs +++ b/src/handlers/message.rs @@ -1,317 +1,317 @@ -use axum::{ - Json, - extract::{Path, Query, State}, -}; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, - QueryOrder, Set, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - database::Database, - entities::{message, multiplayer_room, prelude::*, user}, - utils::response::{ApiResponse, ApiResult}, -}; - -#[derive(Debug, Deserialize)] -pub struct GetMessagesQuery { - page: Option, - limit: Option, - receiver_id: Option, - sender_id: Option, - room_id: Option, - is_read: Option, - is_system_message: Option, -} - -#[derive(Debug, Serialize)] -pub struct MessageResponse { - id: i32, - title: String, - content: String, - sender_id: i32, - receiver_id: i32, - room_id: Option, - is_read: bool, - is_deleted: bool, - is_system_message: bool, - is_accepted: bool, - created_at: chrono::DateTime, - updated_at: chrono::DateTime, -} - -impl From for MessageResponse { - fn from(model: message::Model) -> Self { - Self { - id: model.id, - title: model.title, - content: model.content, - sender_id: model.sender_id, - receiver_id: model.receiver_id, - room_id: model.room_id, - is_read: model.is_read, - is_deleted: model.is_deleted, - is_system_message: model.is_system_message, - is_accepted: model.is_accepted, - created_at: model.created_at, - updated_at: model.updated_at, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct CreateMessageRequest { - title: String, - content: String, - receiver_id: i32, - room_id: Option, - is_system_message: Option, -} - -#[derive(Debug, Deserialize)] -pub struct UpdateMessageRequest { - is_read: Option, - is_accepted: Option, -} - -/// 获取消息列表 -pub async fn get_messages( - State(db): State, - Query(query): Query, -) -> ApiResult> { - let conn = db.get_connection(); - - let page = query.page.unwrap_or(1); - let limit = query.limit.unwrap_or(20); - let offset = (page - 1) * limit; - - let mut query = Message::find() - .filter(message::Column::IsDeleted.eq(false)) - .order_by_desc(message::Column::CreatedAt); - - if let Some(receiver_id) = query.receiver_id { - query = query.filter(message::Column::ReceiverId.eq(receiver_id)); - } - - if let Some(sender_id) = query.sender_id { - query = query.filter(message::Column::SenderId.eq(sender_id)); - } - - if let Some(room_id) = query.room_id { - query = query.filter(message::Column::RoomId.eq(room_id)); - } - - if let Some(is_read) = query.is_read { - query = query.filter(message::Column::IsRead.eq(is_read)); - } - - if let Some(is_system_message) = query.is_system_message { - query = query.filter(message::Column::IsSystemMessage.eq(is_system_message)); - } - - let messages = query - .offset(offset) - .limit(limit) - .all(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch messages: {}", e); - ApiResponse::internal_server_error("Failed to fetch messages") - })?; - - let response = messages.into_iter().map(MessageResponse::from).collect(); - Ok(ApiResponse::success(response)) -} - -/// 获取单个消息 -pub async fn get_message( - State(db): State, - Path(message_id): Path, -) -> ApiResult { - let conn = db.get_connection(); - - let message = Message::find_by_id(message_id) - .filter(message::Column::IsDeleted.eq(false)) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch message: {}", e); - ApiResponse::internal_server_error("Failed to fetch message") - })? - .ok_or_else(|| ApiResponse::not_found("Message not found"))?; - - Ok(ApiResponse::success(MessageResponse::from(message))) -} - -/// 发送消息 -pub async fn send_message( - State(db): State, - Json(payload): Json, -) -> ApiResult { - let conn = db.get_connection(); - - // 验证接收者是否存在 - let receiver = User::find() - .filter(user::Column::Osuid.eq(payload.receiver_id)) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to verify receiver: {}", e); - ApiResponse::internal_server_error("Failed to verify receiver") - })? - .ok_or_else(|| ApiResponse::not_found("Receiver not found"))?; - - // 验证房间是否存在(如果提供了room_id) - if let Some(room_id) = payload.room_id { - let room = MultiplayerRoom::find_by_id(room_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to verify room: {}", e); - ApiResponse::internal_server_error("Failed to verify room") - })?; - - if room.is_none() { - return Err(ApiResponse::not_found("Room not found")); - } - } - - // 创建消息 - let message = message::ActiveModel { - title: Set(payload.title), - content: Set(payload.content), - receiver_id: Set(payload.receiver_id), - room_id: Set(payload.room_id), - is_system_message: Set(payload.is_system_message.unwrap_or(false)), - ..Default::default() - }; - - let message = message.insert(conn).await.map_err(|e| { - tracing::error!("Failed to send message: {}", e); - ApiResponse::internal_server_error("Failed to send message") - })?; - - Ok(ApiResponse::success(MessageResponse::from(message))) -} - -/// 更新消息状态(标记为已读/接受) -pub async fn update_message( - State(db): State, - Path(message_id): Path, - Json(payload): Json, -) -> ApiResult { - let conn = db.get_connection(); - - let mut message: message::ActiveModel = Message::find_by_id(message_id) - .filter(message::Column::IsDeleted.eq(false)) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch message: {}", e); - ApiResponse::internal_server_error("Failed to fetch message") - })? - .ok_or_else(|| ApiResponse::not_found("Message not found"))? - .into(); - - if let Some(is_read) = payload.is_read { - message.is_read = Set(is_read); - } - - if let Some(is_accepted) = payload.is_accepted { - message.is_accepted = Set(is_accepted); - } - - let message = message.update(conn).await.map_err(|e| { - tracing::error!("Failed to update message: {}", e); - ApiResponse::internal_server_error("Failed to update message") - })?; - - Ok(ApiResponse::success(MessageResponse::from(message))) -} - -/// 删除消息(软删除) -pub async fn delete_message( - State(db): State, - Path(message_id): Path, -) -> ApiResult<()> { - let conn = db.get_connection(); - - let mut message: message::ActiveModel = Message::find_by_id(message_id) - .filter(message::Column::IsDeleted.eq(false)) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch message: {}", e); - ApiResponse::internal_server_error("Failed to fetch message") - })? - .ok_or_else(|| ApiResponse::not_found("Message not found"))? - .into(); - - message.is_deleted = Set(true); - - message.update(conn).await.map_err(|e| { - tracing::error!("Failed to delete message: {}", e); - ApiResponse::internal_server_error("Failed to delete message") - })?; - - Ok(ApiResponse::success_message("Message deleted successfully")) -} - -/// 获取消息总数 -pub async fn get_messages_count( - State(db): State, - Query(query): Query, -) -> ApiResult { - let conn = db.get_connection(); - - let mut query = Message::find().filter(message::Column::IsDeleted.eq(false)); - - if let Some(receiver_id) = query.receiver_id { - query = query.filter(message::Column::ReceiverId.eq(receiver_id)); - } - - if let Some(sender_id) = query.sender_id { - query = query.filter(message::Column::SenderId.eq(sender_id)); - } - - if let Some(room_id) = query.room_id { - query = query.filter(message::Column::RoomId.eq(room_id)); - } - - if let Some(is_read) = query.is_read { - query = query.filter(message::Column::IsRead.eq(is_read)); - } - - if let Some(is_system_message) = query.is_system_message { - query = query.filter(message::Column::IsSystemMessage.eq(is_system_message)); - } - - let count = query.count(conn).await.map_err(|e| { - tracing::error!("Failed to count messages: {}", e); - ApiResponse::internal_server_error("Failed to count messages") - })?; - - Ok(ApiResponse::success(count)) -} - -/// 获取未读消息数量 -pub async fn get_unread_messages_count( - State(db): State, - Path(receiver_id): Path, -) -> ApiResult { - let conn = db.get_connection(); - - let count = Message::find() - .filter(message::Column::ReceiverId.eq(receiver_id)) - .filter(message::Column::IsRead.eq(false)) - .filter(message::Column::IsDeleted.eq(false)) - .count(conn) - .await - .map_err(|e| { - tracing::error!("Failed to count unread messages: {}", e); - ApiResponse::internal_server_error("Failed to count unread messages") - })?; - - Ok(ApiResponse::success(count)) -} +use axum::{ + Json, + extract::{Path, Query, State}, +}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, + QueryOrder, Set, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + database::Database, + entities::{message, multiplayer_room, prelude::*, user}, + utils::response::{ApiResponse, ApiResult}, +}; + +#[derive(Debug, Deserialize)] +pub struct GetMessagesQuery { + page: Option, + limit: Option, + receiver_id: Option, + sender_id: Option, + room_id: Option, + is_read: Option, + is_system_message: Option, +} + +#[derive(Debug, Serialize)] +pub struct MessageResponse { + id: i32, + title: String, + content: String, + sender_id: i32, + receiver_id: i32, + room_id: Option, + is_read: bool, + is_deleted: bool, + is_system_message: bool, + is_accepted: bool, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, +} + +impl From for MessageResponse { + fn from(model: message::Model) -> Self { + Self { + id: model.id, + title: model.title, + content: model.content, + sender_id: model.sender_id, + receiver_id: model.receiver_id, + room_id: model.room_id, + is_read: model.is_read, + is_deleted: model.is_deleted, + is_system_message: model.is_system_message, + is_accepted: model.is_accepted, + created_at: model.created_at, + updated_at: model.updated_at, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct CreateMessageRequest { + title: String, + content: String, + receiver_id: i32, + room_id: Option, + is_system_message: Option, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateMessageRequest { + is_read: Option, + is_accepted: Option, +} + +/// 获取消息列表 +pub async fn get_messages( + State(db): State, + Query(query): Query, +) -> ApiResult> { + let conn = db.get_connection(); + + let page = query.page.unwrap_or(1); + let limit = query.limit.unwrap_or(20); + let offset = (page - 1) * limit; + + let mut query = Message::find() + .filter(message::Column::IsDeleted.eq(false)) + .order_by_desc(message::Column::CreatedAt); + + if let Some(receiver_id) = query.receiver_id { + query = query.filter(message::Column::ReceiverId.eq(receiver_id)); + } + + if let Some(sender_id) = query.sender_id { + query = query.filter(message::Column::SenderId.eq(sender_id)); + } + + if let Some(room_id) = query.room_id { + query = query.filter(message::Column::RoomId.eq(room_id)); + } + + if let Some(is_read) = query.is_read { + query = query.filter(message::Column::IsRead.eq(is_read)); + } + + if let Some(is_system_message) = query.is_system_message { + query = query.filter(message::Column::IsSystemMessage.eq(is_system_message)); + } + + let messages = query + .offset(offset) + .limit(limit) + .all(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch messages: {}", e); + ApiResponse::internal_server_error("Failed to fetch messages") + })?; + + let response = messages.into_iter().map(MessageResponse::from).collect(); + Ok(ApiResponse::success(response)) +} + +/// 获取单个消息 +pub async fn get_message( + State(db): State, + Path(message_id): Path, +) -> ApiResult { + let conn = db.get_connection(); + + let message = Message::find_by_id(message_id) + .filter(message::Column::IsDeleted.eq(false)) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch message: {}", e); + ApiResponse::internal_server_error("Failed to fetch message") + })? + .ok_or_else(|| ApiResponse::not_found("Message not found"))?; + + Ok(ApiResponse::success(MessageResponse::from(message))) +} + +/// 发送消息 +pub async fn send_message( + State(db): State, + Json(payload): Json, +) -> ApiResult { + let conn = db.get_connection(); + + // 验证接收者是否存在 + let receiver = User::find() + .filter(user::Column::Osuid.eq(payload.receiver_id)) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to verify receiver: {}", e); + ApiResponse::internal_server_error("Failed to verify receiver") + })? + .ok_or_else(|| ApiResponse::not_found("Receiver not found"))?; + + // 验证房间是否存在(如果提供了room_id) + if let Some(room_id) = payload.room_id { + let room = MultiplayerRoom::find_by_id(room_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to verify room: {}", e); + ApiResponse::internal_server_error("Failed to verify room") + })?; + + if room.is_none() { + return Err(ApiResponse::not_found("Room not found")); + } + } + + // 创建消息 + let message = message::ActiveModel { + title: Set(payload.title), + content: Set(payload.content), + receiver_id: Set(payload.receiver_id), + room_id: Set(payload.room_id), + is_system_message: Set(payload.is_system_message.unwrap_or(false)), + ..Default::default() + }; + + let message = message.insert(conn).await.map_err(|e| { + tracing::error!("Failed to send message: {}", e); + ApiResponse::internal_server_error("Failed to send message") + })?; + + Ok(ApiResponse::success(MessageResponse::from(message))) +} + +/// 更新消息状态(标记为已读/接受) +pub async fn update_message( + State(db): State, + Path(message_id): Path, + Json(payload): Json, +) -> ApiResult { + let conn = db.get_connection(); + + let mut message: message::ActiveModel = Message::find_by_id(message_id) + .filter(message::Column::IsDeleted.eq(false)) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch message: {}", e); + ApiResponse::internal_server_error("Failed to fetch message") + })? + .ok_or_else(|| ApiResponse::not_found("Message not found"))? + .into(); + + if let Some(is_read) = payload.is_read { + message.is_read = Set(is_read); + } + + if let Some(is_accepted) = payload.is_accepted { + message.is_accepted = Set(is_accepted); + } + + let message = message.update(conn).await.map_err(|e| { + tracing::error!("Failed to update message: {}", e); + ApiResponse::internal_server_error("Failed to update message") + })?; + + Ok(ApiResponse::success(MessageResponse::from(message))) +} + +/// 删除消息(软删除) +pub async fn delete_message( + State(db): State, + Path(message_id): Path, +) -> ApiResult<()> { + let conn = db.get_connection(); + + let mut message: message::ActiveModel = Message::find_by_id(message_id) + .filter(message::Column::IsDeleted.eq(false)) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch message: {}", e); + ApiResponse::internal_server_error("Failed to fetch message") + })? + .ok_or_else(|| ApiResponse::not_found("Message not found"))? + .into(); + + message.is_deleted = Set(true); + + message.update(conn).await.map_err(|e| { + tracing::error!("Failed to delete message: {}", e); + ApiResponse::internal_server_error("Failed to delete message") + })?; + + Ok(ApiResponse::success_message("Message deleted successfully")) +} + +/// 获取消息总数 +pub async fn get_messages_count( + State(db): State, + Query(query): Query, +) -> ApiResult { + let conn = db.get_connection(); + + let mut query = Message::find().filter(message::Column::IsDeleted.eq(false)); + + if let Some(receiver_id) = query.receiver_id { + query = query.filter(message::Column::ReceiverId.eq(receiver_id)); + } + + if let Some(sender_id) = query.sender_id { + query = query.filter(message::Column::SenderId.eq(sender_id)); + } + + if let Some(room_id) = query.room_id { + query = query.filter(message::Column::RoomId.eq(room_id)); + } + + if let Some(is_read) = query.is_read { + query = query.filter(message::Column::IsRead.eq(is_read)); + } + + if let Some(is_system_message) = query.is_system_message { + query = query.filter(message::Column::IsSystemMessage.eq(is_system_message)); + } + + let count = query.count(conn).await.map_err(|e| { + tracing::error!("Failed to count messages: {}", e); + ApiResponse::internal_server_error("Failed to count messages") + })?; + + Ok(ApiResponse::success(count)) +} + +/// 获取未读消息数量 +pub async fn get_unread_messages_count( + State(db): State, + Path(receiver_id): Path, +) -> ApiResult { + let conn = db.get_connection(); + + let count = Message::find() + .filter(message::Column::ReceiverId.eq(receiver_id)) + .filter(message::Column::IsRead.eq(false)) + .filter(message::Column::IsDeleted.eq(false)) + .count(conn) + .await + .map_err(|e| { + tracing::error!("Failed to count unread messages: {}", e); + ApiResponse::internal_server_error("Failed to count unread messages") + })?; + + Ok(ApiResponse::success(count)) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index b09c16e..f4f2e68 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,8 +1,8 @@ -pub mod auth; -pub mod health; -pub mod match_room; -pub mod message; -pub mod registration; -pub mod team; -pub mod tournament_config; -pub mod user; +pub mod auth; +pub mod health; +pub mod match_room; +pub mod message; +pub mod registration; +pub mod team; +pub mod tournament_config; +pub mod user; diff --git a/src/handlers/registration.rs b/src/handlers/registration.rs index 60f087e..dd4ffe3 100644 --- a/src/handlers/registration.rs +++ b/src/handlers/registration.rs @@ -1,222 +1,222 @@ -use axum::{ - Json, - extract::{Path, Query, State}, -}; -use sea_orm::{ - ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - database::Database, - entities::{prelude::*, user}, - utils::response::{ApiResponse, ApiResult}, -}; - -#[derive(Debug, Deserialize)] -pub struct GetRegistrationsQuery { - page: Option, - limit: Option, - approved: Option, - seasonal: Option, - user_state: Option, -} - -#[derive(Debug, Serialize)] -pub struct RegistrationResponse { - id: i32, - osuid: i32, - username: String, - avatar_url: Option, - cover_url: Option, - country_code: String, - pp: f64, - global_rank: i32, - country_rank: i32, - approved: bool, - seed: i32, - seasonal: String, - user_state: String, - created_at: chrono::DateTime, - updated_at: chrono::DateTime, -} - -impl From for RegistrationResponse { - fn from(model: user::Model) -> Self { - Self { - id: model.id, - osuid: model.osuid, - username: model.username, - avatar_url: model.avatar_url, - cover_url: model.cover_url, - country_code: model.country_code, - pp: model.pp, - global_rank: model.global_rank, - country_rank: model.country_rank, - approved: model.approved, - seed: model.seed, - seasonal: model.seasonal.into(), - user_state: model.user_state.to_string(), - created_at: model.created_at, - updated_at: model.updated_at, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct ApproveRegistrationRequest { - approved: bool, -} - -/// 获取报名列表 -pub async fn get_registrations( - State(db): State, - Query(query): Query, -) -> ApiResult> { - let conn = db.get_connection(); - - let page = query.page.unwrap_or(1); - let limit = query.limit.unwrap_or(20); - let offset = (page - 1) * limit; - - let mut query = User::find().order_by_asc(user::Column::Id); - - if let Some(approved) = query.approved { - query = query.filter(user::Column::Approved.eq(approved)); - } - - if let Some(seasonal) = query.seasonal { - query = query.filter(user::Column::Seasonal.eq(seasonal)); - } - - if let Some(user_state) = query.user_state { - query = query.filter(user::Column::UserState.eq(user_state)); - } - - let users = query - .offset(offset) - .limit(limit) - .all(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch registrations: {}", e); - ApiResponse::internal_server_error("Failed to fetch registrations") - })?; - - let response = users.into_iter().map(RegistrationResponse::from).collect(); - Ok(ApiResponse::success(response)) -} - -/// 获取已批准的玩家 -pub async fn get_approved_players( - State(db): State, - Query(query): Query, -) -> ApiResult> { - let conn = db.get_connection(); - - let page = query.page.unwrap_or(1); - let limit = query.limit.unwrap_or(20); - let offset = (page - 1) * limit; - - let users = User::find() - .filter(user::Column::Approved.eq(true)) - .order_by_asc(user::Column::Id) - .offset(offset) - .limit(limit) - .all(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch approved players: {}", e); - ApiResponse::internal_server_error("Failed to fetch approved players") - })?; - - let response = users.into_iter().map(RegistrationResponse::from).collect(); - Ok(ApiResponse::success(response)) -} - -/// 批准或拒绝报名 -pub async fn approve_registration( - State(db): State, - Path(user_id): Path, - Json(payload): Json, -) -> ApiResult { - let conn = db.get_connection(); - - let mut user: user::ActiveModel = User::find_by_id(user_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch user: {}", e); - ApiResponse::internal_server_error("Failed to fetch user") - })? - .ok_or_else(|| ApiResponse::not_found("User not found"))? - .into(); - - user.approved = sea_orm::Set(payload.approved); - - let user = user.update(conn).await.map_err(|e| { - tracing::error!("Failed to update user approval status: {}", e); - ApiResponse::internal_server_error("Failed to update approval status") - })?; - - Ok(ApiResponse::success(RegistrationResponse::from(user))) -} - -/// 删除报名 -pub async fn delete_registration( - State(db): State, - Path(user_id): Path, -) -> ApiResult<()> { - let conn = db.get_connection(); - - let user = User::find_by_id(user_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch user: {}", e); - ApiResponse::internal_server_error("Failed to fetch user") - })? - .ok_or_else(|| ApiResponse::not_found("User not found"))?; - - // 软删除:将用户状态设置为BANNED - let mut user_model: user::ActiveModel = user.into(); - user_model.user_state = sea_orm::Set(user::UserState::Banned); - - user_model.update(conn).await.map_err(|e| { - tracing::error!("Failed to delete registration: {}", e); - ApiResponse::internal_server_error("Failed to delete registration") - })?; - - Ok(ApiResponse::success_message( - "Registration deleted successfully", - )) -} - -/// 获取报名总数 -pub async fn get_registrations_count( - State(db): State, - Query(query): Query, -) -> ApiResult { - let conn = db.get_connection(); - - let mut query = User::find(); - - if let Some(approved) = query.approved { - query = query.filter(user::Column::Approved.eq(approved)); - } - - if let Some(seasonal) = query.seasonal { - query = query.filter(user::Column::Seasonal.eq(seasonal)); - } - - if let Some(user_state) = query.user_state { - query = query.filter(user::Column::UserState.eq(user_state)); - } - - let count = query.count(conn).await.map_err(|e| { - tracing::error!("Failed to count registrations: {}", e); - ApiResponse::internal_server_error("Failed to count registrations") - })?; - - Ok(ApiResponse::success(count)) -} +use axum::{ + Json, + extract::{Path, Query, State}, +}; +use sea_orm::{ + ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + database::Database, + entities::{prelude::*, user}, + utils::response::{ApiResponse, ApiResult}, +}; + +#[derive(Debug, Deserialize)] +pub struct GetRegistrationsQuery { + page: Option, + limit: Option, + approved: Option, + seasonal: Option, + user_state: Option, +} + +#[derive(Debug, Serialize)] +pub struct RegistrationResponse { + id: i32, + osuid: i32, + username: String, + avatar_url: Option, + cover_url: Option, + country_code: String, + pp: f64, + global_rank: i32, + country_rank: i32, + approved: bool, + seed: i32, + seasonal: String, + user_state: String, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, +} + +impl From for RegistrationResponse { + fn from(model: user::Model) -> Self { + Self { + id: model.id, + osuid: model.osuid, + username: model.username, + avatar_url: model.avatar_url, + cover_url: model.cover_url, + country_code: model.country_code, + pp: model.pp, + global_rank: model.global_rank, + country_rank: model.country_rank, + approved: model.approved, + seed: model.seed, + seasonal: model.seasonal.into(), + user_state: model.user_state.to_string(), + created_at: model.created_at, + updated_at: model.updated_at, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct ApproveRegistrationRequest { + approved: bool, +} + +/// 获取报名列表 +pub async fn get_registrations( + State(db): State, + Query(query): Query, +) -> ApiResult> { + let conn = db.get_connection(); + + let page = query.page.unwrap_or(1); + let limit = query.limit.unwrap_or(20); + let offset = (page - 1) * limit; + + let mut query = User::find().order_by_asc(user::Column::Id); + + if let Some(approved) = query.approved { + query = query.filter(user::Column::Approved.eq(approved)); + } + + if let Some(seasonal) = query.seasonal { + query = query.filter(user::Column::Seasonal.eq(seasonal)); + } + + if let Some(user_state) = query.user_state { + query = query.filter(user::Column::UserState.eq(user_state)); + } + + let users = query + .offset(offset) + .limit(limit) + .all(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch registrations: {}", e); + ApiResponse::internal_server_error("Failed to fetch registrations") + })?; + + let response = users.into_iter().map(RegistrationResponse::from).collect(); + Ok(ApiResponse::success(response)) +} + +/// 获取已批准的玩家 +pub async fn get_approved_players( + State(db): State, + Query(query): Query, +) -> ApiResult> { + let conn = db.get_connection(); + + let page = query.page.unwrap_or(1); + let limit = query.limit.unwrap_or(20); + let offset = (page - 1) * limit; + + let users = User::find() + .filter(user::Column::Approved.eq(true)) + .order_by_asc(user::Column::Id) + .offset(offset) + .limit(limit) + .all(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch approved players: {}", e); + ApiResponse::internal_server_error("Failed to fetch approved players") + })?; + + let response = users.into_iter().map(RegistrationResponse::from).collect(); + Ok(ApiResponse::success(response)) +} + +/// 批准或拒绝报名 +pub async fn approve_registration( + State(db): State, + Path(user_id): Path, + Json(payload): Json, +) -> ApiResult { + let conn = db.get_connection(); + + let mut user: user::ActiveModel = User::find_by_id(user_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch user: {}", e); + ApiResponse::internal_server_error("Failed to fetch user") + })? + .ok_or_else(|| ApiResponse::not_found("User not found"))? + .into(); + + user.approved = sea_orm::Set(payload.approved); + + let user = user.update(conn).await.map_err(|e| { + tracing::error!("Failed to update user approval status: {}", e); + ApiResponse::internal_server_error("Failed to update approval status") + })?; + + Ok(ApiResponse::success(RegistrationResponse::from(user))) +} + +/// 删除报名 +pub async fn delete_registration( + State(db): State, + Path(user_id): Path, +) -> ApiResult<()> { + let conn = db.get_connection(); + + let user = User::find_by_id(user_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch user: {}", e); + ApiResponse::internal_server_error("Failed to fetch user") + })? + .ok_or_else(|| ApiResponse::not_found("User not found"))?; + + // 软删除:将用户状态设置为BANNED + let mut user_model: user::ActiveModel = user.into(); + user_model.user_state = sea_orm::Set(user::UserState::Banned); + + user_model.update(conn).await.map_err(|e| { + tracing::error!("Failed to delete registration: {}", e); + ApiResponse::internal_server_error("Failed to delete registration") + })?; + + Ok(ApiResponse::success_message( + "Registration deleted successfully", + )) +} + +/// 获取报名总数 +pub async fn get_registrations_count( + State(db): State, + Query(query): Query, +) -> ApiResult { + let conn = db.get_connection(); + + let mut query = User::find(); + + if let Some(approved) = query.approved { + query = query.filter(user::Column::Approved.eq(approved)); + } + + if let Some(seasonal) = query.seasonal { + query = query.filter(user::Column::Seasonal.eq(seasonal)); + } + + if let Some(user_state) = query.user_state { + query = query.filter(user::Column::UserState.eq(user_state)); + } + + let count = query.count(conn).await.map_err(|e| { + tracing::error!("Failed to count registrations: {}", e); + ApiResponse::internal_server_error("Failed to count registrations") + })?; + + Ok(ApiResponse::success(count)) +} diff --git a/src/handlers/team.rs b/src/handlers/team.rs index 029f586..2c45386 100644 --- a/src/handlers/team.rs +++ b/src/handlers/team.rs @@ -1,128 +1,128 @@ -use axum::{ - extract::{Path, Query, State}, - Json, -}; -use sea_orm::{DatabaseConnection, EntityTrait, PaginatorTrait, QueryOrder}; -use serde::{Deserialize, Serialize}; - -use crate::{ - database::Database, - entities::{prelude::*, team}, - utils::response::{ApiResponse, ApiResult}, -}; - -#[derive(Debug, Deserialize)] -pub struct GetTeamsQuery { - page: Option, - limit: Option, - team_state: Option, - team_color: Option, -} - -#[derive(Debug, Serialize)] -pub struct TeamResponse { - id: i32, - name: String, - banner_url: String, - team_color: String, - team_state: String, - created_at: chrono::DateTime, - updated_at: chrono::DateTime, -} - -impl From for TeamResponse { - fn from(model: team::Model) -> Self { - Self { - id: model.id, - name: model.name, - banner_url: model.banner_url, - team_color: model.team_color.to_string(), - team_state: model.team_state.to_string(), - created_at: model.created_at, - updated_at: model.updated_at, - } - } -} - -/// 获取队伍列表 -pub async fn get_teams( - State(db): State, - Query(query): Query, -) -> ApiResult> { - let conn = db.get_connection(); - - let page = query.page.unwrap_or(1); - let limit = query.limit.unwrap_or(20); - let offset = (page - 1) * limit; - - let mut query = Team::find() - .order_by_asc(team::Column::Id); - - if let Some(team_state) = query.team_state { - query = query.filter(team::Column::TeamState.eq(team_state)); - } - - if let Some(team_color) = query.team_color { - query = query.filter(team::Column::TeamColor.eq(team_color)); - } - - let teams = query - .offset(offset) - .limit(limit) - .all(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch teams: {}", e); - ApiResponse::internal_server_error("Failed to fetch teams") - })?; - - let response = teams.into_iter().map(TeamResponse::from).collect(); - Ok(ApiResponse::success(response)) -} - -/// 获取单个队伍 -pub async fn get_team( - State(db): State, - Path(team_id): Path, -) -> ApiResult { - let conn = db.get_connection(); - - let team = Team::find_by_id(team_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch team: {}", e); - ApiResponse::internal_server_error("Failed to fetch team") - })? - .ok_or_else(|| ApiResponse::not_found("Team not found"))?; - - Ok(ApiResponse::success(TeamResponse::from(team))) -} - -/// 获取队伍总数 -pub async fn get_teams_count( - State(db): State, - Query(query): Query, -) -> ApiResult { - let conn = db.get_connection(); - - let mut query = Team::find(); - - if let Some(team_state) = query.team_state { - query = query.filter(team::Column::TeamState.eq(team_state)); - } - - if let Some(team_color) = query.team_color { - query = query.filter(team::Column::TeamColor.eq(team_color)); - } - - let count = query - .count(conn) - .await - .map_err(|e| { - tracing::error!("Failed to count teams: {}", e); - ApiResponse::internal_server_error("Failed to count teams") - })?; - - Ok(ApiResponse::success(count)) +use axum::{ + extract::{Path, Query, State}, + Json, +}; +use sea_orm::{DatabaseConnection, EntityTrait, PaginatorTrait, QueryOrder}; +use serde::{Deserialize, Serialize}; + +use crate::{ + database::Database, + entities::{prelude::*, team}, + utils::response::{ApiResponse, ApiResult}, +}; + +#[derive(Debug, Deserialize)] +pub struct GetTeamsQuery { + page: Option, + limit: Option, + team_state: Option, + team_color: Option, +} + +#[derive(Debug, Serialize)] +pub struct TeamResponse { + id: i32, + name: String, + banner_url: String, + team_color: String, + team_state: String, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, +} + +impl From for TeamResponse { + fn from(model: team::Model) -> Self { + Self { + id: model.id, + name: model.name, + banner_url: model.banner_url, + team_color: model.team_color.to_string(), + team_state: model.team_state.to_string(), + created_at: model.created_at, + updated_at: model.updated_at, + } + } +} + +/// 获取队伍列表 +pub async fn get_teams( + State(db): State, + Query(query): Query, +) -> ApiResult> { + let conn = db.get_connection(); + + let page = query.page.unwrap_or(1); + let limit = query.limit.unwrap_or(20); + let offset = (page - 1) * limit; + + let mut query = Team::find() + .order_by_asc(team::Column::Id); + + if let Some(team_state) = query.team_state { + query = query.filter(team::Column::TeamState.eq(team_state)); + } + + if let Some(team_color) = query.team_color { + query = query.filter(team::Column::TeamColor.eq(team_color)); + } + + let teams = query + .offset(offset) + .limit(limit) + .all(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch teams: {}", e); + ApiResponse::internal_server_error("Failed to fetch teams") + })?; + + let response = teams.into_iter().map(TeamResponse::from).collect(); + Ok(ApiResponse::success(response)) +} + +/// 获取单个队伍 +pub async fn get_team( + State(db): State, + Path(team_id): Path, +) -> ApiResult { + let conn = db.get_connection(); + + let team = Team::find_by_id(team_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch team: {}", e); + ApiResponse::internal_server_error("Failed to fetch team") + })? + .ok_or_else(|| ApiResponse::not_found("Team not found"))?; + + Ok(ApiResponse::success(TeamResponse::from(team))) +} + +/// 获取队伍总数 +pub async fn get_teams_count( + State(db): State, + Query(query): Query, +) -> ApiResult { + let conn = db.get_connection(); + + let mut query = Team::find(); + + if let Some(team_state) = query.team_state { + query = query.filter(team::Column::TeamState.eq(team_state)); + } + + if let Some(team_color) = query.team_color { + query = query.filter(team::Column::TeamColor.eq(team_color)); + } + + let count = query + .count(conn) + .await + .map_err(|e| { + tracing::error!("Failed to count teams: {}", e); + ApiResponse::internal_server_error("Failed to count teams") + })?; + + Ok(ApiResponse::success(count)) } \ No newline at end of file diff --git a/src/handlers/tournament_config.rs b/src/handlers/tournament_config.rs index 1b1b66c..f03a4fd 100644 --- a/src/handlers/tournament_config.rs +++ b/src/handlers/tournament_config.rs @@ -1,310 +1,310 @@ -use axum::{ - Json, - extract::{Path, Query, State}, -}; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, - QueryOrder, Set, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - database::Database, - entities::{prelude::*, tournament_config}, - utils::response::{ApiResponse, ApiResult}, -}; - -#[derive(Debug, Deserialize)] -pub struct GetTournamentConfigsQuery { - page: Option, - limit: Option, - current_seasonal: Option, - current_category: Option, - can_register: Option, -} - -#[derive(Debug, Serialize)] -pub struct TournamentConfigResponse { - id: i32, - tournament_name: String, - max_pp_for_registration: f64, - min_pp_for_registration: f64, - current_seasonal: String, - current_category: String, - can_register: bool, - created_at: chrono::DateTime, - updated_at: chrono::DateTime, -} - -impl From for TournamentConfigResponse { - fn from(model: tournament_config::Model) -> Self { - Self { - id: model.id, - tournament_name: model.tournament_name, - max_pp_for_registration: model.max_pp_for_registration, - min_pp_for_registration: model.min_pp_for_registration, - current_seasonal: model.current_seasonal.into(), - current_category: model.current_category.to_string(), - can_register: model.can_register, - created_at: model.created_at, - updated_at: model.updated_at, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct CreateTournamentConfigRequest { - tournament_name: String, - max_pp_for_registration: f64, - min_pp_for_registration: f64, - current_seasonal: String, - current_category: String, - can_register: bool, -} - -#[derive(Debug, Deserialize)] -pub struct UpdateTournamentConfigRequest { - tournament_name: Option, - max_pp_for_registration: Option, - min_pp_for_registration: Option, - current_seasonal: Option, - current_category: Option, - can_register: Option, -} - -/// 获取比赛设置列表 -pub async fn get_tournament_configs( - State(db): State, - Query(query): Query, -) -> ApiResult> { - let conn = db.get_connection(); - - let page = query.page.unwrap_or(1); - let limit = query.limit.unwrap_or(20); - let offset = (page - 1) * limit; - - let mut query = TournamentConfig::find().order_by_desc(tournament_config::Column::CreatedAt); - - if let Some(current_seasonal) = query.current_seasonal { - query = query.filter(tournament_config::Column::CurrentSeasonal.eq(current_seasonal)); - } - - if let Some(current_category) = query.current_category { - query = query.filter(tournament_config::Column::CurrentCategory.eq(current_category)); - } - - if let Some(can_register) = query.can_register { - query = query.filter(tournament_config::Column::CanRegister.eq(can_register)); - } - - let configs = query - .offset(offset) - .limit(limit) - .all(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch tournament configs: {}", e); - ApiResponse::internal_server_error("Failed to fetch tournament configs") - })?; - - let response = configs - .into_iter() - .map(TournamentConfigResponse::from) - .collect(); - Ok(ApiResponse::success(response)) -} - -/// 获取单个比赛设置 -pub async fn get_tournament_config( - State(db): State, - Path(config_id): Path, -) -> ApiResult { - let conn = db.get_connection(); - - let config = TournamentConfig::find_by_id(config_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch tournament config: {}", e); - ApiResponse::internal_server_error("Failed to fetch tournament config") - })? - .ok_or_else(|| ApiResponse::not_found("Tournament config not found"))?; - - Ok(ApiResponse::success(TournamentConfigResponse::from(config))) -} - -/// 获取当前比赛设置 -pub async fn get_current_tournament_config( - State(db): State, -) -> ApiResult { - let conn = db.get_connection(); - - let config = TournamentConfig::find() - .order_by_desc(tournament_config::Column::CreatedAt) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch current tournament config: {}", e); - ApiResponse::internal_server_error("Failed to fetch current tournament config") - })? - .ok_or_else(|| ApiResponse::not_found("No tournament config found"))?; - - Ok(ApiResponse::success(TournamentConfigResponse::from(config))) -} - -/// 创建比赛设置 -pub async fn create_tournament_config( - State(db): State, - Json(payload): Json, -) -> ApiResult { - let conn = db.get_connection(); - - // 验证赛季 - let current_seasonal = tournament_config::Season::from(payload.current_seasonal); - - // 验证类别 - let current_category = match payload.current_category.as_str() { - "QUA" => tournament_config::Category::Qua, - "RO16" => tournament_config::Category::Ro16, - "QF" => tournament_config::Category::Qf, - "SF" => tournament_config::Category::Sf, - "F" => tournament_config::Category::F, - "GF" => tournament_config::Category::Gf, - _ => return Err(ApiResponse::bad_request("Invalid category")), - }; - - // 创建比赛设置 - let config = tournament_config::ActiveModel { - tournament_name: Set(payload.tournament_name), - max_pp_for_registration: Set(payload.max_pp_for_registration), - min_pp_for_registration: Set(payload.min_pp_for_registration), - current_seasonal: Set(current_seasonal), - current_category: Set(current_category), - can_register: Set(payload.can_register), - ..Default::default() - }; - - let config = config.insert(conn).await.map_err(|e| { - tracing::error!("Failed to create tournament config: {}", e); - ApiResponse::internal_server_error("Failed to create tournament config") - })?; - - Ok(ApiResponse::success(TournamentConfigResponse::from(config))) -} - -/// 更新比赛设置 -pub async fn update_tournament_config( - State(db): State, - Path(config_id): Path, - Json(payload): Json, -) -> ApiResult { - let conn = db.get_connection(); - - let mut config: tournament_config::ActiveModel = TournamentConfig::find_by_id(config_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch tournament config: {}", e); - ApiResponse::internal_server_error("Failed to fetch tournament config") - })? - .ok_or_else(|| ApiResponse::not_found("Tournament config not found"))? - .into(); - - if let Some(tournament_name) = payload.tournament_name { - config.tournament_name = Set(tournament_name); - } - - if let Some(max_pp_for_registration) = payload.max_pp_for_registration { - config.max_pp_for_registration = Set(max_pp_for_registration); - } - - if let Some(min_pp_for_registration) = payload.min_pp_for_registration { - config.min_pp_for_registration = Set(min_pp_for_registration); - } - - if let Some(current_seasonal) = payload.current_seasonal { - config.current_seasonal = Set(tournament_config::Season::from(current_seasonal)); - } - - if let Some(current_category) = payload.current_category { - let category = match current_category.as_str() { - "QUA" => tournament_config::Category::Qua, - "RO16" => tournament_config::Category::Ro16, - "QF" => tournament_config::Category::Qf, - "SF" => tournament_config::Category::Sf, - "F" => tournament_config::Category::F, - "GF" => tournament_config::Category::Gf, - _ => return Err(ApiResponse::bad_request("Invalid category")), - }; - config.current_category = Set(category); - } - - if let Some(can_register) = payload.can_register { - config.can_register = Set(can_register); - } - - let config = config.update(conn).await.map_err(|e| { - tracing::error!("Failed to update tournament config: {}", e); - ApiResponse::internal_server_error("Failed to update tournament config") - })?; - - Ok(ApiResponse::success(TournamentConfigResponse::from(config))) -} - -/// 删除比赛设置 -pub async fn delete_tournament_config( - State(db): State, - Path(config_id): Path, -) -> ApiResult<()> { - let conn = db.get_connection(); - - let config = TournamentConfig::find_by_id(config_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch tournament config: {}", e); - ApiResponse::internal_server_error("Failed to fetch tournament config") - })? - .ok_or_else(|| ApiResponse::not_found("Tournament config not found"))?; - - TournamentConfig::delete_by_id(config.id) - .exec(conn) - .await - .map_err(|e| { - tracing::error!("Failed to delete tournament config: {}", e); - ApiResponse::internal_server_error("Failed to delete tournament config") - })?; - - Ok(ApiResponse::success_message( - "Tournament config deleted successfully", - )) -} - -/// 获取比赛设置总数 -pub async fn get_tournament_configs_count( - State(db): State, - Query(query): Query, -) -> ApiResult { - let conn = db.get_connection(); - - let mut query = TournamentConfig::find(); - - if let Some(current_seasonal) = query.current_seasonal { - query = query.filter(tournament_config::Column::CurrentSeasonal.eq(current_seasonal)); - } - - if let Some(current_category) = query.current_category { - query = query.filter(tournament_config::Column::CurrentCategory.eq(current_category)); - } - - if let Some(can_register) = query.can_register { - query = query.filter(tournament_config::Column::CanRegister.eq(can_register)); - } - - let count = query.count(conn).await.map_err(|e| { - tracing::error!("Failed to count tournament configs: {}", e); - ApiResponse::internal_server_error("Failed to count tournament configs") - })?; - - Ok(ApiResponse::success(count)) -} +use axum::{ + Json, + extract::{Path, Query, State}, +}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, + QueryOrder, Set, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + database::Database, + entities::{prelude::*, tournament_config}, + utils::response::{ApiResponse, ApiResult}, +}; + +#[derive(Debug, Deserialize)] +pub struct GetTournamentConfigsQuery { + page: Option, + limit: Option, + current_seasonal: Option, + current_category: Option, + can_register: Option, +} + +#[derive(Debug, Serialize)] +pub struct TournamentConfigResponse { + id: i32, + tournament_name: String, + max_pp_for_registration: f64, + min_pp_for_registration: f64, + current_seasonal: String, + current_category: String, + can_register: bool, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, +} + +impl From for TournamentConfigResponse { + fn from(model: tournament_config::Model) -> Self { + Self { + id: model.id, + tournament_name: model.tournament_name, + max_pp_for_registration: model.max_pp_for_registration, + min_pp_for_registration: model.min_pp_for_registration, + current_seasonal: model.current_seasonal.into(), + current_category: model.current_category.to_string(), + can_register: model.can_register, + created_at: model.created_at, + updated_at: model.updated_at, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct CreateTournamentConfigRequest { + tournament_name: String, + max_pp_for_registration: f64, + min_pp_for_registration: f64, + current_seasonal: String, + current_category: String, + can_register: bool, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateTournamentConfigRequest { + tournament_name: Option, + max_pp_for_registration: Option, + min_pp_for_registration: Option, + current_seasonal: Option, + current_category: Option, + can_register: Option, +} + +/// 获取比赛设置列表 +pub async fn get_tournament_configs( + State(db): State, + Query(query): Query, +) -> ApiResult> { + let conn = db.get_connection(); + + let page = query.page.unwrap_or(1); + let limit = query.limit.unwrap_or(20); + let offset = (page - 1) * limit; + + let mut query = TournamentConfig::find().order_by_desc(tournament_config::Column::CreatedAt); + + if let Some(current_seasonal) = query.current_seasonal { + query = query.filter(tournament_config::Column::CurrentSeasonal.eq(current_seasonal)); + } + + if let Some(current_category) = query.current_category { + query = query.filter(tournament_config::Column::CurrentCategory.eq(current_category)); + } + + if let Some(can_register) = query.can_register { + query = query.filter(tournament_config::Column::CanRegister.eq(can_register)); + } + + let configs = query + .offset(offset) + .limit(limit) + .all(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch tournament configs: {}", e); + ApiResponse::internal_server_error("Failed to fetch tournament configs") + })?; + + let response = configs + .into_iter() + .map(TournamentConfigResponse::from) + .collect(); + Ok(ApiResponse::success(response)) +} + +/// 获取单个比赛设置 +pub async fn get_tournament_config( + State(db): State, + Path(config_id): Path, +) -> ApiResult { + let conn = db.get_connection(); + + let config = TournamentConfig::find_by_id(config_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch tournament config: {}", e); + ApiResponse::internal_server_error("Failed to fetch tournament config") + })? + .ok_or_else(|| ApiResponse::not_found("Tournament config not found"))?; + + Ok(ApiResponse::success(TournamentConfigResponse::from(config))) +} + +/// 获取当前比赛设置 +pub async fn get_current_tournament_config( + State(db): State, +) -> ApiResult { + let conn = db.get_connection(); + + let config = TournamentConfig::find() + .order_by_desc(tournament_config::Column::CreatedAt) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch current tournament config: {}", e); + ApiResponse::internal_server_error("Failed to fetch current tournament config") + })? + .ok_or_else(|| ApiResponse::not_found("No tournament config found"))?; + + Ok(ApiResponse::success(TournamentConfigResponse::from(config))) +} + +/// 创建比赛设置 +pub async fn create_tournament_config( + State(db): State, + Json(payload): Json, +) -> ApiResult { + let conn = db.get_connection(); + + // 验证赛季 + let current_seasonal = tournament_config::Season::from(payload.current_seasonal); + + // 验证类别 + let current_category = match payload.current_category.as_str() { + "QUA" => tournament_config::Category::Qua, + "RO16" => tournament_config::Category::Ro16, + "QF" => tournament_config::Category::Qf, + "SF" => tournament_config::Category::Sf, + "F" => tournament_config::Category::F, + "GF" => tournament_config::Category::Gf, + _ => return Err(ApiResponse::bad_request("Invalid category")), + }; + + // 创建比赛设置 + let config = tournament_config::ActiveModel { + tournament_name: Set(payload.tournament_name), + max_pp_for_registration: Set(payload.max_pp_for_registration), + min_pp_for_registration: Set(payload.min_pp_for_registration), + current_seasonal: Set(current_seasonal), + current_category: Set(current_category), + can_register: Set(payload.can_register), + ..Default::default() + }; + + let config = config.insert(conn).await.map_err(|e| { + tracing::error!("Failed to create tournament config: {}", e); + ApiResponse::internal_server_error("Failed to create tournament config") + })?; + + Ok(ApiResponse::success(TournamentConfigResponse::from(config))) +} + +/// 更新比赛设置 +pub async fn update_tournament_config( + State(db): State, + Path(config_id): Path, + Json(payload): Json, +) -> ApiResult { + let conn = db.get_connection(); + + let mut config: tournament_config::ActiveModel = TournamentConfig::find_by_id(config_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch tournament config: {}", e); + ApiResponse::internal_server_error("Failed to fetch tournament config") + })? + .ok_or_else(|| ApiResponse::not_found("Tournament config not found"))? + .into(); + + if let Some(tournament_name) = payload.tournament_name { + config.tournament_name = Set(tournament_name); + } + + if let Some(max_pp_for_registration) = payload.max_pp_for_registration { + config.max_pp_for_registration = Set(max_pp_for_registration); + } + + if let Some(min_pp_for_registration) = payload.min_pp_for_registration { + config.min_pp_for_registration = Set(min_pp_for_registration); + } + + if let Some(current_seasonal) = payload.current_seasonal { + config.current_seasonal = Set(tournament_config::Season::from(current_seasonal)); + } + + if let Some(current_category) = payload.current_category { + let category = match current_category.as_str() { + "QUA" => tournament_config::Category::Qua, + "RO16" => tournament_config::Category::Ro16, + "QF" => tournament_config::Category::Qf, + "SF" => tournament_config::Category::Sf, + "F" => tournament_config::Category::F, + "GF" => tournament_config::Category::Gf, + _ => return Err(ApiResponse::bad_request("Invalid category")), + }; + config.current_category = Set(category); + } + + if let Some(can_register) = payload.can_register { + config.can_register = Set(can_register); + } + + let config = config.update(conn).await.map_err(|e| { + tracing::error!("Failed to update tournament config: {}", e); + ApiResponse::internal_server_error("Failed to update tournament config") + })?; + + Ok(ApiResponse::success(TournamentConfigResponse::from(config))) +} + +/// 删除比赛设置 +pub async fn delete_tournament_config( + State(db): State, + Path(config_id): Path, +) -> ApiResult<()> { + let conn = db.get_connection(); + + let config = TournamentConfig::find_by_id(config_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch tournament config: {}", e); + ApiResponse::internal_server_error("Failed to fetch tournament config") + })? + .ok_or_else(|| ApiResponse::not_found("Tournament config not found"))?; + + TournamentConfig::delete_by_id(config.id) + .exec(conn) + .await + .map_err(|e| { + tracing::error!("Failed to delete tournament config: {}", e); + ApiResponse::internal_server_error("Failed to delete tournament config") + })?; + + Ok(ApiResponse::success_message( + "Tournament config deleted successfully", + )) +} + +/// 获取比赛设置总数 +pub async fn get_tournament_configs_count( + State(db): State, + Query(query): Query, +) -> ApiResult { + let conn = db.get_connection(); + + let mut query = TournamentConfig::find(); + + if let Some(current_seasonal) = query.current_seasonal { + query = query.filter(tournament_config::Column::CurrentSeasonal.eq(current_seasonal)); + } + + if let Some(current_category) = query.current_category { + query = query.filter(tournament_config::Column::CurrentCategory.eq(current_category)); + } + + if let Some(can_register) = query.can_register { + query = query.filter(tournament_config::Column::CanRegister.eq(can_register)); + } + + let count = query.count(conn).await.map_err(|e| { + tracing::error!("Failed to count tournament configs: {}", e); + ApiResponse::internal_server_error("Failed to count tournament configs") + })?; + + Ok(ApiResponse::success(count)) +} diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 27e091a..dbfedd7 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -1,166 +1,166 @@ -use axum::{ - extract::{Path, Query, State}, - http::StatusCode, - response::IntoResponse, - Json, -}; -use sea_orm::{DatabaseConnection, EntityTrait, PaginatorTrait, QueryOrder}; -use serde::{Deserialize, Serialize}; - -use crate::{ - database::Database, - entities::{prelude::*, user}, - utils::response::{ApiResponse, ApiResult}, -}; - -#[derive(Debug, Deserialize)] -pub struct GetUsersQuery { - page: Option, - limit: Option, - approved: Option, - seasonal: Option, -} - -#[derive(Debug, Serialize)] -pub struct UserResponse { - id: i32, - osuid: i32, - username: String, - avatar_url: Option, - cover_url: Option, - country_code: String, - pp: f64, - global_rank: i32, - country_rank: i32, - approved: bool, - seed: i32, - seasonal: String, - user_state: String, - created_at: chrono::DateTime, - updated_at: chrono::DateTime, -} - -impl From for UserResponse { - fn from(model: user::Model) -> Self { - Self { - id: model.id, - osuid: model.osuid, - username: model.username, - avatar_url: model.avatar_url, - cover_url: model.cover_url, - country_code: model.country_code, - pp: model.pp, - global_rank: model.global_rank, - country_rank: model.country_rank, - approved: model.approved, - seed: model.seed, - seasonal: model.seasonal.into(), - user_state: model.user_state.to_string(), - created_at: model.created_at, - updated_at: model.updated_at, - } - } -} - -/// 获取用户列表 -pub async fn get_users( - State(db): State, - Query(query): Query, -) -> ApiResult> { - let conn = db.get_connection(); - - let page = query.page.unwrap_or(1); - let limit = query.limit.unwrap_or(20); - let offset = (page - 1) * limit; - - let mut query = User::find() - .order_by_asc(user::Column::Id); - - if let Some(approved) = query.approved { - query = query.filter(user::Column::Approved.eq(approved)); - } - - if let Some(seasonal) = query.seasonal { - query = query.filter(user::Column::Seasonal.eq(seasonal)); - } - - let users = query - .offset(offset) - .limit(limit) - .all(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch users: {}", e); - ApiResponse::internal_server_error("Failed to fetch users") - })?; - - let response = users.into_iter().map(UserResponse::from).collect(); - Ok(ApiResponse::success(response)) -} - -/// 获取单个用户 -pub async fn get_user( - State(db): State, - Path(user_id): Path, -) -> ApiResult { - let conn = db.get_connection(); - - let user = User::find_by_id(user_id) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch user: {}", e); - ApiResponse::internal_server_error("Failed to fetch user") - })? - .ok_or_else(|| ApiResponse::not_found("User not found"))?; - - Ok(ApiResponse::success(UserResponse::from(user))) -} - -/// 获取用户总数 -pub async fn get_users_count( - State(db): State, - Query(query): Query, -) -> ApiResult { - let conn = db.get_connection(); - - let mut query = User::find(); - - if let Some(approved) = query.approved { - query = query.filter(user::Column::Approved.eq(approved)); - } - - if let Some(seasonal) = query.seasonal { - query = query.filter(user::Column::Seasonal.eq(seasonal)); - } - - let count = query - .count(conn) - .await - .map_err(|e| { - tracing::error!("Failed to count users: {}", e); - ApiResponse::internal_server_error("Failed to count users") - })?; - - Ok(ApiResponse::success(count)) -} - -/// 通过osu ID获取用户 -pub async fn get_user_by_osuid( - State(db): State, - Path(osuid): Path, -) -> ApiResult { - let conn = db.get_connection(); - - let user = User::find() - .filter(user::Column::Osuid.eq(osuid)) - .one(conn) - .await - .map_err(|e| { - tracing::error!("Failed to fetch user by osuid: {}", e); - ApiResponse::internal_server_error("Failed to fetch user") - })? - .ok_or_else(|| ApiResponse::not_found("User not found"))?; - - Ok(ApiResponse::success(UserResponse::from(user))) +use axum::{ + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use sea_orm::{DatabaseConnection, EntityTrait, PaginatorTrait, QueryOrder}; +use serde::{Deserialize, Serialize}; + +use crate::{ + database::Database, + entities::{prelude::*, user}, + utils::response::{ApiResponse, ApiResult}, +}; + +#[derive(Debug, Deserialize)] +pub struct GetUsersQuery { + page: Option, + limit: Option, + approved: Option, + seasonal: Option, +} + +#[derive(Debug, Serialize)] +pub struct UserResponse { + id: i32, + osuid: i32, + username: String, + avatar_url: Option, + cover_url: Option, + country_code: String, + pp: f64, + global_rank: i32, + country_rank: i32, + approved: bool, + seed: i32, + seasonal: String, + user_state: String, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, +} + +impl From for UserResponse { + fn from(model: user::Model) -> Self { + Self { + id: model.id, + osuid: model.osuid, + username: model.username, + avatar_url: model.avatar_url, + cover_url: model.cover_url, + country_code: model.country_code, + pp: model.pp, + global_rank: model.global_rank, + country_rank: model.country_rank, + approved: model.approved, + seed: model.seed, + seasonal: model.seasonal.into(), + user_state: model.user_state.to_string(), + created_at: model.created_at, + updated_at: model.updated_at, + } + } +} + +/// 获取用户列表 +pub async fn get_users( + State(db): State, + Query(query): Query, +) -> ApiResult> { + let conn = db.get_connection(); + + let page = query.page.unwrap_or(1); + let limit = query.limit.unwrap_or(20); + let offset = (page - 1) * limit; + + let mut query = User::find() + .order_by_asc(user::Column::Id); + + if let Some(approved) = query.approved { + query = query.filter(user::Column::Approved.eq(approved)); + } + + if let Some(seasonal) = query.seasonal { + query = query.filter(user::Column::Seasonal.eq(seasonal)); + } + + let users = query + .offset(offset) + .limit(limit) + .all(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch users: {}", e); + ApiResponse::internal_server_error("Failed to fetch users") + })?; + + let response = users.into_iter().map(UserResponse::from).collect(); + Ok(ApiResponse::success(response)) +} + +/// 获取单个用户 +pub async fn get_user( + State(db): State, + Path(user_id): Path, +) -> ApiResult { + let conn = db.get_connection(); + + let user = User::find_by_id(user_id) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch user: {}", e); + ApiResponse::internal_server_error("Failed to fetch user") + })? + .ok_or_else(|| ApiResponse::not_found("User not found"))?; + + Ok(ApiResponse::success(UserResponse::from(user))) +} + +/// 获取用户总数 +pub async fn get_users_count( + State(db): State, + Query(query): Query, +) -> ApiResult { + let conn = db.get_connection(); + + let mut query = User::find(); + + if let Some(approved) = query.approved { + query = query.filter(user::Column::Approved.eq(approved)); + } + + if let Some(seasonal) = query.seasonal { + query = query.filter(user::Column::Seasonal.eq(seasonal)); + } + + let count = query + .count(conn) + .await + .map_err(|e| { + tracing::error!("Failed to count users: {}", e); + ApiResponse::internal_server_error("Failed to count users") + })?; + + Ok(ApiResponse::success(count)) +} + +/// 通过osu ID获取用户 +pub async fn get_user_by_osuid( + State(db): State, + Path(osuid): Path, +) -> ApiResult { + let conn = db.get_connection(); + + let user = User::find() + .filter(user::Column::Osuid.eq(osuid)) + .one(conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch user by osuid: {}", e); + ApiResponse::internal_server_error("Failed to fetch user") + })? + .ok_or_else(|| ApiResponse::not_found("User not found"))?; + + Ok(ApiResponse::success(UserResponse::from(user))) } \ No newline at end of file diff --git a/src/routes/api.rs b/src/routes/api.rs index 79eff42..95a365a 100644 --- a/src/routes/api.rs +++ b/src/routes/api.rs @@ -1,114 +1,114 @@ -use axum::{ - Router, - extract::State, - response::IntoResponse, - routing::{delete, get, post, put}, -}; -use axum_extra::TypedHeader; -use axum_extra::headers::Authorization; -use axum_extra::headers::authorization::Bearer; - -use crate::{ - config::Settings, - database::Database, - handlers::{ - auth::{auth_callback, get_auth_url, logout, verify_token}, - health::health_check, - match_room::{ - create_match_room, delete_match_room, get_match_room, get_match_rooms, - get_match_rooms_count, update_match_room, - }, - message::{ - delete_message, get_message, get_messages, get_messages_count, - get_unread_messages_count, send_message, update_message, - }, - registration::{ - approve_registration, delete_registration, get_approved_players, get_registrations, - get_registrations_count, - }, - team::{get_team, get_teams, get_teams_count}, - tournament_config::{ - create_tournament_config, delete_tournament_config, get_current_tournament_config, - get_tournament_config, get_tournament_configs, get_tournament_configs_count, - update_tournament_config, - }, - user::{get_user, get_user_by_osuid, get_users, get_users_count}, - }, - services::auth::AuthService, -}; - -pub fn create_router(database: Database, settings: Settings) -> Router { - let auth_service = AuthService::new(settings.clone()); - - Router::new() - // Health check - .route("/health", get(health_check)) - // Auth routes - .route("/api/auth/url", get(get_auth_url)) - .route("/api/auth/callback/osu", get(auth_callback)) - .route("/api/auth/logout", post(logout)) - .route("/api/auth/verify", get(verify_token_handler)) - // User routes - .route("/api/users", get(get_users)) - .route("/api/users/count", get(get_users_count)) - .route("/api/users/:id", get(get_user)) - .route("/api/users/osuid/:osuid", get(get_user_by_osuid)) - // Registration routes - .route("/api/registrations", get(get_registrations)) - .route("/api/registrations/count", get(get_registrations_count)) - .route("/api/registrations/approved", get(get_approved_players)) - .route( - "/api/admin/registrations/:id/approve", - post(approve_registration), - ) - .route("/api/admin/registrations/:id", delete(delete_registration)) - // Match room routes - .route("/api/match-rooms", get(get_match_rooms)) - .route("/api/match-rooms/count", get(get_match_rooms_count)) - .route("/api/match-rooms/:id", get(get_match_room)) - .route("/api/match-rooms", post(create_match_room)) - .route("/api/match-rooms/:id", put(update_match_room)) - .route("/api/match-rooms/:id", delete(delete_match_room)) - // Message routes - .route("/api/messages", get(get_messages)) - .route("/api/messages/count", get(get_messages_count)) - .route("/api/messages/:id", get(get_message)) - .route("/api/messages", post(send_message)) - .route("/api/messages/:id", put(update_message)) - .route("/api/messages/:id", delete(delete_message)) - .route( - "/api/messages/unread/:receiver_id", - get(get_unread_messages_count), - ) - // Tournament config routes - .route("/api/tournament-configs", get(get_tournament_configs)) - .route( - "/api/tournament-configs/count", - get(get_tournament_configs_count), - ) - .route( - "/api/tournament-configs/current", - get(get_current_tournament_config), - ) - .route("/api/tournament-configs/:id", get(get_tournament_config)) - .route("/api/tournament-configs", post(create_tournament_config)) - .route("/api/tournament-configs/:id", put(update_tournament_config)) - .route( - "/api/tournament-configs/:id", - delete(delete_tournament_config), - ) - // Team routes - .route("/api/teams", get(get_teams)) - .route("/api/teams/count", get(get_teams_count)) - .route("/api/teams/:id", get(get_team)) - .with_state(auth_service) - .with_state(database) -} - -// Handler for token verification with header extraction -async fn verify_token_handler( - TypedHeader(auth_header): TypedHeader>, - State(auth_service): State, -) -> impl IntoResponse { - verify_token(State(auth_service), auth_header.token().to_string()).await -} +use axum::{ + Router, + extract::State, + response::IntoResponse, + routing::{delete, get, post, put}, +}; +use axum_extra::TypedHeader; +use axum_extra::headers::Authorization; +use axum_extra::headers::authorization::Bearer; + +use crate::{ + config::Settings, + database::Database, + handlers::{ + auth::{auth_callback, get_auth_url, logout, verify_token}, + health::health_check, + match_room::{ + create_match_room, delete_match_room, get_match_room, get_match_rooms, + get_match_rooms_count, update_match_room, + }, + message::{ + delete_message, get_message, get_messages, get_messages_count, + get_unread_messages_count, send_message, update_message, + }, + registration::{ + approve_registration, delete_registration, get_approved_players, get_registrations, + get_registrations_count, + }, + team::{get_team, get_teams, get_teams_count}, + tournament_config::{ + create_tournament_config, delete_tournament_config, get_current_tournament_config, + get_tournament_config, get_tournament_configs, get_tournament_configs_count, + update_tournament_config, + }, + user::{get_user, get_user_by_osuid, get_users, get_users_count}, + }, + services::auth::AuthService, +}; + +pub fn create_router(database: Database, settings: Settings) -> Router { + let auth_service = AuthService::new(settings.clone()); + + Router::new() + // Health check + .route("/health", get(health_check)) + // Auth routes + .route("/api/auth/url", get(get_auth_url)) + .route("/api/auth/callback/osu", get(auth_callback)) + .route("/api/auth/logout", post(logout)) + .route("/api/auth/verify", get(verify_token_handler)) + // User routes + .route("/api/users", get(get_users)) + .route("/api/users/count", get(get_users_count)) + .route("/api/users/:id", get(get_user)) + .route("/api/users/osuid/:osuid", get(get_user_by_osuid)) + // Registration routes + .route("/api/registrations", get(get_registrations)) + .route("/api/registrations/count", get(get_registrations_count)) + .route("/api/registrations/approved", get(get_approved_players)) + .route( + "/api/admin/registrations/:id/approve", + post(approve_registration), + ) + .route("/api/admin/registrations/:id", delete(delete_registration)) + // Match room routes + .route("/api/match-rooms", get(get_match_rooms)) + .route("/api/match-rooms/count", get(get_match_rooms_count)) + .route("/api/match-rooms/:id", get(get_match_room)) + .route("/api/match-rooms", post(create_match_room)) + .route("/api/match-rooms/:id", put(update_match_room)) + .route("/api/match-rooms/:id", delete(delete_match_room)) + // Message routes + .route("/api/messages", get(get_messages)) + .route("/api/messages/count", get(get_messages_count)) + .route("/api/messages/:id", get(get_message)) + .route("/api/messages", post(send_message)) + .route("/api/messages/:id", put(update_message)) + .route("/api/messages/:id", delete(delete_message)) + .route( + "/api/messages/unread/:receiver_id", + get(get_unread_messages_count), + ) + // Tournament config routes + .route("/api/tournament-configs", get(get_tournament_configs)) + .route( + "/api/tournament-configs/count", + get(get_tournament_configs_count), + ) + .route( + "/api/tournament-configs/current", + get(get_current_tournament_config), + ) + .route("/api/tournament-configs/:id", get(get_tournament_config)) + .route("/api/tournament-configs", post(create_tournament_config)) + .route("/api/tournament-configs/:id", put(update_tournament_config)) + .route( + "/api/tournament-configs/:id", + delete(delete_tournament_config), + ) + // Team routes + .route("/api/teams", get(get_teams)) + .route("/api/teams/count", get(get_teams_count)) + .route("/api/teams/:id", get(get_team)) + .with_state(auth_service) + .with_state(database) +} + +// Handler for token verification with header extraction +async fn verify_token_handler( + TypedHeader(auth_header): TypedHeader>, + State(auth_service): State, +) -> impl IntoResponse { + verify_token(State(auth_service), auth_header.token().to_string()).await +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 7e2d502..d4c668d 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,3 +1,3 @@ -pub mod api; - +pub mod api; + pub use api::create_router; \ No newline at end of file diff --git a/src/services/auth/mod.rs b/src/services/auth/mod.rs index 243575e..fb8c6cf 100644 --- a/src/services/auth/mod.rs +++ b/src/services/auth/mod.rs @@ -1,4 +1,4 @@ -pub mod service; -pub mod middleware; - +pub mod service; +pub mod middleware; + pub use service::AuthService; \ No newline at end of file diff --git a/src/services/auth/service.rs b/src/services/auth/service.rs index 69edbb5..2096d37 100644 --- a/src/services/auth/service.rs +++ b/src/services/auth/service.rs @@ -1,151 +1,151 @@ -use crate::config::Settings; -use crate::utils::AppError; -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use std::time::{SystemTime, UNIX_EPOCH}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct OsuUserInfo { - pub id: u64, - pub username: String, - pub avatar_url: String, - pub cover_url: String, - pub country_code: String, - pub statistics: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct OsuStatistics { - pub pp: f64, - pub global_rank: Option, - pub country_rank: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct OsuTokenResponse { - pub access_token: String, - pub refresh_token: String, - pub expires_in: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct JwtClaims { - pub sub: String, // osu! user ID - pub username: String, - pub exp: usize, // Expiration time - pub iat: usize, // Issued at -} - -pub struct AuthService { - client: Client, - settings: Settings, -} - -impl AuthService { - pub fn new(settings: Settings) -> Self { - Self { - client: Client::new(), - settings, - } - } - - pub fn get_osu_auth_url(&self) -> String { - let params = vec![ - ("client_id", self.settings.oauth.client_id.as_str()), - ("redirect_uri", self.settings.oauth.redirect_uri.as_str()), - ("response_type", "code"), - ("scope", "identify public"), - ]; - - let query_string = params - .iter() - .map(|(k, v)| format!("{}={}", k, v)) - .collect::>() - .join("&"); - - format!("https://osu.ppy.sh/oauth/authorize?{}", query_string) - } - - pub async fn exchange_code_for_token(&self, code: &str) -> Result { - let params = [ - ("client_id", self.settings.oauth.client_id.as_str()), - ("client_secret", self.settings.oauth.client_secret.as_str()), - ("code", code), - ("grant_type", "authorization_code"), - ("redirect_uri", self.settings.oauth.redirect_uri.as_str()), - ]; - - let response = self - .client - .post("https://osu.ppy.sh/oauth/token") - .form(¶ms) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(AppError::Auth(format!( - "Failed to exchange code for token: {}", - error_text - ))); - } - - let token_data: OsuTokenResponse = response.json().await?; - Ok(token_data) - } - - pub async fn get_osu_user_info(&self, access_token: &str) -> Result { - let response = self - .client - .get("https://osu.ppy.sh/api/v2/me") - .header("Authorization", format!("Bearer {}", access_token)) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(AppError::Auth(format!( - "Failed to get user info: {}", - error_text - ))); - } - - let user_info: OsuUserInfo = response.json().await?; - Ok(user_info) - } - - pub fn generate_jwt(&self, user_info: &OsuUserInfo) -> Result { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as usize; - - let expiry = now + (self.settings.jwt.expiry_hours as usize * 3600); - - let claims = JwtClaims { - sub: user_info.id.to_string(), - username: user_info.username.clone(), - exp: expiry, - iat: now, - }; - - let token = encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(self.settings.jwt.secret.as_bytes()), - )?; - - Ok(token) - } - - pub fn verify_jwt(&self, token: &str) -> Result { - let token_data = decode::( - token, - &DecodingKey::from_secret(self.settings.jwt.secret.as_bytes()), - &Validation::default(), - )?; - - Ok(token_data.claims) - } +use crate::config::Settings; +use crate::utils::AppError; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct OsuUserInfo { + pub id: u64, + pub username: String, + pub avatar_url: String, + pub cover_url: String, + pub country_code: String, + pub statistics: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OsuStatistics { + pub pp: f64, + pub global_rank: Option, + pub country_rank: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OsuTokenResponse { + pub access_token: String, + pub refresh_token: String, + pub expires_in: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JwtClaims { + pub sub: String, // osu! user ID + pub username: String, + pub exp: usize, // Expiration time + pub iat: usize, // Issued at +} + +pub struct AuthService { + client: Client, + settings: Settings, +} + +impl AuthService { + pub fn new(settings: Settings) -> Self { + Self { + client: Client::new(), + settings, + } + } + + pub fn get_osu_auth_url(&self) -> String { + let params = vec![ + ("client_id", self.settings.oauth.client_id.as_str()), + ("redirect_uri", self.settings.oauth.redirect_uri.as_str()), + ("response_type", "code"), + ("scope", "identify public"), + ]; + + let query_string = params + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect::>() + .join("&"); + + format!("https://osu.ppy.sh/oauth/authorize?{}", query_string) + } + + pub async fn exchange_code_for_token(&self, code: &str) -> Result { + let params = [ + ("client_id", self.settings.oauth.client_id.as_str()), + ("client_secret", self.settings.oauth.client_secret.as_str()), + ("code", code), + ("grant_type", "authorization_code"), + ("redirect_uri", self.settings.oauth.redirect_uri.as_str()), + ]; + + let response = self + .client + .post("https://osu.ppy.sh/oauth/token") + .form(¶ms) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(AppError::Auth(format!( + "Failed to exchange code for token: {}", + error_text + ))); + } + + let token_data: OsuTokenResponse = response.json().await?; + Ok(token_data) + } + + pub async fn get_osu_user_info(&self, access_token: &str) -> Result { + let response = self + .client + .get("https://osu.ppy.sh/api/v2/me") + .header("Authorization", format!("Bearer {}", access_token)) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(AppError::Auth(format!( + "Failed to get user info: {}", + error_text + ))); + } + + let user_info: OsuUserInfo = response.json().await?; + Ok(user_info) + } + + pub fn generate_jwt(&self, user_info: &OsuUserInfo) -> Result { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as usize; + + let expiry = now + (self.settings.jwt.expiry_hours as usize * 3600); + + let claims = JwtClaims { + sub: user_info.id.to_string(), + username: user_info.username.clone(), + exp: expiry, + iat: now, + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(self.settings.jwt.secret.as_bytes()), + )?; + + Ok(token) + } + + pub fn verify_jwt(&self, token: &str) -> Result { + let token_data = decode::( + token, + &DecodingKey::from_secret(self.settings.jwt.secret.as_bytes()), + &Validation::default(), + )?; + + Ok(token_data.claims) + } } \ No newline at end of file diff --git a/src/utils/error.rs b/src/utils/error.rs index 36c341a..f3968f9 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -1,111 +1,111 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; -use serde::Serialize; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum AppError { - #[error("Database error: {0}")] - Database(#[from] sea_orm::DbErr), - - #[error("Validation error: {0}")] - Validation(String), - - #[error("Authentication error: {0}")] - Auth(String), - - #[error("Not found: {0}")] - NotFound(String), - - #[error("Internal server error: {0}")] - Internal(String), - - #[error("Bad request: {0}")] - BadRequest(String), - - #[error("Forbidden: {0}")] - Forbidden(String), - - #[error("Conflict: {0}")] - Conflict(String), -} - -impl AppError { - pub fn status_code(&self) -> StatusCode { - match self { - AppError::Database(_) | AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - AppError::Validation(_) | AppError::BadRequest(_) => StatusCode::BAD_REQUEST, - AppError::Auth(_) => StatusCode::UNAUTHORIZED, - AppError::NotFound(_) => StatusCode::NOT_FOUND, - AppError::Forbidden(_) => StatusCode::FORBIDDEN, - AppError::Conflict(_) => StatusCode::CONFLICT, - } - } - - pub fn error_code(&self) -> &'static str { - match self { - AppError::Database(_) => "DATABASE_ERROR", - AppError::Validation(_) => "VALIDATION_ERROR", - AppError::Auth(_) => "AUTH_ERROR", - AppError::NotFound(_) => "NOT_FOUND", - AppError::Internal(_) => "INTERNAL_ERROR", - AppError::BadRequest(_) => "BAD_REQUEST", - AppError::Forbidden(_) => "FORBIDDEN", - AppError::Conflict(_) => "CONFLICT", - } - } -} - -#[derive(Serialize)] -pub struct ErrorResponse { - pub success: bool, - pub error: String, - pub code: String, -} - -impl IntoResponse for AppError { - fn into_response(self) -> Response { - let status = self.status_code(); - let error_response = ErrorResponse { - success: false, - error: self.to_string(), - code: self.error_code().to_string(), - }; - - (status, Json(error_response)).into_response() - } -} - -// Convert common errors to AppError -impl From for AppError { - fn from(err: validator::ValidationErrors) -> Self { - AppError::Validation(err.to_string()) - } -} - -impl From for AppError { - fn from(err: jsonwebtoken::errors::Error) -> Self { - AppError::Auth(err.to_string()) - } -} - -impl From for AppError { - fn from(err: reqwest::Error) -> Self { - AppError::Internal(err.to_string()) - } -} - -impl From for AppError { - fn from(err: std::env::VarError) -> Self { - AppError::Internal(err.to_string()) - } -} - -impl From for AppError { - fn from(err: serde_json::Error) -> Self { - AppError::Internal(err.to_string()) - } +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde::Serialize; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("Database error: {0}")] + Database(#[from] sea_orm::DbErr), + + #[error("Validation error: {0}")] + Validation(String), + + #[error("Authentication error: {0}")] + Auth(String), + + #[error("Not found: {0}")] + NotFound(String), + + #[error("Internal server error: {0}")] + Internal(String), + + #[error("Bad request: {0}")] + BadRequest(String), + + #[error("Forbidden: {0}")] + Forbidden(String), + + #[error("Conflict: {0}")] + Conflict(String), +} + +impl AppError { + pub fn status_code(&self) -> StatusCode { + match self { + AppError::Database(_) | AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + AppError::Validation(_) | AppError::BadRequest(_) => StatusCode::BAD_REQUEST, + AppError::Auth(_) => StatusCode::UNAUTHORIZED, + AppError::NotFound(_) => StatusCode::NOT_FOUND, + AppError::Forbidden(_) => StatusCode::FORBIDDEN, + AppError::Conflict(_) => StatusCode::CONFLICT, + } + } + + pub fn error_code(&self) -> &'static str { + match self { + AppError::Database(_) => "DATABASE_ERROR", + AppError::Validation(_) => "VALIDATION_ERROR", + AppError::Auth(_) => "AUTH_ERROR", + AppError::NotFound(_) => "NOT_FOUND", + AppError::Internal(_) => "INTERNAL_ERROR", + AppError::BadRequest(_) => "BAD_REQUEST", + AppError::Forbidden(_) => "FORBIDDEN", + AppError::Conflict(_) => "CONFLICT", + } + } +} + +#[derive(Serialize)] +pub struct ErrorResponse { + pub success: bool, + pub error: String, + pub code: String, +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let status = self.status_code(); + let error_response = ErrorResponse { + success: false, + error: self.to_string(), + code: self.error_code().to_string(), + }; + + (status, Json(error_response)).into_response() + } +} + +// Convert common errors to AppError +impl From for AppError { + fn from(err: validator::ValidationErrors) -> Self { + AppError::Validation(err.to_string()) + } +} + +impl From for AppError { + fn from(err: jsonwebtoken::errors::Error) -> Self { + AppError::Auth(err.to_string()) + } +} + +impl From for AppError { + fn from(err: reqwest::Error) -> Self { + AppError::Internal(err.to_string()) + } +} + +impl From for AppError { + fn from(err: std::env::VarError) -> Self { + AppError::Internal(err.to_string()) + } +} + +impl From for AppError { + fn from(err: serde_json::Error) -> Self { + AppError::Internal(err.to_string()) + } } \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 426da9c..3cf254e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,6 @@ -pub mod error; -pub mod response; -pub mod validation; - -pub use error::AppError; +pub mod error; +pub mod response; +pub mod validation; + +pub use error::AppError; pub use response::ApiResponse; \ No newline at end of file diff --git a/src/utils/response.rs b/src/utils/response.rs index ec76e82..1dc384d 100644 --- a/src/utils/response.rs +++ b/src/utils/response.rs @@ -1,97 +1,97 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; -use serde::Serialize; - -#[derive(Serialize)] -pub struct ApiResponse { - pub success: bool, - pub data: Option, - pub message: Option, -} - -impl ApiResponse -where - T: Serialize, -{ - pub fn success(data: T) -> Self { - Self { - success: true, - data: Some(data), - message: None, - } - } - - pub fn success_with_message(data: T, message: &str) -> Self { - Self { - success: true, - data: Some(data), - message: Some(message.to_string()), - } - } - - pub fn empty_success() -> Self { - Self { - success: true, - data: None, - message: None, - } - } - - pub fn success_message(message: &str) -> Self { - Self { - success: true, - data: None, - message: Some(message.to_string()), - } - } -} - -impl IntoResponse for ApiResponse -where - T: Serialize, -{ - fn into_response(self) -> Response { - let status = if self.success { - StatusCode::OK - } else { - StatusCode::BAD_REQUEST - }; - - (status, Json(self)).into_response() - } -} - -// Helper functions for common responses -pub fn created(data: T) -> (StatusCode, Json>) -where - T: Serialize, -{ - ( - StatusCode::CREATED, - Json(ApiResponse::success_with_message(data, "Created successfully")), - ) -} - -pub fn ok(data: T) -> (StatusCode, Json>) -where - T: Serialize, -{ - (StatusCode::OK, Json(ApiResponse::success(data))) -} - -pub fn ok_message(message: &str) -> (StatusCode, Json>) { - ( - StatusCode::OK, - Json(ApiResponse::success_message(message)), - ) -} - -pub fn no_content() -> (StatusCode, Json>) { - ( - StatusCode::NO_CONTENT, - Json(ApiResponse::empty_success()), - ) +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde::Serialize; + +#[derive(Serialize)] +pub struct ApiResponse { + pub success: bool, + pub data: Option, + pub message: Option, +} + +impl ApiResponse +where + T: Serialize, +{ + pub fn success(data: T) -> Self { + Self { + success: true, + data: Some(data), + message: None, + } + } + + pub fn success_with_message(data: T, message: &str) -> Self { + Self { + success: true, + data: Some(data), + message: Some(message.to_string()), + } + } + + pub fn empty_success() -> Self { + Self { + success: true, + data: None, + message: None, + } + } + + pub fn success_message(message: &str) -> Self { + Self { + success: true, + data: None, + message: Some(message.to_string()), + } + } +} + +impl IntoResponse for ApiResponse +where + T: Serialize, +{ + fn into_response(self) -> Response { + let status = if self.success { + StatusCode::OK + } else { + StatusCode::BAD_REQUEST + }; + + (status, Json(self)).into_response() + } +} + +// Helper functions for common responses +pub fn created(data: T) -> (StatusCode, Json>) +where + T: Serialize, +{ + ( + StatusCode::CREATED, + Json(ApiResponse::success_with_message(data, "Created successfully")), + ) +} + +pub fn ok(data: T) -> (StatusCode, Json>) +where + T: Serialize, +{ + (StatusCode::OK, Json(ApiResponse::success(data))) +} + +pub fn ok_message(message: &str) -> (StatusCode, Json>) { + ( + StatusCode::OK, + Json(ApiResponse::success_message(message)), + ) +} + +pub fn no_content() -> (StatusCode, Json>) { + ( + StatusCode::NO_CONTENT, + Json(ApiResponse::empty_success()), + ) } \ No newline at end of file