From c7f6c76b0f7242e944406f1075c125953e068365 Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Thu, 28 Aug 2025 13:18:06 +0000 Subject: [PATCH] refactor(api): standardizate API Router --- app/router/private/avatar.py | 5 +-- app/router/private/beatmapset_ratings.py | 11 ++++-- app/router/private/cover.py | 5 +-- app/router/private/oauth.py | 7 ++++ app/router/private/relationship.py | 1 + app/router/private/router.py | 8 +---- app/router/private/team.py | 16 ++++----- app/router/private/username.py | 5 +-- app/router/v2/beatmapset.py | 4 +-- app/router/v2/relationship.py | 8 ++--- app/router/v2/room.py | 8 ++--- app/router/v2/score.py | 14 ++++---- app/router/v2/session_verify.py | 45 ++---------------------- main.py | 30 +++++++++++++--- 14 files changed, 73 insertions(+), 94 deletions(-) diff --git a/app/router/private/avatar.py b/app/router/private/avatar.py index 165339c..15c392f 100644 --- a/app/router/private/avatar.py +++ b/app/router/private/avatar.py @@ -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(...), diff --git a/app/router/private/beatmapset_ratings.py b/app/router/private/beatmapset_ratings.py index 2711d8a..5c39bc9 100644 --- a/app/router/private/beatmapset_ratings.py +++ b/app/router/private/beatmapset_ratings.py @@ -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, diff --git a/app/router/private/cover.py b/app/router/private/cover.py index 0a4c4ec..e08fb79 100644 --- a/app/router/private/cover.py +++ b/app/router/private/cover.py @@ -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(...), diff --git a/app/router/private/oauth.py b/app/router/private/oauth.py index 576f154..d3561dc 100644 --- a/app/router/private/oauth.py +++ b/app/router/private/oauth.py @@ -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, diff --git a/app/router/private/relationship.py b/app/router/private/relationship.py index 1dde7af..4698350 100644 --- a/app/router/private/relationship.py +++ b/app/router/private/relationship.py @@ -23,6 +23,7 @@ class CheckResponse(BaseModel): name="检查关系状态", description="检查当前用户与指定用户的关系状态", response_model=CheckResponse, + tags=["用户关系", "g0v0 API"], ) async def check_user_relationship( db: Database, diff --git a/app/router/private/router.py b/app/router/private/router.py index ca65934..ca77832 100644 --- a/app/router/private/router.py +++ b/app/router/private/router.py @@ -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") diff --git a/app/router/private/team.py b/app/router/private/team.py index 509b650..51a39d5 100644 --- a/app/router/private/team.py +++ b/app/router/private/team.py @@ -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"), diff --git a/app/router/private/username.py b/app/router/private/username.py index dd6a7b7..8f2f6e3 100644 --- a/app/router/private/username.py +++ b/app/router/private/username.py @@ -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="新的用户名"), diff --git a/app/router/v2/beatmapset.py b/app/router/v2/beatmapset.py index 9f16169..8e2f95f 100644 --- a/app/router/v2/beatmapset.py +++ b/app/router/v2/beatmapset.py @@ -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, diff --git a/app/router/v2/relationship.py b/app/router/v2/relationship.py index a941f8a..8d0905a 100644 --- a/app/router/v2/relationship.py +++ b/app/router/v2/relationship.py @@ -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, diff --git a/app/router/v2/room.py b/app/router/v2/room.py index 18304f5..7c963a5 100644 --- a/app/router/v2/room.py +++ b/app/router/v2/room.py @@ -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, diff --git a/app/router/v2/score.py b/app/router/v2/score.py index ce7c092..14eab06 100644 --- a/app/router/v2/score.py +++ b/app/router/v2/score.py @@ -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( diff --git a/app/router/v2/session_verify.py b/app/router/v2/session_verify.py index d8596ee..11d4a81 100644 --- a/app/router/v2/session_verify.py +++ b/app/router/v2/session_verify.py @@ -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), - } diff --git a/main.py b/main.py index d4e12b7..7d701f2 100644 --- a/main.py +++ b/main.py @@ -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 提供的额外 API(g0v0-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")