diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..359bb53
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..d4add55
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..e5787b9
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..92ff71d
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/osu_lazer_api.iml b/.idea/osu_lazer_api.iml
new file mode 100644
index 0000000..e2e520d
--- /dev/null
+++ b/.idea/osu_lazer_api.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/database copy.py b/app/database copy.py
deleted file mode 100644
index 7ba80bc..0000000
--- a/app/database copy.py
+++ /dev/null
@@ -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")
diff --git a/app/utils.py b/app/utils.py
index c762bff..f7ced49 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -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
diff --git a/main.py b/main.py
index 12780fb..f293cf2 100644
--- a/main.py
+++ b/main.py
@@ -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")