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 457e349..0000000 --- a/app/database copy.py +++ /dev/null @@ -1,455 +0,0 @@ -from __future__ import annotations - -from datetime import datetime - -from sqlalchemy import ( - DECIMAL, - JSON, - Boolean, - Column, - DateTime, - Float, - ForeignKey, - Integer, - String, - Text, -) -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship - -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 449ca98..31d3d6a 100644 --- a/app/database.py +++ b/app/database.py @@ -8,6 +8,7 @@ from sqlalchemy import ( JSON, Boolean, Column, + Date, DateTime, Float, ForeignKey, @@ -15,6 +16,7 @@ from sqlalchemy import ( String, Text, ) +from sqlalchemy.dialects.mysql import TINYINT, VARCHAR from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship @@ -91,11 +93,19 @@ class User(Base): 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" + "LazerUserAchievement", + back_populates="user", + cascade="all, delete-orphan", + overlaps="lazer_achievements", ) team_membership = relationship( "TeamMember", back_populates="user", cascade="all, delete-orphan" @@ -115,6 +125,28 @@ class User(Base): primaryjoin="and_(User.id==UserAvatar.user_id, UserAvatar.is_active==True)", uselist=False, ) + active_banners = relationship( + "LazerUserBanners", # 原定义指向LazerUserBanners,实际应为UserAvatar + back_populates="user", + primaryjoin=( + "and_(User.id==LazerUserBanners.user_id, LazerUserBanners.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" + ) # ============================================ @@ -176,6 +208,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" @@ -280,6 +326,22 @@ 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" @@ -294,6 +356,65 @@ 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 ec7e6aa..75021a8 100644 --- a/app/utils.py +++ b/app/utils.py @@ -7,12 +7,10 @@ from app.models import ( Country, Cover, DailyChallengeStats, - GameMode, GradeCounts, Kudosu, Level, Page, - PlayStyle, RankHighest, RankHistory, Statistics, @@ -41,6 +39,13 @@ def convert_db_user_to_api_user( # 如果没有 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: @@ -53,9 +58,9 @@ def convert_db_user_to_api_user( user_stats = create_default_lazer_statistics(ruleset) # 获取国家信息 - country = Country( - code=user_country_code, name=get_country_name(user_country_code) - ) + country_code = db_user.country_code if db_user.country_code else "XX" + + country = Country(code=country_code, name=get_country_name(country_code)) # 获取 Kudosu 信息 kudosu = Kudosu(available=0, total=0) @@ -249,11 +254,99 @@ def convert_db_user_to_api_user( if avatar_url is None: avatar_url = "https://a.gu-osu.gmoe.cc/api/users/avatar/1" + # 处理 profile_order 列表排序 + profile_order = [ + "me", + "recent_activity", + "top_ranks", + "medals", + "historical", + "beatmaps", + "kudosu", + ] + if profile and profile.profile_order: + profile_order = profile.profile_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, @@ -261,75 +354,68 @@ def convert_db_user_to_api_user( 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, 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, ) @@ -358,6 +444,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 @@ -371,7 +458,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 @@ -389,8 +476,23 @@ def create_default_profile(db_user: DBUser): self.max_blocks = 100 self.max_friends = 500 self.post_count = 0 - self.page_html = None - self.page_raw = None + # 添加profile_order字段 + self.profile_order = [ + "me", + "recent_activity", + "top_ranks", + "medals", + "historical", + "beatmaps", + "kudosu", + ] + self.page_html = "" + self.page_raw = "" + # 在MockProfile类中添加active_tournament_banners字段 + self.active_tournament_banners = ( + MockLazerTournamentBanner.create_default_banners() + ) + self.active_tournament_banners = [] # 默认空列表 return MockProfile() @@ -413,11 +515,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 @@ -459,7 +561,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 @@ -478,3 +583,18 @@ def create_default_counts(): self.scores_recent_count = 0 return MockCounts() + + +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 ce8695b..d2d7ba8 100644 --- a/create_sample_data.py +++ b/create_sample_data.py @@ -12,12 +12,12 @@ from app.auth import get_password_hash from app.database import ( Base, DailyChallengeStats, - LazerUserStatistics as UserStatistics, + LazerUserStatistics, RankHistory, User, UserAchievement, ) -from app.dependencies import engine, get_db +from app.dependencies.database import engine, get_db # 创建所有表 Base.metadata.create_all(bind=engine) @@ -58,92 +58,92 @@ def create_sample_user(): custom_badge_icon=None, userpage_content="「世界に忘れられた」", api_key=None, - # # 兼容性字段 - # avatar_url="https://a.ppy.sh/15651670?1732362658.jpeg", - # cover_url="https://assets.ppy.sh/user-profile-covers/15651670/0fc7b77adef39765a570e7f535bc383e5a848850d41a8943f8857984330b8bc6.jpeg", - # has_supported=True, - # interests="「世界に忘れられた」", - # location="咕谷国", - # website="https://gmoe.cc", - # playstyle=["mouse", "keyboard", "tablet"], - # profile_order=[ - # "me", - # "recent_activity", - # "top_ranks", - # "medals", - # "historical", - # "beatmaps", - # "kudosu", - # ], - # beatmap_playcounts_count=3306, - # favourite_beatmapset_count=15, - # follower_count=98, - # graveyard_beatmapset_count=7, - # mapping_follower_count=1, - # previous_usernames=["hehejun"], - # monthly_playcounts=[ - # {"start_date": "2019-11-01", "count": 43}, - # {"start_date": "2020-04-01", "count": 216}, - # {"start_date": "2020-05-01", "count": 656}, - # {"start_date": "2020-07-01", "count": 158}, - # {"start_date": "2020-08-01", "count": 174}, - # {"start_date": "2020-10-01", "count": 13}, - # {"start_date": "2020-11-01", "count": 52}, - # {"start_date": "2020-12-01", "count": 140}, - # {"start_date": "2021-01-01", "count": 359}, - # {"start_date": "2021-02-01", "count": 452}, - # {"start_date": "2021-03-01", "count": 77}, - # {"start_date": "2021-04-01", "count": 114}, - # {"start_date": "2021-05-01", "count": 270}, - # {"start_date": "2021-06-01", "count": 148}, - # {"start_date": "2021-07-01", "count": 246}, - # {"start_date": "2021-08-01", "count": 56}, - # {"start_date": "2021-09-01", "count": 136}, - # {"start_date": "2021-10-01", "count": 45}, - # {"start_date": "2021-11-01", "count": 98}, - # {"start_date": "2021-12-01", "count": 54}, - # {"start_date": "2022-01-01", "count": 88}, - # {"start_date": "2022-02-01", "count": 45}, - # {"start_date": "2022-03-01", "count": 6}, - # {"start_date": "2022-04-01", "count": 54}, - # {"start_date": "2022-05-01", "count": 105}, - # {"start_date": "2022-06-01", "count": 37}, - # {"start_date": "2022-07-01", "count": 88}, - # {"start_date": "2022-08-01", "count": 7}, - # {"start_date": "2022-09-01", "count": 9}, - # {"start_date": "2022-10-01", "count": 6}, - # {"start_date": "2022-11-01", "count": 2}, - # {"start_date": "2022-12-01", "count": 16}, - # {"start_date": "2023-01-01", "count": 7}, - # {"start_date": "2023-04-01", "count": 16}, - # {"start_date": "2023-05-01", "count": 3}, - # {"start_date": "2023-06-01", "count": 8}, - # {"start_date": "2023-07-01", "count": 23}, - # {"start_date": "2023-08-01", "count": 3}, - # {"start_date": "2023-09-01", "count": 1}, - # {"start_date": "2023-10-01", "count": 25}, - # {"start_date": "2023-11-01", "count": 160}, - # {"start_date": "2023-12-01", "count": 306}, - # {"start_date": "2024-01-01", "count": 735}, - # {"start_date": "2024-02-01", "count": 420}, - # {"start_date": "2024-03-01", "count": 549}, - # {"start_date": "2024-04-01", "count": 466}, - # {"start_date": "2024-05-01", "count": 333}, - # {"start_date": "2024-06-01", "count": 1126}, - # {"start_date": "2024-07-01", "count": 534}, - # {"start_date": "2024-08-01", "count": 280}, - # {"start_date": "2024-09-01", "count": 116}, - # {"start_date": "2024-10-01", "count": 120}, - # {"start_date": "2024-11-01", "count": 332}, - # {"start_date": "2024-12-01", "count": 243}, - # {"start_date": "2025-01-01", "count": 122}, - # {"start_date": "2025-02-01", "count": 379}, - # {"start_date": "2025-03-01", "count": 278}, - # {"start_date": "2025-04-01", "count": 296}, - # {"start_date": "2025-05-01", "count": 964}, - # {"start_date": "2025-06-01", "count": 821}, - # {"start_date": "2025-07-01", "count": 230}, - # ], + # # 兼容性字段 + # avatar_url="https://a.ppy.sh/15651670?1732362658.jpeg", + # cover_url="https://assets.ppy.sh/user-profile-covers/15651670/0fc7b77adef39765a570e7f535bc383e5a848850d41a8943f8857984330b8bc6.jpeg", + # has_supported=True, + # interests="「世界に忘れられた」", + # location="咕谷国", + # website="https://gmoe.cc", + # playstyle=["mouse", "keyboard", "tablet"], + # profile_order=[ + # "me", + # "recent_activity", + # "top_ranks", + # "medals", + # "historical", + # "beatmaps", + # "kudosu", + # ], + # beatmap_playcounts_count=3306, + # favourite_beatmapset_count=15, + # follower_count=98, + # graveyard_beatmapset_count=7, + # mapping_follower_count=1, + # previous_usernames=["hehejun"], + # monthly_playcounts=[ + # {"start_date": "2019-11-01", "count": 43}, + # {"start_date": "2020-04-01", "count": 216}, + # {"start_date": "2020-05-01", "count": 656}, + # {"start_date": "2020-07-01", "count": 158}, + # {"start_date": "2020-08-01", "count": 174}, + # {"start_date": "2020-10-01", "count": 13}, + # {"start_date": "2020-11-01", "count": 52}, + # {"start_date": "2020-12-01", "count": 140}, + # {"start_date": "2021-01-01", "count": 359}, + # {"start_date": "2021-02-01", "count": 452}, + # {"start_date": "2021-03-01", "count": 77}, + # {"start_date": "2021-04-01", "count": 114}, + # {"start_date": "2021-05-01", "count": 270}, + # {"start_date": "2021-06-01", "count": 148}, + # {"start_date": "2021-07-01", "count": 246}, + # {"start_date": "2021-08-01", "count": 56}, + # {"start_date": "2021-09-01", "count": 136}, + # {"start_date": "2021-10-01", "count": 45}, + # {"start_date": "2021-11-01", "count": 98}, + # {"start_date": "2021-12-01", "count": 54}, + # {"start_date": "2022-01-01", "count": 88}, + # {"start_date": "2022-02-01", "count": 45}, + # {"start_date": "2022-03-01", "count": 6}, + # {"start_date": "2022-04-01", "count": 54}, + # {"start_date": "2022-05-01", "count": 105}, + # {"start_date": "2022-06-01", "count": 37}, + # {"start_date": "2022-07-01", "count": 88}, + # {"start_date": "2022-08-01", "count": 7}, + # {"start_date": "2022-09-01", "count": 9}, + # {"start_date": "2022-10-01", "count": 6}, + # {"start_date": "2022-11-01", "count": 2}, + # {"start_date": "2022-12-01", "count": 16}, + # {"start_date": "2023-01-01", "count": 7}, + # {"start_date": "2023-04-01", "count": 16}, + # {"start_date": "2023-05-01", "count": 3}, + # {"start_date": "2023-06-01", "count": 8}, + # {"start_date": "2023-07-01", "count": 23}, + # {"start_date": "2023-08-01", "count": 3}, + # {"start_date": "2023-09-01", "count": 1}, + # {"start_date": "2023-10-01", "count": 25}, + # {"start_date": "2023-11-01", "count": 160}, + # {"start_date": "2023-12-01", "count": 306}, + # {"start_date": "2024-01-01", "count": 735}, + # {"start_date": "2024-02-01", "count": 420}, + # {"start_date": "2024-03-01", "count": 549}, + # {"start_date": "2024-04-01", "count": 466}, + # {"start_date": "2024-05-01", "count": 333}, + # {"start_date": "2024-06-01", "count": 1126}, + # {"start_date": "2024-07-01", "count": 534}, + # {"start_date": "2024-08-01", "count": 280}, + # {"start_date": "2024-09-01", "count": 116}, + # {"start_date": "2024-10-01", "count": 120}, + # {"start_date": "2024-11-01", "count": 332}, + # {"start_date": "2024-12-01", "count": 243}, + # {"start_date": "2025-01-01", "count": 122}, + # {"start_date": "2025-02-01", "count": 379}, + # {"start_date": "2025-03-01", "count": 278}, + # {"start_date": "2025-04-01", "count": 296}, + # {"start_date": "2025-05-01", "count": 964}, + # {"start_date": "2025-06-01", "count": 821}, + # {"start_date": "2025-07-01", "count": 230}, + # ], ) db.add(user) @@ -151,7 +151,7 @@ def create_sample_user(): db.refresh(user) # 创建 osu! 模式统计 - osu_stats = UserStatistics( + osu_stats = LazerUserStatistics( user_id=user.id, mode="osu", count_100=276274, @@ -182,7 +182,7 @@ def create_sample_user(): ) # 创建 taiko 模式统计 - taiko_stats = UserStatistics( + taiko_stats = LazerUserStatistics( user_id=user.id, mode="taiko", count_100=160, @@ -205,7 +205,7 @@ def create_sample_user(): ) # 创建 fruits 模式统计 - fruits_stats = UserStatistics( + fruits_stats = LazerUserStatistics( user_id=user.id, mode="fruits", count_100=109, @@ -229,7 +229,7 @@ def create_sample_user(): ) # 创建 mania 模式统计 - mania_stats = UserStatistics( + mania_stats = LazerUserStatistics( user_id=user.id, mode="mania", count_100=7867,