Merge pull request #1 from GooGuTeam/add-lazer-information

feat: 完善获取用户信息接口返回的数据
This commit is contained in:
陈晋瑭
2025-07-23 18:29:12 +08:00
committed by GitHub
13 changed files with 361 additions and 480 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

View File

@@ -0,0 +1,17 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ourVersions">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="3.7" />
<item index="1" class="java.lang.String" itemvalue="3.11" />
<item index="2" class="java.lang.String" itemvalue="3.12" />
<item index="3" class="java.lang.String" itemvalue="3.13" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

10
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="osu_lazer_api" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="osu_lazer_api" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="3" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/osu_lazer_api.iml" filepath="$PROJECT_DIR$/.idea/osu_lazer_api.iml" />
</modules>
</component>
</project>

14
.idea/osu_lazer_api.iml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="osu_lazer_api" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -5,7 +5,7 @@ load_dotenv()
class Settings: 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") REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0")
# JWT 设置 # JWT 设置

View File

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

View File

@@ -1,4 +1,5 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float, Text, JSON, ForeignKey, Date, DECIMAL 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.ext.declarative import declarative_base
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime 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_profile = relationship("LazerUserProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
lazer_statistics = relationship("LazerUserStatistics", back_populates="user", 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_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") 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") 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") 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") 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) 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 专用表模型 # Lazer API 专用表模型
@@ -127,6 +147,20 @@ class LazerUserProfile(Base):
user = relationship("User", back_populates="lazer_profile") 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): class LazerUserCountry(Base):
__tablename__ = "lazer_user_countries" __tablename__ = "lazer_user_countries"
@@ -154,7 +188,7 @@ class LazerUserCounts(Base):
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
# 统计计数字段 # 统计计数字段f
beatmap_playcounts_count = Column(Integer, default=0) beatmap_playcounts_count = Column(Integer, default=0)
comments_count = Column(Integer, default=0) comments_count = Column(Integer, default=0)
favourite_beatmapset_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") 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): class LazerUserAchievement(Base):
__tablename__ = "lazer_user_achievements" __tablename__ = "lazer_user_achievements"
@@ -244,6 +293,64 @@ class LazerUserAchievement(Base):
user = relationship("User", back_populates="lazer_achievements") 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,7 +1,6 @@
from typing import Dict, List, Optional from datetime import datetime, UTC
from datetime import datetime from app.models import User, Statistics, Level, GradeCounts, Country, Cover, Kudosu, GameMode, PlayStyle, Team, UserAchievement, RankHistory, DailyChallengeStats, RankHighest, Page
from app.models import * from app.database import User as DBUser, LazerUserProfile, LazerUserStatistics, LazerUserCountry, LazerUserKudosu, LazerUserCounts, LazerUserAchievement
from app.database import User as DBUser, LazerUserStatistics, LazerUserProfile, LazerUserCountry, LazerUserKudosu, LazerUserCounts, LazerUserAchievement
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -20,6 +19,13 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio
# 如果没有 lazer 资料,使用默认值 # 如果没有 lazer 资料,使用默认值
profile = create_default_profile(db_user) profile = create_default_profile(db_user)
# 获取 Lazer 用户计数
lzrcnt=db_user.lazer_statistics
if not lzrcnt:
# 如果没有 lazer 计数,使用默认值
lzrcnt = create_default_counts()
# 获取指定模式的统计信息 # 获取指定模式的统计信息
user_stats = None user_stats = None
for stat in db_user.lazer_statistics: 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) user_stats = create_default_lazer_statistics(ruleset)
# 获取国家信息 # 获取国家信息
country_code = db_user.country_code if db_user.country_code else 'XX'
country = Country( country = Country(
code=db_user.country_code, code=country_code,
name=get_country_name(db_user.country_code) name=get_country_name(country_code)
) )
# 获取 Kudosu 信息 # 获取 Kudosu 信息
@@ -225,11 +233,69 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio
if avatar_url is None: if avatar_url is None:
avatar_url = f"https://a.gu-osu.gmoe.cc/api/users/avatar/1" 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( user = User(
id=user_id, id=user_id,
username=user_name, username=user_name,
avatar_url=avatar_url, # 使用我们上面获取的头像URL avatar_url=avatar_url,
country_code=user_country, country_code=country_code,
default_group=profile.default_group if profile else "default", default_group=profile.default_group if profile else "default",
is_active=profile.is_active if profile else True, is_active=profile.is_active if profile else True,
is_bot=profile.is_bot if profile else False, 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_online=profile.is_online if profile else True,
is_supporter=profile.is_supporter if profile else False, is_supporter=profile.is_supporter if profile else False,
is_restricted=profile.is_restricted 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, pm_friends_only=profile.pm_friends_only if profile else False,
profile_colour=profile.profile_colour if profile else None, 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, discord=profile.discord if profile else None,
has_supported=profile.has_supported if profile else False, has_supported=profile.has_supported if profile else False,
interests=profile.interests if profile else None, 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, location=profile.location if profile else None,
max_blocks=profile.max_blocks if profile else 100, max_blocks=profile.max_blocks if profile and profile.max_blocks else 100,
max_friends=profile.max_friends if profile else 500, 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,
occupation=None, # 职业字段,默认为 None #待修改 profile_hue=profile.profile_hue if profile and profile.profile_hue else None,
profile_order=profile_order, # 使用排序后的 profile_order
#playmode=GameMode(db_user.playmode), title=profile.title if profile else None,
playmode=GameMode("osu"), #待修改 title_url=profile.title_url if profile else None,
twitter=profile.twitter if profile else None,
playstyle=[PlayStyle.MOUSE, PlayStyle.KEYBOARD, PlayStyle.TABLET], #待修改 website=profile.website if profile else None,
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',
session_verified=True, session_verified=True,
support_level=0, support_level=profile.support_level if profile else 0,
country=country, country=country,
cover=cover, cover=cover,
kudosu=kudosu, kudosu=kudosu,
statistics=statistics, statistics=statistics,
statistics_rulesets=statistics_rulesets, statistics_rulesets=statistics_rulesets,
beatmap_playcounts_count=3306, beatmap_playcounts_count=lzrcnt.beatmap_playcounts_count if lzrcnt else 0,
comments_count=0, comments_count=lzrcnt.comments_count if lzrcnt else 0,
favourite_beatmapset_count=0, favourite_beatmapset_count=lzrcnt.favourite_beatmapset_count if lzrcnt else 0,
follower_count=0, follower_count=lzrcnt.follower_count if lzrcnt else 0,
graveyard_beatmapset_count=0, graveyard_beatmapset_count=lzrcnt.graveyard_beatmapset_count if lzrcnt else 0,
guest_beatmapset_count=0, guest_beatmapset_count=lzrcnt.guest_beatmapset_count if lzrcnt else 0,
loved_beatmapset_count=0, loved_beatmapset_count=lzrcnt.loved_beatmapset_count if lzrcnt else 0,
mapping_follower_count=0, mapping_follower_count=lzrcnt.mapping_follower_count if lzrcnt else 0,
nominated_beatmapset_count=0, nominated_beatmapset_count=lzrcnt.nominated_beatmapset_count if lzrcnt else 0,
pending_beatmapset_count=0, pending_beatmapset_count=lzrcnt.pending_beatmapset_count if lzrcnt else 0,
ranked_beatmapset_count=0, ranked_beatmapset_count=lzrcnt.ranked_beatmapset_count if lzrcnt else 0,
ranked_and_approved_beatmapset_count=0, ranked_and_approved_beatmapset_count=lzrcnt.ranked_and_approved_beatmapset_count if lzrcnt else 0,
unranked_beatmapset_count=0, unranked_beatmapset_count=lzrcnt.unranked_beatmapset_count if lzrcnt else 0,
scores_best_count=0, scores_best_count=lzrcnt.scores_best_count if lzrcnt else 0,
scores_first_count=0, scores_first_count=lzrcnt.scores_first_count if lzrcnt else 0,
scores_pinned_count=0, scores_pinned_count=lzrcnt.scores_pinned_count,
scores_recent_count=0, scores_recent_count=lzrcnt.scores_recent_count if lzrcnt else 0,
account_history=[], account_history=[], #TODO: 获取用户历史账户信息
active_tournament_banner=None, active_tournament_banner=len(active_tournament_banners),
active_tournament_banners=[], active_tournament_banners=active_tournament_banners,
badges=[], badges=badges,
current_season_stats=None, current_season_stats=None,
daily_challenge_user_stats=None, daily_challenge_user_stats=None,
groups=[], groups=[],
monthly_playcounts=[], monthly_playcounts=monthly_playcounts,
#page=Page(html=db_user.page_html, raw=db_user.page_raw), page=Page(html=profile.page_html, raw=profile.page_raw) if profile.page_html or profile.page_raw else Page(),
page=Page(), # Provide a default Page object previous_usernames=previous_usernames,
previous_usernames=[],
rank_highest=rank_highest, rank_highest=rank_highest,
rank_history=rank_history, rank_history=rank_history,
rankHistory=rank_history, # 兼容性别名 rankHistory=rank_history,
replays_watched_counts=[], replays_watched_counts=replays_watched_counts,
team=team, team=team,
user_achievements=user_achievements user_achievements=user_achievements
) )
@@ -329,6 +386,7 @@ def get_country_name(country_code: str) -> str:
def create_default_profile(db_user: DBUser): def create_default_profile(db_user: DBUser):
"""创建默认的用户资料""" """创建默认的用户资料"""
# 完善 MockProfile 类定义
class MockProfile: class MockProfile:
def __init__(self): def __init__(self):
self.is_active = True self.is_active = True
@@ -342,7 +400,7 @@ def create_default_profile(db_user: DBUser):
self.pm_friends_only = False self.pm_friends_only = False
self.default_group = 'default' self.default_group = 'default'
self.last_visit = None 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_colour = None
self.profile_hue = None self.profile_hue = None
self.avatar_url = None self.avatar_url = None
@@ -360,8 +418,14 @@ def create_default_profile(db_user: DBUser):
self.max_blocks = 100 self.max_blocks = 100
self.max_friends = 500 self.max_friends = 500
self.post_count = 0 self.post_count = 0
# 添加profile_order字段
self.profile_order = MockLazerUserProfileSections.get_sorted_sections()
self.page_html = None self.page_html = None
self.page_raw = None self.page_raw = None
# 在MockProfile类中添加active_tournament_banners字段
self.active_tournament_banners = MockLazerTournamentBanner.create_default_banners()
self.active_tournament_banners = [] # 默认空列表
return MockProfile() return MockProfile()
@@ -383,11 +447,11 @@ def create_default_lazer_statistics(mode: str):
self.pp_exp = 0.0 self.pp_exp = 0.0
self.ranked_score = 0 self.ranked_score = 0
self.hit_accuracy = 0.0 self.hit_accuracy = 0.0
self.play_count = 0
self.play_time = 0
self.total_score = 0 self.total_score = 0
self.total_hits = 0 self.total_hits = 0
self.maximum_combo = 0 self.maximum_combo = 0
self.play_count = 0
self.play_time = 0
self.replays_watched_by_others = 0 self.replays_watched_by_others = 0
self.is_ranked = False self.is_ranked = False
self.grade_ss = 0 self.grade_ss = 0
@@ -426,7 +490,10 @@ def create_default_counts():
"""创建默认的计数信息""" """创建默认的计数信息"""
class MockCounts: class MockCounts:
def __init__(self): def __init__(self):
self.recent_scores_count = None
self.beatmap_playcounts_count = 0 self.beatmap_playcounts_count = 0
self.scores_first_count = 0
self.scores_pinned_count = 0
self.comments_count = 0 self.comments_count = 0
self.favourite_beatmapset_count = 0 self.favourite_beatmapset_count = 0
self.follower_count = 0 self.follower_count = 0
@@ -443,5 +510,52 @@ def create_default_counts():
self.scores_first_count = 0 self.scores_first_count = 0
self.scores_pinned_count = 0 self.scores_pinned_count = 0
self.scores_recent_count = 0 self.scores_recent_count = 0
return MockCounts() 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)
]

View File

@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
import time import time
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.dependencies import get_db, engine 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 from app.auth import get_password_hash
# 创建所有表 # 创建所有表
@@ -134,7 +134,7 @@ def create_sample_user():
db.refresh(user) db.refresh(user)
# 创建 osu! 模式统计 # 创建 osu! 模式统计
osu_stats = UserStatistics( osu_stats = LazerUserStatistics(
user_id=user.id, user_id=user.id,
mode="osu", mode="osu",
count_100=276274, count_100=276274,
@@ -165,7 +165,7 @@ def create_sample_user():
) )
# 创建 taiko 模式统计 # 创建 taiko 模式统计
taiko_stats = UserStatistics( taiko_stats = LazerUserStatistics(
user_id=user.id, user_id=user.id,
mode="taiko", mode="taiko",
count_100=160, count_100=160,
@@ -188,7 +188,7 @@ def create_sample_user():
) )
# 创建 fruits 模式统计 # 创建 fruits 模式统计
fruits_stats = UserStatistics( fruits_stats = LazerUserStatistics(
user_id=user.id, user_id=user.id,
mode="fruits", mode="fruits",
count_100=109, count_100=109,
@@ -212,7 +212,7 @@ def create_sample_user():
) )
# 创建 mania 模式统计 # 创建 mania 模式统计
mania_stats = UserStatistics( mania_stats = LazerUserStatistics(
user_id=user.id, user_id=user.id,
mode="mania", mode="mania",
count_100=7867, count_100=7867,

View File

@@ -20,7 +20,6 @@ app = FastAPI(title="osu! API 模拟服务器", version="1.0.0")
security = HTTPBearer() security = HTTPBearer()
@app.post("/oauth/token", response_model=TokenResponse) @app.post("/oauth/token", response_model=TokenResponse)
async def oauth_token( async def oauth_token(
grant_type: str = Form(...), grant_type: str = Form(...),