From 91f15d572d1f967d966300286d69e604d49e216d Mon Sep 17 00:00:00 2001 From: jimmy-sketch Date: Sat, 19 Jul 2025 20:43:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(database):=20=E6=96=B0=E5=A2=9E=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=B5=84=E6=96=99=E7=9B=B8=E5=85=B3=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=92=8C=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 LazerUserProfileSections、LazerUserBanners、LazerUserBadge、LazerUserMonthlyPlaycounts、LazerUserPreviousUsername 和 LazerUserReplaysWatched 数据模型- 更新 User 数据模型,添加与新模型的关联关系 - 修改 convert_db_user_to_api_user 函数,支持新数据模型的处理- 完善 MockProfile 类,添加新模型的默认值和辅助方法 --- app/database.py | 86 +++++++++++++++++++++++++-- app/utils.py | 154 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 213 insertions(+), 27 deletions(-) diff --git a/app/database.py b/app/database.py index 8a12a94..b6f6a86 100644 --- a/app/database.py +++ b/app/database.py @@ -1,5 +1,5 @@ from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float, Text, JSON, ForeignKey, Date, DECIMAL -from sqlalchemy.dialects.mysql import VARCHAR +from sqlalchemy.dialects.mysql import VARCHAR, TINYINT from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from datetime import datetime @@ -62,14 +62,18 @@ 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_order=relationship("LazerUserProfileOrder", back_populates="user", cascade="all, delete-orphan") + lazer_profile_sections=relationship("LazerUserProfileSection", 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") 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("LazerUserBanners",back_populates="user",cascade="all, delete-orphan") + 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 专用表模型 @@ -131,8 +135,9 @@ class LazerUserProfile(Base): class LazerUserProfileSections(Base): __tablename__ = "lazer_user_profile_sections" - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - section_name = Column(VARCHAR(50)) + 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) @@ -167,7 +172,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) @@ -243,6 +248,17 @@ 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=relationship("User", back_populates="lazer_active_banners") + class LazerUserAchievement(Base): __tablename__ = "lazer_user_achievements" @@ -257,6 +273,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 60080d5..9ee96ae 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,4 +1,3 @@ -from typing import Dict, List, Optional 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 @@ -234,10 +233,68 @@ 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 + 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, @@ -253,13 +310,13 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio 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 if db_user.join_date else None, + join_date=profile.join_date if profile else None, location=profile.location if profile else 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=profile.profile_order if profile and profile.profile_order else ['me', 'recent_activity', 'top_ranks', 'medals', 'historical', 'beatmaps', 'kudosu'], + 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, @@ -271,7 +328,7 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio kudosu=kudosu, statistics=statistics, statistics_rulesets=statistics_rulesets, - beatmap_playcounts_count=db_user.beatmap_playcounts_count if db_user.beatmap_playcounts_count is not None else 0, + 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, @@ -285,23 +342,23 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio 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.socres_first_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.recent_scores_count if lzrcnt else 0, - account_history=[], - active_tournament_banner=0, - active_tournament_banners=[], - badges=[], + 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=[], + 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=previous_usernames, rank_highest=rank_highest, rank_history=rank_history, - rankHistory=rank_history, # 保留旧API兼容性字段 - 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 @@ -427,8 +491,9 @@ def create_default_counts(): class MockCounts: def __init__(self): self.recent_scores_count = None - self.socres_first_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 @@ -445,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) + ]