feat(database): 新增用户资料相关数据模型和处理逻辑

- 新增 LazerUserProfileSections、LazerUserBanners、LazerUserBadge、LazerUserMonthlyPlaycounts、LazerUserPreviousUsername 和 LazerUserReplaysWatched 数据模型- 更新 User 数据模型,添加与新模型的关联关系
- 修改 convert_db_user_to_api_user 函数,支持新数据模型的处理- 完善 MockProfile 类,添加新模型的默认值和辅助方法
This commit is contained in:
jimmy-sketch
2025-07-19 20:43:36 +08:00
parent 757166b665
commit 91f15d572d
2 changed files with 213 additions and 27 deletions

View File

@@ -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")
# ============================================
# 旧的兼容性表模型(保留以便向后兼容)
# ============================================

View File

@@ -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)
]