463 lines
17 KiB
Python
463 lines
17 KiB
Python
# ruff: noqa: I002
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
from .legacy import LegacyUserStatistics
|
||
from .team import TeamMember
|
||
|
||
from sqlalchemy import DECIMAL, JSON, Column, Date, DateTime, Text
|
||
from sqlalchemy.dialects.mysql import VARCHAR
|
||
from sqlalchemy.orm import Mapped
|
||
from sqlmodel import BigInteger, Field, Relationship, SQLModel
|
||
|
||
|
||
class User(SQLModel, table=True):
|
||
__tablename__ = "users" # pyright: ignore[reportAssignmentType]
|
||
|
||
# 主键
|
||
id: int = Field(default=None, primary_key=True, index=True, nullable=False)
|
||
|
||
# 基本信息(匹配 migrations 中的结构)
|
||
name: str = Field(max_length=32, unique=True, index=True) # 用户名
|
||
safe_name: str = Field(max_length=32, unique=True, index=True) # 安全用户名
|
||
email: str = Field(max_length=254, unique=True, index=True)
|
||
priv: int = Field(default=1) # 权限
|
||
pw_bcrypt: str = Field(max_length=60) # bcrypt 哈希密码
|
||
country: str = Field(default="CN", max_length=2) # 国家代码
|
||
|
||
# 状态和时间
|
||
silence_end: int = Field(default=0)
|
||
donor_end: int = Field(default=0)
|
||
creation_time: int = Field(default=0) # Unix 时间戳
|
||
latest_activity: int = Field(default=0) # Unix 时间戳
|
||
|
||
# 游戏相关
|
||
preferred_mode: int = Field(default=0) # 偏好游戏模式
|
||
play_style: int = Field(default=0) # 游戏风格
|
||
|
||
# 扩展信息
|
||
clan_id: int = Field(default=0)
|
||
clan_priv: int = Field(default=0)
|
||
custom_badge_name: str | None = Field(default=None, max_length=16)
|
||
custom_badge_icon: str | None = Field(default=None, max_length=64)
|
||
userpage_content: str | None = Field(default=None, max_length=2048)
|
||
api_key: str | None = Field(default=None, max_length=36, unique=True)
|
||
|
||
# 虚拟字段用于兼容性
|
||
@property
|
||
def username(self):
|
||
return self.name
|
||
|
||
@property
|
||
def country_code(self):
|
||
return self.country
|
||
|
||
@property
|
||
def join_date(self):
|
||
creation_time = getattr(self, "creation_time", 0)
|
||
return (
|
||
datetime.fromtimestamp(creation_time)
|
||
if creation_time > 0
|
||
else datetime.utcnow()
|
||
)
|
||
|
||
@property
|
||
def last_visit(self):
|
||
latest_activity = getattr(self, "latest_activity", 0)
|
||
return datetime.fromtimestamp(latest_activity) if latest_activity > 0 else None
|
||
|
||
# 关联关系
|
||
lazer_profile: Mapped[Optional["LazerUserProfile"]] = Relationship(back_populates="user")
|
||
lazer_statistics: Mapped[list["LazerUserStatistics"]] = Relationship(back_populates="user")
|
||
lazer_counts: Mapped[Optional["LazerUserCounts"]] = Relationship(back_populates="user")
|
||
lazer_achievements: Mapped[list["LazerUserAchievement"]] = Relationship(
|
||
back_populates="user"
|
||
)
|
||
lazer_profile_sections: Mapped[list["LazerUserProfileSections"]] = Relationship(
|
||
back_populates="user"
|
||
)
|
||
statistics: list["LegacyUserStatistics"] = Relationship(back_populates="user")
|
||
team_membership: Mapped[list["TeamMember"]] = Relationship(back_populates="user")
|
||
daily_challenge_stats: Mapped[Optional["DailyChallengeStats"]] = Relationship(
|
||
back_populates="user"
|
||
)
|
||
rank_history: Mapped[list["RankHistory"]] = Relationship(back_populates="user")
|
||
avatar: Mapped[Optional["UserAvatar"]] = Relationship(back_populates="user")
|
||
active_banners: Mapped[list["LazerUserBanners"]] = Relationship(back_populates="user")
|
||
lazer_badges: Mapped[list["LazerUserBadge"]] = Relationship(back_populates="user")
|
||
lazer_monthly_playcounts: Mapped[list["LazerUserMonthlyPlaycounts"]] = Relationship(
|
||
back_populates="user"
|
||
)
|
||
lazer_previous_usernames: Mapped[list["LazerUserPreviousUsername"]] = Relationship(
|
||
back_populates="user"
|
||
)
|
||
lazer_replays_watched: Mapped[list["LazerUserReplaysWatched"]] = Relationship(
|
||
back_populates="user"
|
||
)
|
||
|
||
# ============================================
|
||
# Lazer API 专用表模型
|
||
# ============================================
|
||
|
||
|
||
class LazerUserProfile(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_profiles" # pyright: ignore[reportAssignmentType]
|
||
|
||
user_id: int = Field(foreign_key="users.id", primary_key=True)
|
||
|
||
# 基本状态字段
|
||
is_active: bool = Field(default=True)
|
||
is_bot: bool = Field(default=False)
|
||
is_deleted: bool = Field(default=False)
|
||
is_online: bool = Field(default=True)
|
||
is_supporter: bool = Field(default=False)
|
||
is_restricted: bool = Field(default=False)
|
||
session_verified: bool = Field(default=False)
|
||
has_supported: bool = Field(default=False)
|
||
pm_friends_only: bool = Field(default=False)
|
||
|
||
# 基本资料字段
|
||
default_group: str = Field(default="default", max_length=50)
|
||
last_visit: datetime | None = Field(default=None, sa_column=Column(DateTime))
|
||
join_date: datetime | None = Field(default=None, sa_column=Column(DateTime))
|
||
profile_colour: str | None = Field(default=None, max_length=7)
|
||
profile_hue: int | None = Field(default=None)
|
||
|
||
# 社交媒体和个人资料字段
|
||
avatar_url: str | None = Field(default=None, max_length=500)
|
||
cover_url: str | None = Field(default=None, max_length=500)
|
||
discord: str | None = Field(default=None, max_length=100)
|
||
twitter: str | None = Field(default=None, max_length=100)
|
||
website: str | None = Field(default=None, max_length=500)
|
||
title: str | None = Field(default=None, max_length=100)
|
||
title_url: str | None = Field(default=None, max_length=500)
|
||
interests: str | None = Field(default=None, sa_column=Column(Text))
|
||
location: str | None = Field(default=None, max_length=100)
|
||
|
||
occupation: str | None = Field(default=None) # 职业字段,默认为 None
|
||
|
||
# 游戏相关字段
|
||
playmode: str = Field(default="osu", max_length=10)
|
||
support_level: int = Field(default=0)
|
||
max_blocks: int = Field(default=100)
|
||
max_friends: int = Field(default=500)
|
||
post_count: int = Field(default=0)
|
||
|
||
# 页面内容
|
||
page_html: str | None = Field(default=None, sa_column=Column(Text))
|
||
page_raw: str | None = Field(default=None, sa_column=Column(Text))
|
||
|
||
profile_order: str = Field(
|
||
default="me,recent_activity,top_ranks,medals,historical,beatmaps,kudosu"
|
||
)
|
||
|
||
# 关联关系
|
||
user: Mapped["User"] = Relationship(back_populates="lazer_profile")
|
||
|
||
|
||
class LazerUserProfileSections(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_profile_sections" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True)
|
||
user_id: int = Field(foreign_key="users.id")
|
||
section_name: str = Field(sa_column=Column(VARCHAR(50)))
|
||
display_order: int | None = Field(default=None)
|
||
|
||
created_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
updated_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
user: Mapped["User"] = Relationship(back_populates="lazer_profile_sections")
|
||
|
||
|
||
class LazerUserCountry(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_countries" # pyright: ignore[reportAssignmentType]
|
||
|
||
user_id: int = Field(foreign_key="users.id", primary_key=True)
|
||
code: str = Field(max_length=2)
|
||
name: str = Field(max_length=100)
|
||
|
||
created_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
updated_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
|
||
class LazerUserKudosu(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_kudosu" # pyright: ignore[reportAssignmentType]
|
||
|
||
user_id: int = Field(foreign_key="users.id", primary_key=True)
|
||
available: int = Field(default=0)
|
||
total: int = Field(default=0)
|
||
|
||
created_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
updated_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
|
||
class LazerUserCounts(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_counts" # pyright: ignore[reportAssignmentType]
|
||
|
||
user_id: int = Field(foreign_key="users.id", primary_key=True)
|
||
|
||
# 统计计数字段
|
||
beatmap_playcounts_count: int = Field(default=0)
|
||
comments_count: int = Field(default=0)
|
||
favourite_beatmapset_count: int = Field(default=0)
|
||
follower_count: int = Field(default=0)
|
||
graveyard_beatmapset_count: int = Field(default=0)
|
||
guest_beatmapset_count: int = Field(default=0)
|
||
loved_beatmapset_count: int = Field(default=0)
|
||
mapping_follower_count: int = Field(default=0)
|
||
nominated_beatmapset_count: int = Field(default=0)
|
||
pending_beatmapset_count: int = Field(default=0)
|
||
ranked_beatmapset_count: int = Field(default=0)
|
||
ranked_and_approved_beatmapset_count: int = Field(default=0)
|
||
unranked_beatmapset_count: int = Field(default=0)
|
||
scores_best_count: int = Field(default=0)
|
||
scores_first_count: int = Field(default=0)
|
||
scores_pinned_count: int = Field(default=0)
|
||
scores_recent_count: int = Field(default=0)
|
||
|
||
created_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
updated_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
# 关联关系
|
||
user: Mapped["User"] = Relationship(back_populates="lazer_counts")
|
||
|
||
|
||
class LazerUserStatistics(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_statistics" # pyright: ignore[reportAssignmentType]
|
||
|
||
user_id: int = Field(foreign_key="users.id", primary_key=True)
|
||
mode: str = Field(default="osu", max_length=10, primary_key=True)
|
||
|
||
# 基本命中统计
|
||
count_100: int = Field(default=0)
|
||
count_300: int = Field(default=0)
|
||
count_50: int = Field(default=0)
|
||
count_miss: int = Field(default=0)
|
||
|
||
# 等级信息
|
||
level_current: int = Field(default=1)
|
||
level_progress: int = Field(default=0)
|
||
|
||
# 排名信息
|
||
global_rank: int | None = Field(default=None)
|
||
global_rank_exp: int | None = Field(default=None)
|
||
country_rank: int | None = Field(default=None)
|
||
|
||
# PP 和分数
|
||
pp: float = Field(default=0.00, sa_column=Column(DECIMAL(10, 2)))
|
||
pp_exp: float = Field(default=0.00, sa_column=Column(DECIMAL(10, 2)))
|
||
ranked_score: int = Field(default=0, sa_column=Column(BigInteger))
|
||
hit_accuracy: float = Field(default=0.00, sa_column=Column(DECIMAL(5, 2)))
|
||
total_score: int = Field(default=0, sa_column=Column(BigInteger))
|
||
total_hits: int = Field(default=0, sa_column=Column(BigInteger))
|
||
maximum_combo: int = Field(default=0)
|
||
|
||
# 游戏统计
|
||
play_count: int = Field(default=0)
|
||
play_time: int = Field(default=0) # 秒
|
||
replays_watched_by_others: int = Field(default=0)
|
||
is_ranked: bool = Field(default=False)
|
||
|
||
# 成绩等级计数
|
||
grade_ss: int = Field(default=0)
|
||
grade_ssh: int = Field(default=0)
|
||
grade_s: int = Field(default=0)
|
||
grade_sh: int = Field(default=0)
|
||
grade_a: int = Field(default=0)
|
||
|
||
# 最高排名记录
|
||
rank_highest: int | None = Field(default=None)
|
||
rank_highest_updated_at: datetime | None = Field(
|
||
default=None, sa_column=Column(DateTime)
|
||
)
|
||
|
||
created_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
updated_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
# 关联关系
|
||
user: Mapped["User"] = Relationship(back_populates="lazer_statistics")
|
||
|
||
|
||
class LazerUserBanners(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_tournament_banners" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True)
|
||
user_id: int = Field(foreign_key="users.id")
|
||
tournament_id: int
|
||
image_url: str = Field(sa_column=Column(VARCHAR(500)))
|
||
is_active: bool | None = Field(default=None)
|
||
|
||
# 修正user关系的back_populates值
|
||
user: Mapped["User"] = Relationship(back_populates="active_banners")
|
||
|
||
|
||
class LazerUserAchievement(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_achievements" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||
user_id: int = Field(foreign_key="users.id")
|
||
achievement_id: int
|
||
achieved_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
user: Mapped["User"] = Relationship(back_populates="lazer_achievements")
|
||
|
||
|
||
class LazerUserBadge(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_badges" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||
user_id: int = Field(foreign_key="users.id")
|
||
badge_id: int
|
||
awarded_at: datetime | None = Field(default=None, sa_column=Column(DateTime))
|
||
description: str | None = Field(default=None, sa_column=Column(Text))
|
||
image_url: str | None = Field(default=None, max_length=500)
|
||
url: str | None = Field(default=None, max_length=500)
|
||
|
||
created_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
updated_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
user: Mapped["User"] = Relationship(back_populates="lazer_badges")
|
||
|
||
|
||
class LazerUserMonthlyPlaycounts(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_monthly_playcounts" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||
user_id: int = Field(foreign_key="users.id")
|
||
start_date: datetime = Field(sa_column=Column(Date))
|
||
play_count: int = Field(default=0)
|
||
|
||
created_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
updated_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
user: Mapped["User"] = Relationship(back_populates="lazer_monthly_playcounts")
|
||
|
||
|
||
class LazerUserPreviousUsername(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_previous_usernames" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||
user_id: int = Field(foreign_key="users.id")
|
||
username: str = Field(max_length=32)
|
||
changed_at: datetime = Field(sa_column=Column(DateTime))
|
||
|
||
created_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
updated_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
user: Mapped["User"] = Relationship(back_populates="lazer_previous_usernames")
|
||
|
||
|
||
class LazerUserReplaysWatched(SQLModel, table=True):
|
||
__tablename__ = "lazer_user_replays_watched" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||
user_id: int = Field(foreign_key="users.id")
|
||
start_date: datetime = Field(sa_column=Column(Date))
|
||
count: int = Field(default=0)
|
||
|
||
created_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
updated_at: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
user: Mapped["User"] = Relationship(back_populates="lazer_replays_watched")
|
||
|
||
|
||
# 类型转换用的 UserAchievement(不是 SQLAlchemy 模型)
|
||
@dataclass
|
||
class UserAchievement:
|
||
achieved_at: datetime
|
||
achievement_id: int
|
||
|
||
|
||
class DailyChallengeStats(SQLModel, table=True):
|
||
__tablename__ = "daily_challenge_stats" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||
user_id: int = Field(foreign_key="users.id", unique=True)
|
||
|
||
daily_streak_best: int = Field(default=0)
|
||
daily_streak_current: int = Field(default=0)
|
||
last_update: datetime | None = Field(default=None, sa_column=Column(DateTime))
|
||
last_weekly_streak: datetime | None = Field(
|
||
default=None, sa_column=Column(DateTime)
|
||
)
|
||
playcount: int = Field(default=0)
|
||
top_10p_placements: int = Field(default=0)
|
||
top_50p_placements: int = Field(default=0)
|
||
weekly_streak_best: int = Field(default=0)
|
||
weekly_streak_current: int = Field(default=0)
|
||
|
||
user: Mapped["User"] = Relationship(back_populates="daily_challenge_stats")
|
||
|
||
|
||
class RankHistory(SQLModel, table=True):
|
||
__tablename__ = "rank_history" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||
user_id: int = Field(foreign_key="users.id")
|
||
mode: str = Field(max_length=10)
|
||
rank_data: list = Field(sa_column=Column(JSON)) # Array of ranks
|
||
date_recorded: datetime = Field(
|
||
default_factory=datetime.utcnow, sa_column=Column(DateTime)
|
||
)
|
||
|
||
user: Mapped["User"] = Relationship(back_populates="rank_history")
|
||
|
||
|
||
class UserAvatar(SQLModel, table=True):
|
||
__tablename__ = "user_avatars" # pyright: ignore[reportAssignmentType]
|
||
|
||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||
user_id: int = Field(foreign_key="users.id")
|
||
filename: str = Field(max_length=255)
|
||
original_filename: str = Field(max_length=255)
|
||
file_size: int
|
||
mime_type: str = Field(max_length=100)
|
||
is_active: bool = Field(default=True)
|
||
created_at: int = Field(default_factory=lambda: int(datetime.now().timestamp()))
|
||
updated_at: int = Field(default_factory=lambda: int(datetime.now().timestamp()))
|
||
r2_original_url: str | None = Field(default=None, max_length=500)
|
||
r2_game_url: str | None = Field(default=None, max_length=500)
|
||
|
||
user: Mapped["User"] = Relationship(back_populates="avatar")
|