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/config.py b/app/config.py index c1310e8..a4a4dfd 100644 --- a/app/config.py +++ b/app/config.py @@ -5,7 +5,7 @@ load_dotenv() class Settings: # 数据库设置 - DATABASE_URL: str = os.getenv("DATABASE_URL", "mysql+pymysql://root:password@localhost:3306/osu_api") + DATABASE_URL: str = os.getenv("DATABASE_URL", "mysql+pymysql://root:Chinabug610@localhost:3306/osu_api") REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0") # JWT 设置 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/database.py b/app/database.py index ed2ba61..96a48de 100644 --- a/app/database.py +++ b/app/database.py @@ -1,4 +1,5 @@ from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float, Text, JSON, ForeignKey, Date, DECIMAL +from sqlalchemy.dialects.mysql import VARCHAR, TINYINT from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from datetime import datetime @@ -61,13 +62,32 @@ class User(Base): 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_profile_sections = relationship( + "LazerUserProfileSections", # 修正类名拼写(添加s) + back_populates="user", + cascade="all, delete-orphan" + ) statistics = relationship("LegacyUserStatistics", back_populates="user", cascade="all, delete-orphan") - achievements = relationship("LazerUserAchievement", back_populates="user", cascade="all, delete-orphan") + achievements = relationship( + "LazerUserAchievement", + back_populates="user", + cascade="all, delete-orphan", + overlaps="lazer_achievements" + ) 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") avatar = relationship("UserAvatar", back_populates="user", primaryjoin="and_(User.id==UserAvatar.user_id, UserAvatar.is_active==True)", uselist=False) - + active_banners = relationship( + "UserAvatar", # 原定义指向LazerUserBanners,实际应为UserAvatar + back_populates="user", + primaryjoin="and_(User.id==UserAvatar.user_id, UserAvatar.is_active==True)", + uselist=False + ) + lazer_badges = relationship("LazerUserBadge", back_populates="user", cascade="all, delete-orphan") + lazer_monthly_playcounts = relationship("LazerUserMonthlyPlaycounts", back_populates="user", cascade="all, delete-orphan") + lazer_previous_usernames = relationship("LazerUserPreviousUsername", back_populates="user", cascade="all, delete-orphan") + lazer_replays_watched = relationship("LazerUserReplaysWatched", back_populates="user", cascade="all, delete-orphan") # ============================================ # Lazer API 专用表模型 @@ -127,6 +147,20 @@ class LazerUserProfile(Base): user = relationship("User", back_populates="lazer_profile") +class LazerUserProfileSections(Base): + __tablename__ = "lazer_user_profile_sections" + + id=Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id"),nullable=False) + section_name = Column(VARCHAR(50),nullable=False) + display_order=Column(Integer) + + created_at=Column(DateTime, default=datetime.utcnow) + updated_at=Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User", back_populates="lazer_profile_sections") + + class LazerUserCountry(Base): __tablename__ = "lazer_user_countries" @@ -154,7 +188,7 @@ class LazerUserCounts(Base): user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - # 统计计数字段 + # 统计计数字段f beatmap_playcounts_count = Column(Integer, default=0) comments_count = Column(Integer, default=0) favourite_beatmapset_count = Column(Integer, default=0) @@ -230,6 +264,21 @@ class LazerUserStatistics(Base): # 关联关系 user = relationship("User", back_populates="lazer_statistics") +class LazerUserBanners(Base): + __tablename__ = "lazer_user_tournament_banners" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + tournament_id = Column(Integer, nullable=False) + image_url = Column(VARCHAR(500), nullable=False) + is_active = Column(TINYINT(1)) + + # 修正user关系的back_populates值 + user = relationship( + "User", + back_populates="active_banners" # 改为实际存在的属性名 + ) + class LazerUserAchievement(Base): __tablename__ = "lazer_user_achievements" @@ -244,6 +293,64 @@ class LazerUserAchievement(Base): user = relationship("User", back_populates="lazer_achievements") +class LazerUserBadge(Base): + __tablename__ = "lazer_user_badges" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + badge_id = Column(Integer, nullable=False) + awarded_at = Column(DateTime) + description = Column(Text) + image_url = Column(String(500)) + url = Column(String(500)) + + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User", back_populates="lazer_badges") + + +class LazerUserMonthlyPlaycounts(Base): + __tablename__ = "lazer_user_monthly_playcounts" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + start_date = Column(Date, nullable=False) + play_count = Column(Integer, default=0) + + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User", back_populates="lazer_monthly_playcounts") + + +class LazerUserPreviousUsername(Base): + __tablename__ = "lazer_user_previous_usernames" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + username = Column(String(32), nullable=False) + changed_at = Column(DateTime, nullable=False) + + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User", back_populates="lazer_previous_usernames") + + +class LazerUserReplaysWatched(Base): + __tablename__ = "lazer_user_replays_watched" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + start_date = Column(Date, nullable=False) + count = Column(Integer, default=0) + + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User", back_populates="lazer_replays_watched") + # ============================================ # 旧的兼容性表模型(保留以便向后兼容) # ============================================ diff --git a/app/utils.py b/app/utils.py index c762bff..9ee96ae 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,7 +1,6 @@ -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 @@ -19,7 +18,14 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio if not profile: # 如果没有 lazer 资料,使用默认值 profile = create_default_profile(db_user) - + + # 获取 Lazer 用户计数 + + lzrcnt=db_user.lazer_statistics + if not lzrcnt: + # 如果没有 lazer 计数,使用默认值 + lzrcnt = create_default_counts() + # 获取指定模式的统计信息 user_stats = None for stat in db_user.lazer_statistics: @@ -32,9 +38,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 +194,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 +210,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: @@ -225,11 +233,69 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio if avatar_url is None: avatar_url = f"https://a.gu-osu.gmoe.cc/api/users/avatar/1" + # 处理 profile_order 列表排序 + profile_order = [] + if profile and profile.profile_order: + profile_order = sorted( + profile.profile_order, + key=lambda x: (x.display_order == 0, x.display_order) + ) + + # 在convert_db_user_to_api_user函数中添加active_tournament_banners处理 + active_tournament_banners = [] + if hasattr(db_user, 'lazer_tournament_banners') and db_user.lazer_tournament_banners: + for banner in db_user.lazer_tournament_banners: + active_tournament_banners.append({ + "tournament_id": banner.tournament_id, + "image_url": banner.image_url, + "is_active": banner.is_active + }) + + # 在convert_db_user_to_api_user函数中添加badges处理 + badges = [] + if hasattr(db_user, 'lazer_badges') and db_user.lazer_badges: + for badge in db_user.lazer_badges: + badges.append({ + "badge_id": badge.badge_id, + "awarded_at": badge.awarded_at, + "description": badge.description, + "image_url": badge.image_url, + "url": badge.url + }) + + # 在convert_db_user_to_api_user函数中添加monthly_playcounts处理 + monthly_playcounts = [] + if hasattr(db_user, 'lazer_monthly_playcounts') and db_user.lazer_monthly_playcounts: + for playcount in db_user.lazer_monthly_playcounts: + monthly_playcounts.append({ + "start_date": playcount.start_date.isoformat() if playcount.start_date else None, + "play_count": playcount.play_count + }) + + # 在convert_db_user_to_api_user函数中添加previous_usernames处理 + previous_usernames = [] + if hasattr(db_user, 'lazer_previous_usernames') and db_user.lazer_previous_usernames: + for username in db_user.lazer_previous_usernames: + previous_usernames.append({ + "username": username.username, + "changed_at": username.changed_at.isoformat() if username.changed_at else None + }) + + # 在convert_db_user_to_api_user函数中添加replays_watched_counts处理 + replays_watched_counts = [] + if hasattr(db_user, 'lazer_replays_watched') and db_user.lazer_replays_watched: + for replay in db_user.lazer_replays_watched: + replays_watched_counts.append({ + "start_date": replay.start_date.isoformat() if replay.start_date else None, + "count": replay.count + }) + + # 创建用户对象 user = User( id=user_id, username=user_name, - avatar_url=avatar_url, # 使用我们上面获取的头像URL - country_code=user_country, + avatar_url=avatar_url, + 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,71 +303,62 @@ 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=profile.join_date if profile 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, - profile_order= ['me', 'recent_activity', 'top_ranks', 'medals', 'historical', 'beatmaps', 'kudosu'], - title=None, - title_url=None, - twitter=None, - website='https://gmoe.cc', + 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=profile_order, # 使用排序后的 profile_order + title=profile.title if profile else None, + title_url=profile.title_url if profile else None, + twitter=profile.twitter if profile else None, + website=profile.website if profile else None, session_verified=True, - support_level=0, + support_level=profile.support_level if profile else 0, country=country, cover=cover, kudosu=kudosu, statistics=statistics, statistics_rulesets=statistics_rulesets, - beatmap_playcounts_count=3306, - comments_count=0, - favourite_beatmapset_count=0, - follower_count=0, - graveyard_beatmapset_count=0, - guest_beatmapset_count=0, - loved_beatmapset_count=0, - mapping_follower_count=0, - nominated_beatmapset_count=0, - pending_beatmapset_count=0, - ranked_beatmapset_count=0, - ranked_and_approved_beatmapset_count=0, - unranked_beatmapset_count=0, - scores_best_count=0, - scores_first_count=0, - scores_pinned_count=0, - scores_recent_count=0, - account_history=[], - active_tournament_banner=None, - active_tournament_banners=[], - badges=[], + beatmap_playcounts_count=lzrcnt.beatmap_playcounts_count if lzrcnt else 0, + comments_count=lzrcnt.comments_count if lzrcnt else 0, + favourite_beatmapset_count=lzrcnt.favourite_beatmapset_count if lzrcnt else 0, + follower_count=lzrcnt.follower_count if lzrcnt else 0, + graveyard_beatmapset_count=lzrcnt.graveyard_beatmapset_count if lzrcnt else 0, + guest_beatmapset_count=lzrcnt.guest_beatmapset_count if lzrcnt else 0, + loved_beatmapset_count=lzrcnt.loved_beatmapset_count if lzrcnt else 0, + mapping_follower_count=lzrcnt.mapping_follower_count if lzrcnt else 0, + nominated_beatmapset_count=lzrcnt.nominated_beatmapset_count if lzrcnt else 0, + pending_beatmapset_count=lzrcnt.pending_beatmapset_count if lzrcnt else 0, + ranked_beatmapset_count=lzrcnt.ranked_beatmapset_count if lzrcnt else 0, + ranked_and_approved_beatmapset_count=lzrcnt.ranked_and_approved_beatmapset_count if lzrcnt else 0, + unranked_beatmapset_count=lzrcnt.unranked_beatmapset_count if lzrcnt else 0, + scores_best_count=lzrcnt.scores_best_count if lzrcnt else 0, + scores_first_count=lzrcnt.scores_first_count if lzrcnt else 0, + scores_pinned_count=lzrcnt.scores_pinned_count, + scores_recent_count=lzrcnt.scores_recent_count if lzrcnt else 0, + account_history=[], #TODO: 获取用户历史账户信息 + active_tournament_banner=len(active_tournament_banners), + active_tournament_banners=active_tournament_banners, + badges=badges, current_season_stats=None, 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 - previous_usernames=[], + monthly_playcounts=monthly_playcounts, + page=Page(html=profile.page_html, raw=profile.page_raw) if profile.page_html or profile.page_raw else Page(), + previous_usernames=previous_usernames, rank_highest=rank_highest, rank_history=rank_history, - rankHistory=rank_history, # 兼容性别名 - replays_watched_counts=[], + rankHistory=rank_history, + replays_watched_counts=replays_watched_counts, team=team, user_achievements=user_achievements ) @@ -329,6 +386,7 @@ def get_country_name(country_code: str) -> str: def create_default_profile(db_user: DBUser): """创建默认的用户资料""" + # 完善 MockProfile 类定义 class MockProfile: def __init__(self): self.is_active = True @@ -342,7 +400,7 @@ def create_default_profile(db_user: DBUser): self.pm_friends_only = False self.default_group = 'default' self.last_visit = None - self.join_date = db_user.join_date + self.join_date = db_user.join_date if db_user else datetime.utcnow() self.profile_colour = None self.profile_hue = None self.avatar_url = None @@ -360,9 +418,15 @@ def create_default_profile(db_user: DBUser): self.max_blocks = 100 self.max_friends = 500 self.post_count = 0 + # 添加profile_order字段 + self.profile_order = MockLazerUserProfileSections.get_sorted_sections() self.page_html = None self.page_raw = None - + # 在MockProfile类中添加active_tournament_banners字段 + self.active_tournament_banners = MockLazerTournamentBanner.create_default_banners() + self.active_tournament_banners = [] # 默认空列表 + + return MockProfile() @@ -383,11 +447,11 @@ def create_default_lazer_statistics(mode: str): self.pp_exp = 0.0 self.ranked_score = 0 self.hit_accuracy = 0.0 - self.play_count = 0 - self.play_time = 0 self.total_score = 0 self.total_hits = 0 self.maximum_combo = 0 + self.play_count = 0 + self.play_time = 0 self.replays_watched_by_others = 0 self.is_ranked = False self.grade_ss = 0 @@ -426,7 +490,10 @@ def create_default_counts(): """创建默认的计数信息""" class MockCounts: def __init__(self): + self.recent_scores_count = None self.beatmap_playcounts_count = 0 + self.scores_first_count = 0 + self.scores_pinned_count = 0 self.comments_count = 0 self.favourite_beatmapset_count = 0 self.follower_count = 0 @@ -443,5 +510,52 @@ def create_default_counts(): self.scores_first_count = 0 self.scores_pinned_count = 0 self.scores_recent_count = 0 - return MockCounts() + + +class MockLazerUserProfileSections: + def __init__(self, section_name: str, display_order: int = 0): + self.section_name = section_name + self.display_order = display_order + + @staticmethod + def create_default_sections(): + """创建默认的用户资料板块配置""" + return [ + MockLazerUserProfileSections("me", 1), + MockLazerUserProfileSections("recent_activity", 2), + MockLazerUserProfileSections("top_ranks", 3), + MockLazerUserProfileSections("medals", 4), + MockLazerUserProfileSections("historical", 5), + MockLazerUserProfileSections("beatmaps", 6), + MockLazerUserProfileSections("kudosu", 7) + ] + + @staticmethod + def get_sorted_sections(sections=None): + """ + 对profile_order列表进行排序 + display_order = 0 的记录排在最后 + """ + if sections is None: + sections = MockLazerUserProfileSections.create_default_sections() + + return sorted( + sections, + key=lambda x: (x.display_order == 0, x.display_order) + ) + + +class MockLazerTournamentBanner: + def __init__(self, tournament_id: int, image_url: str, is_active: bool = True): + self.tournament_id = tournament_id + self.image_url = image_url + self.is_active = is_active + + @staticmethod + def create_default_banners(): + """创建默认的锦标赛横幅配置""" + return [ + MockLazerTournamentBanner(1, "https://example.com/banner1.jpg", True), + MockLazerTournamentBanner(2, "https://example.com/banner2.jpg", False) + ] diff --git a/create_sample_data.py b/create_sample_data.py index e6bf457..2ad12c8 100644 --- a/create_sample_data.py +++ b/create_sample_data.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta import time from sqlalchemy.orm import Session from app.dependencies import get_db, engine -from app.database import Base, User, UserStatistics, UserAchievement, DailyChallengeStats, RankHistory +from app.database import Base, User, LazerUserStatistics, UserAchievement, DailyChallengeStats, RankHistory from app.auth import get_password_hash # 创建所有表 @@ -134,7 +134,7 @@ def create_sample_user(): db.refresh(user) # 创建 osu! 模式统计 - osu_stats = UserStatistics( + osu_stats = LazerUserStatistics( user_id=user.id, mode="osu", count_100=276274, @@ -165,7 +165,7 @@ def create_sample_user(): ) # 创建 taiko 模式统计 - taiko_stats = UserStatistics( + taiko_stats = LazerUserStatistics( user_id=user.id, mode="taiko", count_100=160, @@ -188,7 +188,7 @@ def create_sample_user(): ) # 创建 fruits 模式统计 - fruits_stats = UserStatistics( + fruits_stats = LazerUserStatistics( user_id=user.id, mode="fruits", count_100=109, @@ -212,7 +212,7 @@ def create_sample_user(): ) # 创建 mania 模式统计 - mania_stats = UserStatistics( + mania_stats = LazerUserStatistics( user_id=user.id, mode="mania", count_100=7867, 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")