diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..d4add55 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e5787b9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..92ff71d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/osu_lazer_api.iml b/.idea/osu_lazer_api.iml new file mode 100644 index 0000000..e2e520d --- /dev/null +++ b/.idea/osu_lazer_api.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/database copy.py b/app/database copy.py deleted file mode 100644 index 7ba80bc..0000000 --- a/app/database copy.py +++ /dev/null @@ -1,403 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float, Text, JSON, ForeignKey, Date, DECIMAL -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship -from datetime import datetime - -Base = declarative_base() - - -class User(Base): - __tablename__ = "users" - - # 主键 - id = Column(Integer, primary_key=True, index=True) - - # 基本信息(匹配 migrations 中的结构) - name = Column(String(32), unique=True, index=True, nullable=False) # 用户名 - safe_name = Column(String(32), unique=True, index=True, nullable=False) # 安全用户名 - email = Column(String(254), unique=True, index=True, nullable=False) - priv = Column(Integer, default=1, nullable=False) # 权限 - pw_bcrypt = Column(String(60), nullable=False) # bcrypt 哈希密码 - country = Column(String(2), default='CN', nullable=False) # 国家代码 - - # 状态和时间 - silence_end = Column(Integer, default=0, nullable=False) - donor_end = Column(Integer, default=0, nullable=False) - creation_time = Column(Integer, default=0, nullable=False) # Unix 时间戳 - latest_activity = Column(Integer, default=0, nullable=False) # Unix 时间戳 - - # 游戏相关 - preferred_mode = Column(Integer, default=0, nullable=False) # 偏好游戏模式 - play_style = Column(Integer, default=0, nullable=False) # 游戏风格 - - # 扩展信息 - clan_id = Column(Integer, default=0, nullable=False) - clan_priv = Column(Integer, default=0, nullable=False) - custom_badge_name = Column(String(16)) - custom_badge_icon = Column(String(64)) - userpage_content = Column(String(2048)) - api_key = Column(String(36), unique=True) - - # 虚拟字段用于兼容性 - @property - def username(self): - return self.name - - @property - def country_code(self): - return self.country - - @property - def join_date(self): - return datetime.fromtimestamp(self.creation_time) if self.creation_time > 0 else datetime.utcnow() - - @property - def last_visit(self): - return datetime.fromtimestamp(self.latest_activity) if self.latest_activity > 0 else None - - # 关联关系 - lazer_profile = relationship("LazerUserProfile", back_populates="user", uselist=False, cascade="all, delete-orphan") - lazer_statistics = relationship("LazerUserStatistics", back_populates="user", cascade="all, delete-orphan") - lazer_achievements = relationship("LazerUserAchievement", back_populates="user", cascade="all, delete-orphan") - - -# ============================================ -# Lazer API 专用表模型 -# ============================================ - -class LazerUserProfile(Base): - __tablename__ = "lazer_user_profiles" - - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - - # 基本状态字段 - is_active = Column(Boolean, default=True) - is_bot = Column(Boolean, default=False) - is_deleted = Column(Boolean, default=False) - is_online = Column(Boolean, default=True) - is_supporter = Column(Boolean, default=False) - is_restricted = Column(Boolean, default=False) - session_verified = Column(Boolean, default=False) - has_supported = Column(Boolean, default=False) - pm_friends_only = Column(Boolean, default=False) - - # 基本资料字段 - default_group = Column(String(50), default='default') - last_visit = Column(DateTime) - join_date = Column(DateTime) - profile_colour = Column(String(7)) - profile_hue = Column(Integer) - - # 社交媒体和个人资料字段 - avatar_url = Column(String(500)) - cover_url = Column(String(500)) - discord = Column(String(100)) - twitter = Column(String(100)) - website = Column(String(500)) - title = Column(String(100)) - title_url = Column(String(500)) - interests = Column(Text) - location = Column(String(100)) - occupation = Column(String(100)) - - # 游戏相关字段 - playmode = Column(String(10), default='osu') - support_level = Column(Integer, default=0) - max_blocks = Column(Integer, default=100) - max_friends = Column(Integer, default=500) - post_count = Column(Integer, default=0) - - # 页面内容 - page_html = Column(Text) - page_raw = Column(Text) - - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # 关联关系 - user = relationship("User", back_populates="lazer_profile") - - -class LazerUserCountry(Base): - __tablename__ = "lazer_user_countries" - - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - code = Column(String(2), nullable=False) - name = Column(String(100), nullable=False) - - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - -class LazerUserKudosu(Base): - __tablename__ = "lazer_user_kudosu" - - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - available = Column(Integer, default=0) - total = Column(Integer, default=0) - - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - -class LazerUserCounts(Base): - __tablename__ = "lazer_user_counts" - - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - - # 统计计数字段 - beatmap_playcounts_count = Column(Integer, default=0) - comments_count = Column(Integer, default=0) - favourite_beatmapset_count = Column(Integer, default=0) - follower_count = Column(Integer, default=0) - graveyard_beatmapset_count = Column(Integer, default=0) - guest_beatmapset_count = Column(Integer, default=0) - loved_beatmapset_count = Column(Integer, default=0) - mapping_follower_count = Column(Integer, default=0) - nominated_beatmapset_count = Column(Integer, default=0) - pending_beatmapset_count = Column(Integer, default=0) - ranked_beatmapset_count = Column(Integer, default=0) - ranked_and_approved_beatmapset_count = Column(Integer, default=0) - unranked_beatmapset_count = Column(Integer, default=0) - scores_best_count = Column(Integer, default=0) - scores_first_count = Column(Integer, default=0) - scores_pinned_count = Column(Integer, default=0) - scores_recent_count = Column(Integer, default=0) - - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - -class LazerUserStatistics(Base): - __tablename__ = "lazer_user_statistics" - - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - mode = Column(String(10), nullable=False, default='osu') - - # 基本命中统计 - count_100 = Column(Integer, default=0) - count_300 = Column(Integer, default=0) - count_50 = Column(Integer, default=0) - count_miss = Column(Integer, default=0) - - # 等级信息 - level_current = Column(Integer, default=1) - level_progress = Column(Integer, default=0) - - # 排名信息 - global_rank = Column(Integer) - global_rank_exp = Column(Integer) - country_rank = Column(Integer) - - # PP 和分数 - pp = Column(DECIMAL(10, 2), default=0.00) - pp_exp = Column(DECIMAL(10, 2), default=0.00) - ranked_score = Column(Integer, default=0) - hit_accuracy = Column(DECIMAL(5, 2), default=0.00) - total_score = Column(Integer, default=0) - total_hits = Column(Integer, default=0) - maximum_combo = Column(Integer, default=0) -# ============================================ -# 旧的兼容性表模型(保留以便向后兼容) -# ============================================ - -class LegacyUserStatistics(Base): - __tablename__ = "user_statistics" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - mode = Column(String(10), nullable=False) # osu, taiko, fruits, mania - - # 基本统计 - count_100 = Column(Integer, default=0) - count_300 = Column(Integer, default=0) - count_50 = Column(Integer, default=0) - count_miss = Column(Integer, default=0) - - # 等级信息 - level_current = Column(Integer, default=1) - level_progress = Column(Integer, default=0) - - # 排名信息 - global_rank = Column(Integer) - global_rank_exp = Column(Integer) - country_rank = Column(Integer) - - # PP 和分数 - pp = Column(Float, default=0.0) - pp_exp = Column(Float, default=0.0) - ranked_score = Column(Integer, default=0) - hit_accuracy = Column(Float, default=0.0) - total_score = Column(Integer, default=0) - total_hits = Column(Integer, default=0) - maximum_combo = Column(Integer, default=0) - - # 游戏统计 - play_count = Column(Integer, default=0) - play_time = Column(Integer, default=0) - replays_watched_by_others = Column(Integer, default=0) - is_ranked = Column(Boolean, default=False) - - # 成绩等级计数 - grade_ss = Column(Integer, default=0) - grade_ssh = Column(Integer, default=0) - grade_s = Column(Integer, default=0) - grade_sh = Column(Integer, default=0) - grade_a = Column(Integer, default=0) - - -class OAuthToken(Base): - __tablename__ = "oauth_tokens" - - id = Column(Integer, primary_key=True, autoincrement=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - access_token = Column(String(255), nullable=False, index=True) - refresh_token = Column(String(255), nullable=False, index=True) - expires_at = Column(DateTime, nullable=False) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - previous_usernames = Column(JSON, default=list) - replays_watched_counts = Column(JSON, default=list) - - # 关联关系 - statistics = relationship("UserStatistics", back_populates="user", cascade="all, delete-orphan") - achievements = relationship("UserAchievement", back_populates="user", cascade="all, delete-orphan") - team_membership = relationship("TeamMember", back_populates="user", cascade="all, delete-orphan") - daily_challenge_stats = relationship("DailyChallengeStats", back_populates="user", uselist=False, cascade="all, delete-orphan") - rank_history = relationship("RankHistory", back_populates="user", cascade="all, delete-orphan") - - -class UserStatistics(Base): - __tablename__ = "user_statistics" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - mode = Column(String(10), nullable=False) # osu, taiko, fruits, mania - - # 基本统计 - count_100 = Column(Integer, default=0) - count_300 = Column(Integer, default=0) - count_50 = Column(Integer, default=0) - count_miss = Column(Integer, default=0) - - # 等级信息 - level_current = Column(Integer, default=1) - level_progress = Column(Integer, default=0) - - # 排名信息 - global_rank = Column(Integer) - global_rank_exp = Column(Integer) - country_rank = Column(Integer) - - # PP 和分数 - pp = Column(Float, default=0.0) - pp_exp = Column(Float, default=0.0) - ranked_score = Column(Integer, default=0) - hit_accuracy = Column(Float, default=0.0) - total_score = Column(Integer, default=0) - total_hits = Column(Integer, default=0) - maximum_combo = Column(Integer, default=0) - - # 游戏统计 - play_count = Column(Integer, default=0) - play_time = Column(Integer, default=0) # 秒 - replays_watched_by_others = Column(Integer, default=0) - is_ranked = Column(Boolean, default=False) - - # 成绩等级计数 - grade_ss = Column(Integer, default=0) - grade_ssh = Column(Integer, default=0) - grade_s = Column(Integer, default=0) - grade_sh = Column(Integer, default=0) - grade_a = Column(Integer, default=0) - - # 最高排名记录 - rank_highest = Column(Integer) - rank_highest_updated_at = Column(DateTime) - - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # 关联关系 - user = relationship("User", back_populates="statistics") - - -class UserAchievement(Base): - __tablename__ = "user_achievements" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - achievement_id = Column(Integer, nullable=False) - achieved_at = Column(DateTime, default=datetime.utcnow) - - user = relationship("User", back_populates="achievements") - - -class Team(Base): - __tablename__ = "teams" - - id = Column(Integer, primary_key=True, index=True) - name = Column(String(100), nullable=False) - short_name = Column(String(10), nullable=False) - flag_url = Column(String(500)) - created_at = Column(DateTime, default=datetime.utcnow) - - members = relationship("TeamMember", back_populates="team", cascade="all, delete-orphan") - - -class TeamMember(Base): - __tablename__ = "team_members" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - team_id = Column(Integer, ForeignKey("teams.id"), nullable=False) - joined_at = Column(DateTime, default=datetime.utcnow) - - user = relationship("User", back_populates="team_membership") - team = relationship("Team", back_populates="members") - - -class DailyChallengeStats(Base): - __tablename__ = "daily_challenge_stats" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True) - - daily_streak_best = Column(Integer, default=0) - daily_streak_current = Column(Integer, default=0) - last_update = Column(DateTime) - last_weekly_streak = Column(DateTime) - playcount = Column(Integer, default=0) - top_10p_placements = Column(Integer, default=0) - top_50p_placements = Column(Integer, default=0) - weekly_streak_best = Column(Integer, default=0) - weekly_streak_current = Column(Integer, default=0) - - user = relationship("User", back_populates="daily_challenge_stats") - - -class RankHistory(Base): - __tablename__ = "rank_history" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - mode = Column(String(10), nullable=False) - rank_data = Column(JSON, nullable=False) # Array of ranks - date_recorded = Column(DateTime, default=datetime.utcnow) - - user = relationship("User", back_populates="rank_history") - - -class OAuthToken(Base): - __tablename__ = "oauth_tokens" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - access_token = Column(String(500), unique=True, nullable=False) - refresh_token = Column(String(500), unique=True, nullable=False) - token_type = Column(String(20), default="Bearer") - scope = Column(String(100), default="*") - expires_at = Column(DateTime, nullable=False) - created_at = Column(DateTime, default=datetime.utcnow) - - user = relationship("User") diff --git a/app/utils.py b/app/utils.py index c762bff..f7ced49 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,7 +1,7 @@ from typing import Dict, List, Optional -from datetime import datetime -from app.models import * -from app.database import User as DBUser, LazerUserStatistics, LazerUserProfile, LazerUserCountry, LazerUserKudosu, LazerUserCounts, LazerUserAchievement +from datetime import datetime, UTC +from app.models import User, Statistics, Level, GradeCounts, Country, Cover, Kudosu, GameMode, PlayStyle, Team, UserAchievement, RankHistory, DailyChallengeStats, RankHighest, Page +from app.database import User as DBUser, LazerUserProfile, LazerUserStatistics, LazerUserCountry, LazerUserKudosu, LazerUserCounts, LazerUserAchievement from sqlalchemy.orm import Session @@ -32,9 +32,11 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio user_stats = create_default_lazer_statistics(ruleset) # 获取国家信息 + country_code = db_user.country_code if db_user.country_code else 'XX' + country = Country( - code=db_user.country_code, - name=get_country_name(db_user.country_code) + code=country_code, + name=get_country_name(country_code) ) # 获取 Kudosu 信息 @@ -186,14 +188,14 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio user_id = getattr(db_user, 'id') user_name = getattr(db_user, 'name') user_country = getattr(db_user, 'country') - + # 获取用户头像URL avatar_url = None # 首先检查 profile 中的 avatar_url if profile and hasattr(profile, 'avatar_url') and profile.avatar_url: avatar_url = str(profile.avatar_url) - + # 然后检查是否有关联的头像记录 if avatar_url is None and hasattr(db_user, 'avatar') and db_user.avatar is not None: if db_user.avatar.r2_game_url: @@ -202,13 +204,13 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio elif db_user.avatar.r2_original_url: # 其次使用原始头像URL avatar_url = str(db_user.avatar.r2_original_url) - + # 如果还是没有找到,通过查询获取 if db_session and avatar_url is None: try: # 导入UserAvatar模型 from app.database import UserAvatar - + # 尝试查找用户的头像记录 avatar_record = db_session.query(UserAvatar).filter_by(user_id=user_id, is_active=True).first() if avatar_record is not None: @@ -229,7 +231,7 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio id=user_id, username=user_name, avatar_url=avatar_url, # 使用我们上面获取的头像URL - country_code=user_country, + country_code=country_code, default_group=profile.default_group if profile else "default", is_active=profile.is_active if profile else True, is_bot=profile.is_bot if profile else False, @@ -237,27 +239,19 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio is_online=profile.is_online if profile else True, is_supporter=profile.is_supporter if profile else False, is_restricted=profile.is_restricted if profile else False, - last_visit=db_user.last_visit, + last_visit=db_user.last_visit if db_user.last_visit else None, pm_friends_only=profile.pm_friends_only if profile else False, profile_colour=profile.profile_colour if profile else None, - cover_url=cover_url, + cover_url=profile.cover_url if profile and profile.cover_url else "https://assets.ppy.sh/user-profile-covers/default.jpeg", discord=profile.discord if profile else None, has_supported=profile.has_supported if profile else False, interests=profile.interests if profile else None, - join_date=db_user.join_date, + join_date=db_user.join_date if db_user.join_date else None, location=profile.location if profile else None, - max_blocks=profile.max_blocks if profile else 100, - max_friends=profile.max_friends if profile else 500, - - occupation=None, # 职业字段,默认为 None #待修改 - - #playmode=GameMode(db_user.playmode), - playmode=GameMode("osu"), #待修改 - - playstyle=[PlayStyle.MOUSE, PlayStyle.KEYBOARD, PlayStyle.TABLET], #待修改 - - post_count=0, - profile_hue=None, + max_blocks=profile.max_blocks if profile and profile.max_blocks else 100, + max_friends=profile.max_friends if profile and profile.max_friends else 500, + post_count=profile.post_count if profile and profile.post_count else 0, + profile_hue=profile.profile_hue if profile and profile.profile_hue else None, profile_order= ['me', 'recent_activity', 'top_ranks', 'medals', 'historical', 'beatmaps', 'kudosu'], title=None, title_url=None, @@ -295,12 +289,11 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio daily_challenge_user_stats=None, groups=[], monthly_playcounts=[], - #page=Page(html=db_user.page_html, raw=db_user.page_raw), - page=Page(), # Provide a default Page object + page=Page(html=profile.page_html, raw=profile.page_raw) if profile.page_html or profile.page_raw else Page(), previous_usernames=[], rank_highest=rank_highest, rank_history=rank_history, - rankHistory=rank_history, # 兼容性别名 + rankHistory=rank_history, # 保留旧API兼容性字段 replays_watched_counts=[], team=team, user_achievements=user_achievements diff --git a/main.py b/main.py index 12780fb..f293cf2 100644 --- a/main.py +++ b/main.py @@ -20,7 +20,6 @@ app = FastAPI(title="osu! API 模拟服务器", version="1.0.0") security = HTTPBearer() - @app.post("/oauth/token", response_model=TokenResponse) async def oauth_token( grant_type: str = Form(...), @@ -104,7 +103,7 @@ async def oauth_token( refresh_token=new_refresh_token, scope=scope ) - + else: raise HTTPException(status_code=400, detail="Unsupported grant type")