feat(api): 添加测试,小修小补
- **未经测试**
This commit is contained in:
@@ -13,10 +13,11 @@ RUN apt-get update && apt-get install -y \
|
|||||||
# 复制依赖文件
|
# 复制依赖文件
|
||||||
COPY uv.lock .
|
COPY uv.lock .
|
||||||
COPY pyproject.toml .
|
COPY pyproject.toml .
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
# 安装Python依赖
|
# 安装Python依赖
|
||||||
RUN uv sync --locked
|
RUN uv sync
|
||||||
RUN pip install uvicorn
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
# 复制应用代码
|
# 复制应用代码
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ load_dotenv()
|
|||||||
class Settings:
|
class Settings:
|
||||||
# 数据库设置
|
# 数据库设置
|
||||||
DATABASE_URL: str = os.getenv(
|
DATABASE_URL: str = os.getenv(
|
||||||
"DATABASE_URL", "mysql+aiomysql://root:password@localhost:3306/osu_api"
|
"DATABASE_URL", "mysql+aiomysql://root:password@127.0.0.1:3306/osu_api"
|
||||||
)
|
)
|
||||||
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
REDIS_URL: str = os.getenv("REDIS_URL", "redis://127.0.0.1:6379/0")
|
||||||
|
|
||||||
# JWT 设置
|
# JWT 设置
|
||||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-here")
|
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-here")
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class BeatmapsetBase(SQLModel):
|
|||||||
# Beatmapset
|
# Beatmapset
|
||||||
artist: str = Field(index=True)
|
artist: str = Field(index=True)
|
||||||
artist_unicode: str = Field(index=True)
|
artist_unicode: str = Field(index=True)
|
||||||
covers: BeatmapCovers = Field(sa_column=Column(JSON))
|
covers: BeatmapCovers | None = Field(sa_column=Column(JSON))
|
||||||
creator: str
|
creator: str
|
||||||
favourite_count: int
|
favourite_count: int
|
||||||
nsfw: bool = Field(default=False)
|
nsfw: bool = Field(default=False)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from sqlalchemy import JSON, Column, DateTime
|
from sqlalchemy import JSON, Column, DateTime
|
||||||
from sqlmodel import Field, Relationship, SQLModel
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
from sqlalchemy.orm import Mapped
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
from .user import User
|
||||||
@@ -70,7 +71,7 @@ class LegacyUserStatistics(SQLModel, table=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 关联关系
|
# 关联关系
|
||||||
user: "User" = Relationship(back_populates="statistics")
|
user: Mapped["User"] = Relationship(back_populates="statistics")
|
||||||
|
|
||||||
|
|
||||||
class LegacyOAuthToken(SQLModel, table=True):
|
class LegacyOAuthToken(SQLModel, table=True):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from datetime import datetime
|
|||||||
import math
|
import math
|
||||||
from typing import Literal, TYPE_CHECKING, List
|
from typing import Literal, TYPE_CHECKING, List
|
||||||
|
|
||||||
from app.models.score import Rank, APIMod, GameMode
|
from app.models.score import Rank, APIMod, GameMode, MODE_TO_INT
|
||||||
|
|
||||||
from .beatmap import Beatmap, BeatmapResp
|
from .beatmap import Beatmap, BeatmapResp
|
||||||
from .beatmapset import Beatmapset, BeatmapsetResp
|
from .beatmapset import Beatmapset, BeatmapsetResp
|
||||||
@@ -84,6 +84,7 @@ class ScoreResp(ScoreBase):
|
|||||||
legacy_total_score: int = 0 # FIXME
|
legacy_total_score: int = 0 # FIXME
|
||||||
processed: bool = False # solo_score
|
processed: bool = False # solo_score
|
||||||
weight: float = 0.0
|
weight: float = 0.0
|
||||||
|
ruleset_id: int | None
|
||||||
beatmap: BeatmapResp | None = None
|
beatmap: BeatmapResp | None = None
|
||||||
beatmapset: BeatmapsetResp | None = None
|
beatmapset: BeatmapsetResp | None = None
|
||||||
# FIXME: user: APIUser | None = None
|
# FIXME: user: APIUser | None = None
|
||||||
@@ -96,6 +97,7 @@ class ScoreResp(ScoreBase):
|
|||||||
s.beatmapset = BeatmapsetResp.from_db(score.beatmap.beatmapset)
|
s.beatmapset = BeatmapsetResp.from_db(score.beatmap.beatmapset)
|
||||||
s.is_perfect_combo = s.max_combo == s.beatmap.max_combo
|
s.is_perfect_combo = s.max_combo == s.beatmap.max_combo
|
||||||
s.legacy_perfect = s.max_combo == s.beatmap.max_combo
|
s.legacy_perfect = s.max_combo == s.beatmap.max_combo
|
||||||
|
s.ruleset_id=MODE_TO_INT[score.ruleset_id]
|
||||||
if score.best_id:
|
if score.best_id:
|
||||||
# https://osu.ppy.sh/wiki/Performance_points/Weighting_system
|
# https://osu.ppy.sh/wiki/Performance_points/Weighting_system
|
||||||
s.weight = math.pow(0.95, score.best_id)
|
s.weight = math.pow(0.95, score.best_id)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime
|
from sqlalchemy import Column, DateTime
|
||||||
|
from sqlalchemy.orm import Mapped
|
||||||
from sqlmodel import Field, Relationship, SQLModel
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -20,7 +21,7 @@ class Team(SQLModel, table=True):
|
|||||||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||||||
)
|
)
|
||||||
|
|
||||||
members: list["TeamMember"] = Relationship(back_populates="team")
|
members: Mapped[list["TeamMember"]] = Relationship(back_populates="team")
|
||||||
|
|
||||||
|
|
||||||
class TeamMember(SQLModel, table=True):
|
class TeamMember(SQLModel, table=True):
|
||||||
@@ -33,5 +34,5 @@ class TeamMember(SQLModel, table=True):
|
|||||||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||||||
)
|
)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="team_membership")
|
user: Mapped["User"] = Relationship(back_populates="team_membership")
|
||||||
team: "Team" = Relationship(back_populates="members")
|
team: Mapped["Team"] = Relationship(back_populates="members")
|
||||||
@@ -10,6 +10,7 @@ from .team import TeamMember
|
|||||||
|
|
||||||
from sqlalchemy import DECIMAL, JSON, Column, Date, DateTime, Text
|
from sqlalchemy import DECIMAL, JSON, Column, Date, DateTime, Text
|
||||||
from sqlalchemy.dialects.mysql import VARCHAR
|
from sqlalchemy.dialects.mysql import VARCHAR
|
||||||
|
from sqlalchemy.orm import Mapped
|
||||||
from sqlmodel import BigInteger, Field, Relationship, SQLModel
|
from sqlmodel import BigInteger, Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
|
||||||
@@ -69,31 +70,31 @@ class User(SQLModel, table=True):
|
|||||||
return datetime.fromtimestamp(latest_activity) if latest_activity > 0 else None
|
return datetime.fromtimestamp(latest_activity) if latest_activity > 0 else None
|
||||||
|
|
||||||
# 关联关系
|
# 关联关系
|
||||||
lazer_profile: Optional["LazerUserProfile"] = Relationship(back_populates="user")
|
lazer_profile: Mapped[Optional["LazerUserProfile"]] = Relationship(back_populates="user")
|
||||||
lazer_statistics: list["LazerUserStatistics"] = Relationship(back_populates="user")
|
lazer_statistics: Mapped[list["LazerUserStatistics"]] = Relationship(back_populates="user")
|
||||||
lazer_counts: Optional["LazerUserCounts"] = Relationship(back_populates="user")
|
lazer_counts: Mapped[Optional["LazerUserCounts"]] = Relationship(back_populates="user")
|
||||||
lazer_achievements: list["LazerUserAchievement"] = Relationship(
|
lazer_achievements: Mapped[list["LazerUserAchievement"]] = Relationship(
|
||||||
back_populates="user"
|
back_populates="user"
|
||||||
)
|
)
|
||||||
lazer_profile_sections: list["LazerUserProfileSections"] = Relationship(
|
lazer_profile_sections: Mapped[list["LazerUserProfileSections"]] = Relationship(
|
||||||
back_populates="user"
|
back_populates="user"
|
||||||
)
|
)
|
||||||
statistics: list[LegacyUserStatistics] = Relationship(back_populates="user")
|
statistics: list["LegacyUserStatistics"] = Relationship(back_populates="user")
|
||||||
team_membership: list[TeamMember] = Relationship(back_populates="user")
|
team_membership: Mapped[list["TeamMember"]] = Relationship(back_populates="user")
|
||||||
daily_challenge_stats: Optional["DailyChallengeStats"] = Relationship(
|
daily_challenge_stats: Mapped[Optional["DailyChallengeStats"]] = Relationship(
|
||||||
back_populates="user"
|
back_populates="user"
|
||||||
)
|
)
|
||||||
rank_history: list["RankHistory"] = Relationship(back_populates="user")
|
rank_history: Mapped[list["RankHistory"]] = Relationship(back_populates="user")
|
||||||
avatar: Optional["UserAvatar"] = Relationship(back_populates="user")
|
avatar: Mapped[Optional["UserAvatar"]] = Relationship(back_populates="user")
|
||||||
active_banners: list["LazerUserBanners"] = Relationship(back_populates="user")
|
active_banners: Mapped[list["LazerUserBanners"]] = Relationship(back_populates="user")
|
||||||
lazer_badges: list["LazerUserBadge"] = Relationship(back_populates="user")
|
lazer_badges: Mapped[list["LazerUserBadge"]] = Relationship(back_populates="user")
|
||||||
lazer_monthly_playcounts: list["LazerUserMonthlyPlaycounts"] = Relationship(
|
lazer_monthly_playcounts: Mapped[list["LazerUserMonthlyPlaycounts"]] = Relationship(
|
||||||
back_populates="user"
|
back_populates="user"
|
||||||
)
|
)
|
||||||
lazer_previous_usernames: list["LazerUserPreviousUsername"] = Relationship(
|
lazer_previous_usernames: Mapped[list["LazerUserPreviousUsername"]] = Relationship(
|
||||||
back_populates="user"
|
back_populates="user"
|
||||||
)
|
)
|
||||||
lazer_replays_watched: list["LazerUserReplaysWatched"] = Relationship(
|
lazer_replays_watched: Mapped[list["LazerUserReplaysWatched"]] = Relationship(
|
||||||
back_populates="user"
|
back_populates="user"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ class LazerUserProfile(SQLModel, table=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 关联关系
|
# 关联关系
|
||||||
user: "User" = Relationship(back_populates="lazer_profile")
|
user: Mapped["User"] = Relationship(back_populates="lazer_profile")
|
||||||
|
|
||||||
|
|
||||||
class LazerUserProfileSections(SQLModel, table=True):
|
class LazerUserProfileSections(SQLModel, table=True):
|
||||||
@@ -172,7 +173,7 @@ class LazerUserProfileSections(SQLModel, table=True):
|
|||||||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||||||
)
|
)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="lazer_profile_sections")
|
user: Mapped["User"] = Relationship(back_populates="lazer_profile_sections")
|
||||||
|
|
||||||
|
|
||||||
class LazerUserCountry(SQLModel, table=True):
|
class LazerUserCountry(SQLModel, table=True):
|
||||||
@@ -237,7 +238,7 @@ class LazerUserCounts(SQLModel, table=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 关联关系
|
# 关联关系
|
||||||
user: "User" = Relationship(back_populates="lazer_counts")
|
user: Mapped["User"] = Relationship(back_populates="lazer_counts")
|
||||||
|
|
||||||
|
|
||||||
class LazerUserStatistics(SQLModel, table=True):
|
class LazerUserStatistics(SQLModel, table=True):
|
||||||
@@ -297,7 +298,7 @@ class LazerUserStatistics(SQLModel, table=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 关联关系
|
# 关联关系
|
||||||
user: "User" = Relationship(back_populates="lazer_statistics")
|
user: Mapped["User"] = Relationship(back_populates="lazer_statistics")
|
||||||
|
|
||||||
|
|
||||||
class LazerUserBanners(SQLModel, table=True):
|
class LazerUserBanners(SQLModel, table=True):
|
||||||
@@ -310,7 +311,7 @@ class LazerUserBanners(SQLModel, table=True):
|
|||||||
is_active: bool | None = Field(default=None)
|
is_active: bool | None = Field(default=None)
|
||||||
|
|
||||||
# 修正user关系的back_populates值
|
# 修正user关系的back_populates值
|
||||||
user: "User" = Relationship(back_populates="active_banners")
|
user: Mapped["User"] = Relationship(back_populates="active_banners")
|
||||||
|
|
||||||
|
|
||||||
class LazerUserAchievement(SQLModel, table=True):
|
class LazerUserAchievement(SQLModel, table=True):
|
||||||
@@ -323,7 +324,7 @@ class LazerUserAchievement(SQLModel, table=True):
|
|||||||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||||||
)
|
)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="lazer_achievements")
|
user: Mapped["User"] = Relationship(back_populates="lazer_achievements")
|
||||||
|
|
||||||
|
|
||||||
class LazerUserBadge(SQLModel, table=True):
|
class LazerUserBadge(SQLModel, table=True):
|
||||||
@@ -344,7 +345,7 @@ class LazerUserBadge(SQLModel, table=True):
|
|||||||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||||||
)
|
)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="lazer_badges")
|
user: Mapped["User"] = Relationship(back_populates="lazer_badges")
|
||||||
|
|
||||||
|
|
||||||
class LazerUserMonthlyPlaycounts(SQLModel, table=True):
|
class LazerUserMonthlyPlaycounts(SQLModel, table=True):
|
||||||
@@ -362,7 +363,7 @@ class LazerUserMonthlyPlaycounts(SQLModel, table=True):
|
|||||||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||||||
)
|
)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="lazer_monthly_playcounts")
|
user: Mapped["User"] = Relationship(back_populates="lazer_monthly_playcounts")
|
||||||
|
|
||||||
|
|
||||||
class LazerUserPreviousUsername(SQLModel, table=True):
|
class LazerUserPreviousUsername(SQLModel, table=True):
|
||||||
@@ -380,7 +381,7 @@ class LazerUserPreviousUsername(SQLModel, table=True):
|
|||||||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||||||
)
|
)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="lazer_previous_usernames")
|
user: Mapped["User"] = Relationship(back_populates="lazer_previous_usernames")
|
||||||
|
|
||||||
|
|
||||||
class LazerUserReplaysWatched(SQLModel, table=True):
|
class LazerUserReplaysWatched(SQLModel, table=True):
|
||||||
@@ -398,7 +399,7 @@ class LazerUserReplaysWatched(SQLModel, table=True):
|
|||||||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||||||
)
|
)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="lazer_replays_watched")
|
user: Mapped["User"] = Relationship(back_populates="lazer_replays_watched")
|
||||||
|
|
||||||
|
|
||||||
# 类型转换用的 UserAchievement(不是 SQLAlchemy 模型)
|
# 类型转换用的 UserAchievement(不是 SQLAlchemy 模型)
|
||||||
@@ -426,7 +427,7 @@ class DailyChallengeStats(SQLModel, table=True):
|
|||||||
weekly_streak_best: int = Field(default=0)
|
weekly_streak_best: int = Field(default=0)
|
||||||
weekly_streak_current: int = Field(default=0)
|
weekly_streak_current: int = Field(default=0)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="daily_challenge_stats")
|
user: Mapped["User"] = Relationship(back_populates="daily_challenge_stats")
|
||||||
|
|
||||||
|
|
||||||
class RankHistory(SQLModel, table=True):
|
class RankHistory(SQLModel, table=True):
|
||||||
@@ -440,7 +441,7 @@ class RankHistory(SQLModel, table=True):
|
|||||||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||||||
)
|
)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="rank_history")
|
user: Mapped["User"] = Relationship(back_populates="rank_history")
|
||||||
|
|
||||||
|
|
||||||
class UserAvatar(SQLModel, table=True):
|
class UserAvatar(SQLModel, table=True):
|
||||||
@@ -458,4 +459,4 @@ class UserAvatar(SQLModel, table=True):
|
|||||||
r2_original_url: str | None = Field(default=None, max_length=500)
|
r2_original_url: str | None = Field(default=None, max_length=500)
|
||||||
r2_game_url: str | None = Field(default=None, max_length=500)
|
r2_game_url: str | None = Field(default=None, max_length=500)
|
||||||
|
|
||||||
user: "User" = Relationship(back_populates="avatar")
|
user: Mapped["User"] = Relationship(back_populates="avatar")
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ from app.auth import get_password_hash
|
|||||||
from app.database import (
|
from app.database import (
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
from app.database.beatmapset import Beatmapset, BeatmapsetResp
|
||||||
|
from app.database.beatmap import Beatmap, BeatmapResp
|
||||||
|
from app.database.score import Score
|
||||||
|
from app.models.score import GameMode, Rank, APIMod
|
||||||
|
from app.models.beatmap import BeatmapRankStatus, Genre, Language
|
||||||
from app.dependencies.database import create_tables, engine
|
from app.dependencies.database import create_tables, engine
|
||||||
|
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
@@ -72,10 +77,138 @@ async def create_sample_user():
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def create_sample_beatmap_data(user: User):
|
||||||
|
"""创建示例谱面数据"""
|
||||||
|
async with AsyncSession(engine) as session:
|
||||||
|
async with session.begin():
|
||||||
|
# 检查谱面集是否已存在
|
||||||
|
statement = select(Beatmapset).where(Beatmapset.id == 1)
|
||||||
|
result = await session.execute(statement)
|
||||||
|
existing_beatmapset = result.scalars().first()
|
||||||
|
if existing_beatmapset:
|
||||||
|
print("示例谱面集已存在,跳过创建")
|
||||||
|
return existing_beatmapset
|
||||||
|
|
||||||
|
# 创建谱面集
|
||||||
|
beatmapset = Beatmapset(
|
||||||
|
id=1,
|
||||||
|
artist="Example Artist",
|
||||||
|
artist_unicode="Example Artist",
|
||||||
|
covers=None,
|
||||||
|
creator="Googujiang",
|
||||||
|
favourite_count=0,
|
||||||
|
hype_current=0,
|
||||||
|
hype_required=0,
|
||||||
|
nsfw=False,
|
||||||
|
play_count=0,
|
||||||
|
preview_url="",
|
||||||
|
source="",
|
||||||
|
spotlight=False,
|
||||||
|
title="Example Song",
|
||||||
|
title_unicode="Example Song",
|
||||||
|
user_id=user.id,
|
||||||
|
video=False,
|
||||||
|
availability_info=None,
|
||||||
|
download_disabled=False,
|
||||||
|
bpm=180.0,
|
||||||
|
can_be_hyped=False,
|
||||||
|
discussion_locked=False,
|
||||||
|
last_updated=datetime.now(),
|
||||||
|
ranked_date=datetime.now(),
|
||||||
|
storyboard=False,
|
||||||
|
submitted_date=datetime.now(),
|
||||||
|
current_nominations=[],
|
||||||
|
beatmap_status=BeatmapRankStatus.RANKED,
|
||||||
|
beatmap_genre=Genre.ANY, # 使用整数表示Genre枚举
|
||||||
|
beatmap_language=Language.ANY, # 使用整数表示Language枚举
|
||||||
|
nominations_required=0,
|
||||||
|
nominations_current=0,
|
||||||
|
pack_tags=[],
|
||||||
|
ratings=[],
|
||||||
|
)
|
||||||
|
session.add(beatmapset)
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
# 创建谱面
|
||||||
|
beatmap = Beatmap(
|
||||||
|
id=1,
|
||||||
|
url="",
|
||||||
|
mode=GameMode.OSU,
|
||||||
|
beatmapset_id=1,
|
||||||
|
difficulty_rating=5.5,
|
||||||
|
beatmap_status=BeatmapRankStatus.RANKED,
|
||||||
|
total_length=195,
|
||||||
|
user_id=user.id,
|
||||||
|
version="Example Difficulty",
|
||||||
|
checksum="example_checksum",
|
||||||
|
current_user_playcount=0,
|
||||||
|
max_combo=1200,
|
||||||
|
ar=9.0,
|
||||||
|
cs=4.0,
|
||||||
|
drain=5.0,
|
||||||
|
accuracy=8.0,
|
||||||
|
bpm=180.0,
|
||||||
|
count_circles=1000,
|
||||||
|
count_sliders=200,
|
||||||
|
count_spinners=1,
|
||||||
|
deleted_at=None,
|
||||||
|
hit_length=180,
|
||||||
|
last_updated=datetime.now(),
|
||||||
|
passcount=10,
|
||||||
|
playcount=50,
|
||||||
|
)
|
||||||
|
session.add(beatmap)
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
# 创建成绩
|
||||||
|
score = Score(
|
||||||
|
id=1,
|
||||||
|
accuracy=0.9876,
|
||||||
|
map_md5="example_checksum",
|
||||||
|
best_id=1,
|
||||||
|
build_id=None,
|
||||||
|
classic_total_score=1234567,
|
||||||
|
ended_at=datetime.now(),
|
||||||
|
has_replay=True,
|
||||||
|
max_combo=1100,
|
||||||
|
mods=[APIMod(acronym="HD"), APIMod(acronym="DT")],
|
||||||
|
passed=True,
|
||||||
|
playlist_item_id=None,
|
||||||
|
pp=250.5,
|
||||||
|
preserve=True,
|
||||||
|
rank=Rank.S,
|
||||||
|
room_id=None,
|
||||||
|
ruleset_id=GameMode.OSU,
|
||||||
|
started_at=datetime.now(),
|
||||||
|
total_score=1234567,
|
||||||
|
type="solo_score",
|
||||||
|
position=None,
|
||||||
|
beatmap_id=1,
|
||||||
|
user_id=user.id,
|
||||||
|
n300=950,
|
||||||
|
n100=30,
|
||||||
|
n50=20,
|
||||||
|
nmiss=5,
|
||||||
|
ngeki=150,
|
||||||
|
nkatu=50,
|
||||||
|
nlarge_tick_miss=None,
|
||||||
|
nslider_tail_hit=None,
|
||||||
|
)
|
||||||
|
session.add(score)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(beatmapset)
|
||||||
|
|
||||||
|
print(f"成功创建示例谱面集: {beatmapset.title} (ID: {beatmapset.id})")
|
||||||
|
print(f"成功创建示例谱面: {beatmap.version} (ID: {beatmap.id})")
|
||||||
|
print(f"成功创建示例成绩: ID {score.id}")
|
||||||
|
return beatmapset
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
print("开始创建示例数据...")
|
print("开始创建示例数据...")
|
||||||
await create_tables()
|
await create_tables()
|
||||||
user = await create_sample_user()
|
user = await create_sample_user()
|
||||||
|
await create_sample_beatmap_data(user)
|
||||||
print("示例数据创建完成!")
|
print("示例数据创建完成!")
|
||||||
print(f"用户名: {user.name}")
|
print(f"用户名: {user.name}")
|
||||||
print("密码: password123")
|
print("密码: password123")
|
||||||
|
|||||||
@@ -1,17 +1,54 @@
|
|||||||
fastapi~=0.116.1
|
|
||||||
uvicorn[standard]==0.24.0
|
|
||||||
sqlalchemy~=2.0.41
|
|
||||||
alembic==1.12.1
|
|
||||||
pymysql~=1.1.1
|
|
||||||
cryptography==41.0.7
|
|
||||||
redis~=6.2.0
|
|
||||||
python-jose[cryptography]~=3.5.0
|
|
||||||
passlib[bcrypt]==1.7.4
|
|
||||||
python-multipart==0.0.6
|
|
||||||
pydantic[email]~=2.11.7
|
|
||||||
python-dotenv~=1.1.1
|
|
||||||
bcrypt~=4.3.0
|
|
||||||
msgpack~=1.1.1
|
|
||||||
sqlmodel~=0.0.24
|
|
||||||
starlette~=0.47.2
|
|
||||||
aiomysql==0.2.0
|
aiomysql==0.2.0
|
||||||
|
alembic==1.16.4
|
||||||
|
annotated-types==0.7.0
|
||||||
|
anyio==4.9.0
|
||||||
|
async-timeout==5.0.1
|
||||||
|
bcrypt==4.3.0
|
||||||
|
cffi==1.17.1
|
||||||
|
cfgv==3.4.0
|
||||||
|
click==8.2.1
|
||||||
|
colorama==0.4.6
|
||||||
|
cryptography==45.0.5
|
||||||
|
distlib==0.4.0
|
||||||
|
dnspython==2.7.0
|
||||||
|
ecdsa==0.19.1
|
||||||
|
email-validator==2.2.0
|
||||||
|
fastapi==0.116.1
|
||||||
|
filelock==3.18.0
|
||||||
|
greenlet==3.2.3
|
||||||
|
h11==0.16.0
|
||||||
|
httptools==0.6.4
|
||||||
|
identify==2.6.12
|
||||||
|
idna==3.10
|
||||||
|
mako==1.3.10
|
||||||
|
markupsafe==3.0.2
|
||||||
|
msgpack==1.1.1
|
||||||
|
msgpack-types==0.5.0
|
||||||
|
nodeenv==1.9.1
|
||||||
|
passlib==1.7.4
|
||||||
|
platformdirs==4.3.8
|
||||||
|
pre-commit==4.2.0
|
||||||
|
pyasn1==0.6.1
|
||||||
|
pycparser==2.22
|
||||||
|
pydantic==2.11.7
|
||||||
|
pydantic-core==2.33.2
|
||||||
|
pymysql==1.1.1
|
||||||
|
python-dotenv==1.1.1
|
||||||
|
python-jose==3.5.0
|
||||||
|
python-multipart==0.0.20
|
||||||
|
pyyaml==6.0.2
|
||||||
|
redis==6.2.0
|
||||||
|
rsa==4.9.1
|
||||||
|
ruff==0.12.4
|
||||||
|
six==1.17.0
|
||||||
|
sniffio==1.3.1
|
||||||
|
sqlalchemy==2.0.41
|
||||||
|
sqlmodel==0.0.24
|
||||||
|
starlette==0.47.2
|
||||||
|
typing-extensions==4.14.1
|
||||||
|
typing-inspection==0.4.1
|
||||||
|
uvicorn==0.35.0
|
||||||
|
uvloop==0.21.0
|
||||||
|
virtualenv==20.32.0
|
||||||
|
watchfiles==1.1.0
|
||||||
|
websockets==15.0.1
|
||||||
37
test_api.py
37
test_api.py
@@ -105,6 +105,25 @@ def get_current_user(access_token: str, ruleset: str = "osu"):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_beatmap_scores(access_token: str, beatmap_id: int):
|
||||||
|
"""获取谱面成绩数据"""
|
||||||
|
url = f"{API_URL}/api/v2/beatmaps/{beatmap_id}/scores"
|
||||||
|
headers = {"Authorization": f"Bearer {access_token}"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f"✅ 成功获取谱面 {beatmap_id} 的成绩数据")
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
print(f"❌ 获取谱面成绩失败: {response.status_code}")
|
||||||
|
print(f"响应内容: {response.text}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 获取谱面成绩请求失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主测试函数"""
|
"""主测试函数"""
|
||||||
print("=== osu! API 模拟服务器测试 ===\n")
|
print("=== osu! API 模拟服务器测试 ===\n")
|
||||||
@@ -149,14 +168,26 @@ def main():
|
|||||||
print(f"游戏次数: {user_data['statistics']['play_count']}")
|
print(f"游戏次数: {user_data['statistics']['play_count']}")
|
||||||
print(f"命中精度: {user_data['statistics']['hit_accuracy']:.2f}%")
|
print(f"命中精度: {user_data['statistics']['hit_accuracy']:.2f}%")
|
||||||
|
|
||||||
# 5. 测试令牌刷新
|
# 5. 测试获取谱面成绩
|
||||||
print("\n5. 测试令牌刷新...")
|
print("\n5. 测试获取谱面成绩...")
|
||||||
|
scores_data = get_beatmap_scores(token_data["access_token"], 1)
|
||||||
|
if scores_data:
|
||||||
|
print(f"谱面成绩总数: {len(scores_data['scores'])}")
|
||||||
|
if scores_data['userScore']:
|
||||||
|
print("用户在该谱面有成绩记录")
|
||||||
|
print(f"用户成绩 ID: {scores_data['userScore']['id']}")
|
||||||
|
print(f"用户成绩分数: {scores_data['userScore']['total_score']}")
|
||||||
|
else:
|
||||||
|
print("用户在该谱面没有成绩记录")
|
||||||
|
|
||||||
|
# 6. 测试令牌刷新
|
||||||
|
print("\n6. 测试令牌刷新...")
|
||||||
new_token_data = refresh_token(token_data["refresh_token"])
|
new_token_data = refresh_token(token_data["refresh_token"])
|
||||||
if new_token_data:
|
if new_token_data:
|
||||||
print(f"新访问令牌: {new_token_data['access_token']}")
|
print(f"新访问令牌: {new_token_data['access_token']}")
|
||||||
|
|
||||||
# 使用新令牌获取用户数据
|
# 使用新令牌获取用户数据
|
||||||
print("\n6. 使用新令牌获取用户数据...")
|
print("\n7. 使用新令牌获取用户数据...")
|
||||||
user_data = get_current_user(new_token_data["access_token"])
|
user_data = get_current_user(new_token_data["access_token"])
|
||||||
if user_data:
|
if user_data:
|
||||||
print(f"✅ 新令牌有效,用户: {user_data['username']}")
|
print(f"✅ 新令牌有效,用户: {user_data['username']}")
|
||||||
|
|||||||
Reference in New Issue
Block a user