feat: 添加从数据库中读取部分lazer资料的功能

This commit is contained in:
jimmy-sketch
2025-07-19 14:45:15 +08:00
parent c0246440f3
commit d8fcbf02cf
10 changed files with 86 additions and 433 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

@@ -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,7 +1,7 @@
from typing import Dict, List, Optional
from datetime import datetime
from app.models import *
from app.database import User as DBUser, LazerUserStatistics, LazerUserProfile, LazerUserCountry, LazerUserKudosu, LazerUserCounts, LazerUserAchievement
from datetime import datetime, UTC
from app.models import User, Statistics, Level, GradeCounts, Country, Cover, Kudosu, GameMode, PlayStyle, Team, UserAchievement, RankHistory, DailyChallengeStats, RankHighest, Page
from app.database import User as DBUser, LazerUserProfile, LazerUserStatistics, LazerUserCountry, LazerUserKudosu, LazerUserCounts, LazerUserAchievement
from sqlalchemy.orm import Session
@@ -32,9 +32,11 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio
user_stats = create_default_lazer_statistics(ruleset)
# 获取国家信息
country_code = db_user.country_code if db_user.country_code else 'XX'
country = Country(
code=db_user.country_code,
name=get_country_name(db_user.country_code)
code=country_code,
name=get_country_name(country_code)
)
# 获取 Kudosu 信息
@@ -186,14 +188,14 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio
user_id = getattr(db_user, 'id')
user_name = getattr(db_user, 'name')
user_country = getattr(db_user, 'country')
# 获取用户头像URL
avatar_url = None
# 首先检查 profile 中的 avatar_url
if profile and hasattr(profile, 'avatar_url') and profile.avatar_url:
avatar_url = str(profile.avatar_url)
# 然后检查是否有关联的头像记录
if avatar_url is None and hasattr(db_user, 'avatar') and db_user.avatar is not None:
if db_user.avatar.r2_game_url:
@@ -202,13 +204,13 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio
elif db_user.avatar.r2_original_url:
# 其次使用原始头像URL
avatar_url = str(db_user.avatar.r2_original_url)
# 如果还是没有找到,通过查询获取
if db_session and avatar_url is None:
try:
# 导入UserAvatar模型
from app.database import UserAvatar
# 尝试查找用户的头像记录
avatar_record = db_session.query(UserAvatar).filter_by(user_id=user_id, is_active=True).first()
if avatar_record is not None:
@@ -229,7 +231,7 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio
id=user_id,
username=user_name,
avatar_url=avatar_url, # 使用我们上面获取的头像URL
country_code=user_country,
country_code=country_code,
default_group=profile.default_group if profile else "default",
is_active=profile.is_active if profile else True,
is_bot=profile.is_bot if profile else False,
@@ -237,27 +239,19 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio
is_online=profile.is_online if profile else True,
is_supporter=profile.is_supporter if profile else False,
is_restricted=profile.is_restricted if profile else False,
last_visit=db_user.last_visit,
last_visit=db_user.last_visit if db_user.last_visit else None,
pm_friends_only=profile.pm_friends_only if profile else False,
profile_colour=profile.profile_colour if profile else None,
cover_url=cover_url,
cover_url=profile.cover_url if profile and profile.cover_url else "https://assets.ppy.sh/user-profile-covers/default.jpeg",
discord=profile.discord if profile else None,
has_supported=profile.has_supported if profile else False,
interests=profile.interests if profile else None,
join_date=db_user.join_date,
join_date=db_user.join_date if db_user.join_date else None,
location=profile.location if profile else None,
max_blocks=profile.max_blocks if profile else 100,
max_friends=profile.max_friends if profile else 500,
occupation=None, # 职业字段,默认为 None #待修改
#playmode=GameMode(db_user.playmode),
playmode=GameMode("osu"), #待修改
playstyle=[PlayStyle.MOUSE, PlayStyle.KEYBOARD, PlayStyle.TABLET], #待修改
post_count=0,
profile_hue=None,
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= ['me', 'recent_activity', 'top_ranks', 'medals', 'historical', 'beatmaps', 'kudosu'],
title=None,
title_url=None,
@@ -295,12 +289,11 @@ def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu", db_sessio
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
page=Page(html=profile.page_html, raw=profile.page_raw) if profile.page_html or profile.page_raw else Page(),
previous_usernames=[],
rank_highest=rank_highest,
rank_history=rank_history,
rankHistory=rank_history, # 兼容性别名
rankHistory=rank_history, # 保留旧API兼容性字段
replays_watched_counts=[],
team=team,
user_achievements=user_achievements

View File

@@ -20,7 +20,6 @@ app = FastAPI(title="osu! API 模拟服务器", version="1.0.0")
security = HTTPBearer()
@app.post("/oauth/token", response_model=TokenResponse)
async def oauth_token(
grant_type: str = Form(...),
@@ -104,7 +103,7 @@ async def oauth_token(
refresh_token=new_refresh_token,
scope=scope
)
else:
raise HTTPException(status_code=400, detail="Unsupported grant type")