refactor(api): standardizate API Router

This commit is contained in:
MingxuanGame
2025-08-28 13:18:06 +00:00
parent 3c5336ed61
commit c7f6c76b0f
14 changed files with 73 additions and 94 deletions

View File

@@ -14,10 +14,7 @@ from .router import router
from fastapi import Depends, File, Security
@router.post(
"/avatar/upload",
name="上传头像",
)
@router.post("/avatar/upload", name="上传头像", tags=["用户", "g0v0 API"])
async def upload_avatar(
session: Database,
content: bytes = File(...),

View File

@@ -14,7 +14,12 @@ from fastapi import Body, HTTPException, Security
from sqlmodel import col, exists, select
@router.get("/beatmapsets/{beatmapset_id}/can_rate", name="判断用户能否为谱面集打分", response_model=bool)
@router.get(
"/beatmapsets/{beatmapset_id}/can_rate",
name="判断用户能否为谱面集打分",
response_model=bool,
tags=["谱面集", "g0v0 API"],
)
async def can_rate_beatmapset(
beatmapset_id: int,
session: Database,
@@ -44,7 +49,9 @@ async def can_rate_beatmapset(
return (await session.exec(query)).first() or False
@router.post("/beatmapsets/{beatmapset_id}/ratings", name="上传对谱面集的打分", status_code=201)
@router.post(
"/beatmapsets/{beatmapset_id}/ratings", name="上传对谱面集的打分", status_code=201, tags=["谱面集", "g0v0 API"]
)
async def rate_beatmaps(
beatmapset_id: int,
session: Database,

View File

@@ -14,10 +14,7 @@ from .router import router
from fastapi import Depends, File, Security
@router.post(
"/cover/upload",
name="上传头图",
)
@router.post("/cover/upload", name="上传头图", tags=["用户", "g0v0 API"])
async def upload_cover(
session: Database,
content: bytes = File(...),

View File

@@ -18,6 +18,7 @@ from sqlmodel import select, text
"/oauth-app/create",
name="创建 OAuth 应用",
description="创建一个新的 OAuth 应用程序,并生成客户端 ID 和密钥",
tags=["osu! OAuth 认证", "g0v0 API"],
)
async def create_oauth_app(
session: Database,
@@ -58,6 +59,7 @@ async def create_oauth_app(
"/oauth-apps/{client_id}",
name="获取 OAuth 应用信息",
description="通过客户端 ID 获取 OAuth 应用的详细信息",
tags=["osu! OAuth 认证", "g0v0 API"],
)
async def get_oauth_app(
session: Database,
@@ -79,6 +81,7 @@ async def get_oauth_app(
"/oauth-apps",
name="获取用户的 OAuth 应用列表",
description="获取当前用户创建的所有 OAuth 应用程序",
tags=["osu! OAuth 认证", "g0v0 API"],
)
async def get_user_oauth_apps(
session: Database,
@@ -101,6 +104,7 @@ async def get_user_oauth_apps(
status_code=204,
name="删除 OAuth 应用",
description="删除指定的 OAuth 应用程序及其关联的所有令牌",
tags=["osu! OAuth 认证", "g0v0 API"],
)
async def delete_oauth_app(
session: Database,
@@ -125,6 +129,7 @@ async def delete_oauth_app(
"/oauth-app/{client_id}",
name="更新 OAuth 应用",
description="更新指定 OAuth 应用的名称、描述和重定向 URI",
tags=["osu! OAuth 认证", "g0v0 API"],
)
async def update_oauth_app(
session: Database,
@@ -158,6 +163,7 @@ async def update_oauth_app(
"/oauth-app/{client_id}/refresh",
name="刷新 OAuth 密钥",
description="为指定的 OAuth 应用生成新的客户端密钥,并使所有现有的令牌失效",
tags=["osu! OAuth 认证", "g0v0 API"],
)
async def refresh_secret(
session: Database,
@@ -189,6 +195,7 @@ async def refresh_secret(
"/oauth-app/{client_id}/code",
name="生成 OAuth 授权码",
description="为特定用户和 OAuth 应用生成授权码,用于授权码授权流程",
tags=["osu! OAuth 认证", "g0v0 API"],
)
async def generate_oauth_code(
session: Database,

View File

@@ -23,6 +23,7 @@ class CheckResponse(BaseModel):
name="检查关系状态",
description="检查当前用户与指定用户的关系状态",
response_model=CheckResponse,
tags=["用户关系", "g0v0 API"],
)
async def check_user_relationship(
db: Database,

View File

@@ -1,11 +1,5 @@
from __future__ import annotations
from app.config import settings
from fastapi import APIRouter
router = APIRouter(
prefix="/api/private",
include_in_schema=settings.debug,
tags=["私有 API"],
)
router = APIRouter(prefix="/api/private")

View File

@@ -23,7 +23,7 @@ from pydantic import BaseModel
from sqlmodel import exists, select
@router.post("/team", name="创建战队", response_model=Team)
@router.post("/team", name="创建战队", response_model=Team, tags=["战队", "g0v0 API"])
async def create_team(
session: Database,
storage: StorageService = Depends(get_storage_service),
@@ -78,7 +78,7 @@ async def create_team(
return team
@router.patch("/team/{team_id}", name="修改战队", response_model=Team)
@router.patch("/team/{team_id}", name="修改战队", response_model=Team, tags=["战队", "g0v0 API"])
async def update_team(
team_id: int,
session: Database,
@@ -152,7 +152,7 @@ async def update_team(
return team
@router.delete("/team/{team_id}", name="删除战队", status_code=204)
@router.delete("/team/{team_id}", name="删除战队", status_code=204, tags=["战队", "g0v0 API"])
async def delete_team(
session: Database,
team_id: int = Path(..., description="战队 ID"),
@@ -178,7 +178,7 @@ class TeamQueryResp(BaseModel):
members: list[UserResp]
@router.get("/team/{team_id}", name="查询战队", response_model=TeamQueryResp)
@router.get("/team/{team_id}", name="查询战队", response_model=TeamQueryResp, tags=["用户", "g0v0 API"])
async def get_team(
session: Database,
team_id: int = Path(..., description="战队 ID"),
@@ -190,7 +190,7 @@ async def get_team(
)
@router.post("/team/{team_id}/request", name="请求加入战队", status_code=204)
@router.post("/team/{team_id}/request", name="请求加入战队", status_code=204, tags=["战队", "g0v0 API"])
async def request_join_team(
session: Database,
team_id: int = Path(..., description="战队 ID"),
@@ -216,8 +216,8 @@ async def request_join_team(
await server.new_private_notification(TeamApplicationStore.init(team_request))
@router.post("/team/{team_id}/{user_id}/request", name="接受加入请求", status_code=204)
@router.delete("/team/{team_id}/{user_id}/request", name="拒绝加入请求", status_code=204)
@router.post("/team/{team_id}/{user_id}/request", name="接受加入请求", status_code=204, tags=["战队", "g0v0 API"])
@router.delete("/team/{team_id}/{user_id}/request", name="拒绝加入请求", status_code=204, tags=["战队", "g0v0 API"])
async def handle_request(
req: Request,
session: Database,
@@ -255,7 +255,7 @@ async def handle_request(
await session.commit()
@router.delete("/team/{team_id}/{user_id}", name="踢出成员 / 退出战队", status_code=204)
@router.delete("/team/{team_id}/{user_id}", name="踢出成员 / 退出战队", status_code=204, tags=["战队", "g0v0 API"])
async def kick_member(
session: Database,
team_id: int = Path(..., description="战队 ID"),

View File

@@ -14,10 +14,7 @@ from fastapi import Body, HTTPException, Security
from sqlmodel import exists, select
@router.post(
"/rename",
name="修改用户名",
)
@router.post("/rename", name="修改用户名", tags=["用户", "g0v0 API"])
async def user_rename(
session: Database,
new_name: str = Body(..., description="新的用户名"),

View File

@@ -151,7 +151,7 @@ async def get_beatmapset(
"/beatmapsets/{beatmapset_id}/download",
tags=["谱面集"],
name="下载谱面集",
description="**客户端专属**\n下载谱面集文件。基于请求IP地理位置智能分流支持负载均衡和自动故障转移。中国IP使用Sayobot镜像其他地区使用Nerinyan和OsuDirect镜像。",
description="\n下载谱面集文件。基于请求IP地理位置智能分流支持负载均衡和自动故障转移。中国IP使用Sayobot镜像其他地区使用Nerinyan和OsuDirect镜像。",
)
async def download_beatmapset(
request: Request,
@@ -189,7 +189,7 @@ async def download_beatmapset(
"/beatmapsets/{beatmapset_id}/favourites",
tags=["谱面集"],
name="收藏或取消收藏谱面集",
description="**客户端专属**\n收藏或取消收藏指定谱面集。",
description="\n收藏或取消收藏指定谱面集。",
)
async def favourite_beatmapset(
db: Database,

View File

@@ -53,13 +53,13 @@ class AddFriendResp(BaseModel):
tags=["用户关系"],
response_model=AddFriendResp,
name="添加或更新好友关系",
description="**客户端专属**\n添加或更新与目标用户的好友关系。",
description="\n添加或更新与目标用户的好友关系。",
)
@router.post(
"/blocks",
tags=["用户关系"],
name="添加或更新屏蔽关系",
description="**客户端专属**\n添加或更新与目标用户的屏蔽关系。",
description="\n添加或更新与目标用户的屏蔽关系。",
)
async def add_relationship(
db: Database,
@@ -119,13 +119,13 @@ async def add_relationship(
"/friends/{target}",
tags=["用户关系"],
name="取消好友关系",
description="**客户端专属**\n删除与目标用户的好友关系。",
description="\n删除与目标用户的好友关系。",
)
@router.delete(
"/blocks/{target}",
tags=["用户关系"],
name="取消屏蔽关系",
description="**客户端专属**\n删除与目标用户的屏蔽关系。",
description="\n删除与目标用户的屏蔽关系。",
)
async def delete_relationship(
db: Database,

View File

@@ -135,7 +135,7 @@ async def _participate_room(room_id: int, user_id: int, db_room: Room, session:
tags=["房间"],
response_model=APICreatedRoom,
name="创建房间",
description="**客户端专属**\n创建一个新的房间。",
description="\n创建一个新的房间。",
)
async def create_room(
db: Database,
@@ -181,7 +181,7 @@ async def get_room(
"/rooms/{room_id}",
tags=["房间"],
name="结束房间",
description="**客户端专属**\n结束歌单模式房间。",
description="\n结束歌单模式房间。",
)
async def delete_room(
db: Database,
@@ -201,7 +201,7 @@ async def delete_room(
"/rooms/{room_id}/users/{user_id}",
tags=["房间"],
name="加入房间",
description="**客户端专属**\n加入指定歌单模式房间。",
description="\n加入指定歌单模式房间。",
)
async def add_user_to_room(
db: Database,
@@ -225,7 +225,7 @@ async def add_user_to_room(
"/rooms/{room_id}/users/{user_id}",
tags=["房间"],
name="离开房间",
description="**客户端专属**\n离开指定歌单模式房间。",
description="\n离开指定歌单模式房间。",
)
async def remove_user_from_room(
db: Database,

View File

@@ -402,7 +402,7 @@ async def get_user_all_beatmap_scores(
tags=["游玩"],
response_model=ScoreTokenResp,
name="创建单曲成绩提交令牌",
description="**客户端专属**\n为指定谱面创建一次性的成绩提交令牌。",
description="\n为指定谱面创建一次性的成绩提交令牌。",
)
async def create_solo_score(
background_task: BackgroundTasks,
@@ -434,7 +434,7 @@ async def create_solo_score(
tags=["游玩"],
response_model=ScoreResp,
name="提交单曲成绩",
description="**客户端专属**\n使用令牌提交单曲成绩。",
description="\n使用令牌提交单曲成绩。",
)
async def submit_solo_score(
background_task: BackgroundTasks,
@@ -454,7 +454,7 @@ async def submit_solo_score(
tags=["游玩"],
response_model=ScoreTokenResp,
name="创建房间项目成绩令牌",
description="**客户端专属**\n为房间游玩项目创建成绩提交令牌。",
description="\n为房间游玩项目创建成绩提交令牌。",
)
async def create_playlist_score(
session: Database,
@@ -520,7 +520,7 @@ async def create_playlist_score(
"/rooms/{room_id}/playlist/{playlist_id}/scores/{token}",
tags=["游玩"],
name="提交房间项目成绩",
description="**客户端专属**\n提交房间游玩项目成绩。",
description="\n提交房间游玩项目成绩。",
)
async def submit_playlist_score(
background_task: BackgroundTasks,
@@ -753,7 +753,7 @@ async def get_user_playlist_score(
"/score-pins/{score_id}",
status_code=204,
name="置顶成绩",
description="**客户端专属**\n将指定成绩置顶到用户主页 (按顺序)。",
description="\n将指定成绩置顶到用户主页 (按顺序)。",
tags=["成绩"],
)
async def pin_score(
@@ -798,7 +798,7 @@ async def pin_score(
"/score-pins/{score_id}",
status_code=204,
name="取消置顶成绩",
description="**客户端专属**\n取消置顶指定成绩。",
description="\n取消置顶指定成绩。",
tags=["成绩"],
)
async def unpin_score(
@@ -833,7 +833,7 @@ async def unpin_score(
"/score-pins/{score_id}/reorder",
status_code=204,
name="调整置顶成绩顺序",
description=("**客户端专属**\n调整已置顶成绩的展示顺序。仅提供 after_score_id 或 before_score_id 之一。"),
description=("\n调整已置顶成绩的展示顺序。仅提供 after_score_id 或 before_score_id 之一。"),
tags=["成绩"],
)
async def reorder_score_pin(

View File

@@ -9,10 +9,8 @@ from typing import Annotated
from app.database import User
from app.dependencies import get_current_user
from app.dependencies.database import Database, get_redis
from app.dependencies.geoip import GeoIPHelper, get_geoip_helper
from app.service.email_verification_service import (
EmailVerificationService,
LoginSessionService,
)
from app.service.login_log_service import LoginLogService
@@ -32,10 +30,7 @@ class SessionReissueResponse(BaseModel):
@router.post(
"/session/verify",
name="验证会话",
description="验证邮件验证码并完成会话认证",
status_code=204,
"/session/verify", name="验证会话", description="验证邮件验证码并完成会话认证", status_code=204, tags=["验证"]
)
async def verify_session(
request: Request,
@@ -101,6 +96,7 @@ async def verify_session(
name="重新发送验证码",
description="重新发送邮件验证码",
response_model=SessionReissueResponse,
tags=["验证"],
)
async def reissue_verification_code(
request: Request,
@@ -141,40 +137,3 @@ async def reissue_verification_code(
return SessionReissueResponse(success=False, message="无效的用户会话")
except Exception:
return SessionReissueResponse(success=False, message="重新发送过程中发生错误")
@router.post(
"/session/check-new-location",
name="检查新位置登录",
description="检查登录是否来自新位置(内部接口)",
)
async def check_new_location(
request: Request,
db: Database,
user_id: int,
geoip: GeoIPHelper = Depends(get_geoip_helper),
):
"""
检查是否为新位置登录
这是一个内部接口,用于登录流程中判断是否需要邮件验证
"""
try:
from app.dependencies.geoip import get_client_ip
ip_address = get_client_ip(request)
geo_info = geoip.lookup(ip_address)
country_code = geo_info.get("country_iso", "XX")
is_new_location = await LoginSessionService.check_new_location(db, user_id, ip_address, country_code)
return {
"is_new_location": is_new_location,
"ip_address": ip_address,
"country_code": country_code,
}
except Exception as e:
return {
"is_new_location": True, # 出错时默认为新位置
"error": str(e),
}

30
main.py
View File

@@ -80,11 +80,31 @@ async def lifespan(app: FastAPI):
await redis_client.aclose()
desc = (
"osu! API 模拟服务器,支持 osu! API v1, v2 和 osu!lazer 的绝大部分功能。\n\n"
"官方文档:[osu!web 文档](https://osu.ppy.sh/docs/index.html)\n\n"
"V1 API 文档:[osu-api](https://github.com/ppy/osu-api/wiki)"
)
desc = """osu! API 模拟服务器,支持 osu! API v1, v2 和 osu!lazer 的绝大部分功能。
## 端点说明
所有 v2 API 均以 `/api/v2/` 开头,所有 v1 API 均以 `/api/v1` 开头(直接访问 `/api` 的 v1 API 会进行重定向)。
所有 g0v0-server 提供的额外 APIg0v0-api 均以 `/api/private/` 开头。
## 鉴权
v2 API 采用 OAuth 2.0 鉴权,支持以下鉴权方式:
- `password` 密码鉴权,仅适用于 osu!lazer 客户端和前端等服务,需要提供用户的用户名和密码进行登录。
- `authorization_code` 授权码鉴权,适用于第三方应用,需要提供用户的授权码进行登录。
- `client_credentials` 客户端凭证鉴权,适用于服务端应用,需要提供客户端 ID 和客户端密钥进行登录。
使用 `password` 鉴权的具有全部权限。`authorization_code` 具有指定 scope 的权限。`client_credentials` 只有 `public` 权限。各接口需要的权限请查看每个 Endpoint 的 Authorization。
v1 API 采用 API Key 鉴权,将 API Key 放入 Query `k` 中。
## 参考
- v2 API 文档:[osu-web 文档](https://osu.ppy.sh/docs/index.html)
- v1 API 文档:[osu-api](https://github.com/ppy/osu-api/wiki)
""" # noqa: E501
# 检查 New Relic 配置文件是否存在,如果存在则初始化 New Relic
newrelic_config_path = Path("newrelic.ini")