chore(merge): merge pull request #7 from GooGuTeam/feat/solo-play

feat: 单人游戏
This commit is contained in:
MingxuanGame
2025-07-28 16:53:20 +08:00
committed by GitHub
43 changed files with 6155 additions and 751 deletions

View File

@@ -9,6 +9,13 @@ from .beatmapset import (
)
from .legacy import LegacyOAuthToken, LegacyUserStatistics
from .relationship import Relationship, RelationshipResp, RelationshipType
from .score import (
Score,
ScoreBase,
ScoreResp,
ScoreStatistics,
)
from .score_token import ScoreToken, ScoreTokenResp
from .team import Team, TeamMember
from .user import (
DailyChallengeStats,
@@ -57,6 +64,12 @@ __all__ = [
"Relationship",
"RelationshipResp",
"RelationshipType",
"Score",
"ScoreBase",
"ScoreResp",
"ScoreStatistics",
"ScoreToken",
"ScoreTokenResp",
"Team",
"TeamMember",
"User",

View File

@@ -2,7 +2,7 @@ from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import Column, DateTime
from sqlmodel import Field, Relationship, SQLModel
from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel
if TYPE_CHECKING:
from .user import User
@@ -12,7 +12,9 @@ class OAuthToken(SQLModel, table=True):
__tablename__ = "oauth_tokens" # pyright: ignore[reportAssignmentType]
id: int | None = Field(default=None, primary_key=True, index=True)
user_id: int = Field(foreign_key="users.id")
user_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("users.id"), index=True)
)
access_token: str = Field(max_length=500, unique=True)
refresh_token: str = Field(max_length=500, unique=True)
token_type: str = Field(default="Bearer", max_length=20)

View File

@@ -1,6 +1,6 @@
from datetime import datetime
from typing import TYPE_CHECKING
from app.fetcher import Fetcher
from app.models.beatmap import BeatmapRankStatus
from app.models.score import MODE_TO_INT, GameMode
@@ -11,6 +11,9 @@ from sqlalchemy.orm import joinedload
from sqlmodel import VARCHAR, Field, Relationship, SQLModel, select
from sqlmodel.ext.asyncio.session import AsyncSession
if TYPE_CHECKING:
from app.fetcher import Fetcher
class BeatmapOwner(SQLModel):
id: int
@@ -65,6 +68,10 @@ class Beatmap(BeatmapBase, table=True):
# optional
beatmapset: Beatmapset = Relationship(back_populates="beatmaps")
@property
def can_ranked(self) -> bool:
return self.beatmap_status > BeatmapRankStatus.PENDING
@classmethod
async def from_resp(cls, session: AsyncSession, resp: "BeatmapResp") -> "Beatmap":
d = resp.model_dump()
@@ -107,19 +114,25 @@ class Beatmap(BeatmapBase, table=True):
@classmethod
async def get_or_fetch(
cls, session: AsyncSession, bid: int, fetcher: Fetcher
cls,
session: AsyncSession,
fetcher: "Fetcher",
bid: int | None = None,
md5: str | None = None,
) -> "Beatmap":
beatmap = (
await session.exec(
select(Beatmap)
.where(Beatmap.id == bid)
.where(
Beatmap.id == bid if bid is not None else Beatmap.checksum == md5
)
.options(
joinedload(Beatmap.beatmapset).selectinload(Beatmapset.beatmaps) # pyright: ignore[reportArgumentType]
)
)
).first()
if not beatmap:
resp = await fetcher.get_beatmap(bid)
resp = await fetcher.get_beatmap(bid, md5)
r = await session.exec(
select(Beatmapset.id).where(Beatmapset.id == resp.beatmapset_id)
)

View File

@@ -2,6 +2,7 @@ from datetime import datetime
from typing import TYPE_CHECKING, TypedDict, cast
from app.models.beatmap import BeatmapRankStatus, Genre, Language
from app.models.score import GameMode
from pydantic import BaseModel, model_serializer
from sqlalchemy import DECIMAL, JSON, Column, DateTime, Text
@@ -68,7 +69,7 @@ class BeatmapNomination(TypedDict):
beatmapset_id: int
reset: bool
user_id: int
rulesets: list[str] | None
rulesets: list[GameMode] | None
class BeatmapDescription(SQLModel):

View File

@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
from sqlalchemy import JSON, Column, DateTime
from sqlalchemy.orm import Mapped
from sqlmodel import Field, Relationship, SQLModel
from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel
if TYPE_CHECKING:
from .user import User
@@ -16,7 +16,7 @@ class LegacyUserStatistics(SQLModel, table=True):
__tablename__ = "user_statistics" # pyright: ignore[reportAssignmentType]
id: int | None = Field(default=None, primary_key=True, index=True)
user_id: int = Field(foreign_key="users.id")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
mode: str = Field(max_length=10) # osu, taiko, fruits, mania
# 基本统计
@@ -77,7 +77,7 @@ class LegacyOAuthToken(SQLModel, table=True):
__tablename__ = "legacy_oauth_tokens" # pyright: ignore[reportAssignmentType]
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="users.id")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
access_token: str = Field(max_length=255, index=True)
refresh_token: str = Field(max_length=255, index=True)
expires_at: datetime = Field(sa_column=Column(DateTime))

View File

@@ -4,7 +4,10 @@ from .user import User
from pydantic import BaseModel
from sqlmodel import (
BigInteger,
Column,
Field,
ForeignKey,
Relationship as SQLRelationship,
SQLModel,
select,
@@ -20,10 +23,22 @@ class RelationshipType(str, Enum):
class Relationship(SQLModel, table=True):
__tablename__ = "relationship" # pyright: ignore[reportAssignmentType]
user_id: int = Field(
default=None, foreign_key="users.id", primary_key=True, index=True
default=None,
sa_column=Column(
BigInteger,
ForeignKey("users.id"),
primary_key=True,
index=True,
),
)
target_id: int = Field(
default=None, foreign_key="users.id", primary_key=True, index=True
default=None,
sa_column=Column(
BigInteger,
ForeignKey("users.id"),
primary_key=True,
index=True,
),
)
type: RelationshipType = Field(default=RelationshipType.FOLLOW, nullable=False)
target: "User" = SQLRelationship(

View File

@@ -2,15 +2,36 @@ from datetime import datetime
import math
from app.database.user import User
from app.models.beatmap import BeatmapRankStatus
from app.models.mods import APIMod
from app.models.score import MODE_TO_INT, GameMode, Rank
from app.models.score import (
MODE_TO_INT,
GameMode,
HitResult,
LeaderboardType,
Rank,
ScoreStatistics,
)
from .beatmap import Beatmap, BeatmapResp
from .beatmapset import BeatmapsetResp
from .beatmapset import Beatmapset, BeatmapsetResp
from pydantic import BaseModel
from sqlalchemy import Column, DateTime
from sqlmodel import JSON, BigInteger, Field, Relationship, SQLModel
from sqlalchemy import Column, ColumnExpressionArgument, DateTime
from sqlalchemy.orm import aliased, joinedload
from sqlmodel import (
JSON,
BigInteger,
Field,
ForeignKey,
Relationship,
SQLModel,
col,
false,
func,
select,
)
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodel.sql._expression_select_cls import SelectOfScalar
class ScoreBase(SQLModel):
@@ -34,6 +55,9 @@ class ScoreBase(SQLModel):
room_id: int | None = Field(default=None) # multiplayer
started_at: datetime = Field(sa_column=Column(DateTime))
total_score: int = Field(default=0, sa_column=Column(BigInteger))
total_score_without_mods: int = Field(
default=0, sa_column=Column(BigInteger), exclude=True
)
type: str
# optional
@@ -41,22 +65,20 @@ class ScoreBase(SQLModel):
position: int | None = Field(default=None) # multiplayer
class ScoreStatistics(BaseModel):
count_miss: int
count_50: int
count_100: int
count_300: int
count_geki: int
count_katu: int
count_large_tick_miss: int | None = None
count_slider_tail_hit: int | None = None
class Score(ScoreBase, table=True):
__tablename__ = "scores" # pyright: ignore[reportAssignmentType]
id: int = Field(primary_key=True)
id: int | None = Field(
default=None, sa_column=Column(BigInteger, autoincrement=True, primary_key=True)
)
beatmap_id: int = Field(index=True, foreign_key="beatmaps.id")
user_id: int = Field(foreign_key="users.id", index=True)
user_id: int = Field(
default=None,
sa_column=Column(
BigInteger,
ForeignKey("users.id"),
index=True,
),
)
# ScoreStatistics
n300: int = Field(exclude=True)
n100: int = Field(exclude=True)
@@ -72,9 +94,51 @@ class Score(ScoreBase, table=True):
beatmap: "Beatmap" = Relationship()
user: "User" = Relationship()
@property
def is_perfect_combo(self) -> bool:
return self.max_combo == self.beatmap.max_combo
@staticmethod
def select_clause() -> SelectOfScalar["Score"]:
return select(Score).options(
joinedload(Score.beatmap) # pyright: ignore[reportArgumentType]
.joinedload(Beatmap.beatmapset) # pyright: ignore[reportArgumentType]
.selectinload(
Beatmapset.beatmaps # pyright: ignore[reportArgumentType]
),
joinedload(Score.user).joinedload(User.lazer_profile), # pyright: ignore[reportArgumentType]
)
@staticmethod
def select_clause_unique(
*where_clauses: ColumnExpressionArgument[bool] | bool,
) -> SelectOfScalar["Score"]:
rownum = (
func.row_number()
.over(
partition_by=col(Score.user_id), order_by=col(Score.total_score).desc()
)
.label("rn")
)
subq = select(Score, rownum).where(*where_clauses).subquery()
best = aliased(Score, subq, adapt_on_names=True)
return (
select(best)
.where(subq.c.rn == 1)
.options(
joinedload(best.beatmap) # pyright: ignore[reportArgumentType]
.joinedload(Beatmap.beatmapset) # pyright: ignore[reportArgumentType]
.selectinload(
Beatmapset.beatmaps # pyright: ignore[reportArgumentType]
),
joinedload(best.user).joinedload(User.lazer_profile), # pyright: ignore[reportArgumentType]
)
)
class ScoreResp(ScoreBase):
id: int
user_id: int
is_perfect_combo: bool = False
legacy_perfect: bool = False
legacy_total_score: int = 0 # FIXME
@@ -85,10 +149,13 @@ class ScoreResp(ScoreBase):
beatmapset: BeatmapsetResp | None = None
# FIXME: user: APIUser | None = None
statistics: ScoreStatistics | None = None
rank_global: int | None = None
rank_country: int | None = None
@classmethod
def from_db(cls, score: Score) -> "ScoreResp":
async def from_db(cls, session: AsyncSession, score: Score) -> "ScoreResp":
s = cls.model_validate(score.model_dump())
assert score.id
s.beatmap = BeatmapResp.from_db(score.beatmap)
s.beatmapset = BeatmapsetResp.from_db(score.beatmap.beatmapset)
s.is_perfect_combo = s.max_combo == s.beatmap.max_combo
@@ -97,14 +164,220 @@ class ScoreResp(ScoreBase):
if score.best_id:
# https://osu.ppy.sh/wiki/Performance_points/Weighting_system
s.weight = math.pow(0.95, score.best_id)
s.statistics = ScoreStatistics(
count_miss=score.nmiss,
count_50=score.n50,
count_100=score.n100,
count_300=score.n300,
count_geki=score.ngeki,
count_katu=score.nkatu,
count_large_tick_miss=score.nlarge_tick_miss,
count_slider_tail_hit=score.nslider_tail_hit,
s.statistics = {
HitResult.MISS: score.nmiss,
HitResult.MEH: score.n50,
HitResult.OK: score.n100,
HitResult.GREAT: score.n300,
HitResult.PERFECT: score.ngeki,
HitResult.GOOD: score.nkatu,
}
if score.nlarge_tick_miss is not None:
s.statistics[HitResult.LARGE_TICK_MISS] = score.nlarge_tick_miss
if score.nslider_tail_hit is not None:
s.statistics[HitResult.SLIDER_TAIL_HIT] = score.nslider_tail_hit
# s.user = await convert_db_user_to_api_user(score.user)
s.rank_global = (
await get_score_position_by_id(
session,
score.map_md5,
score.id,
mode=score.gamemode,
user=score.user,
)
or None
)
s.rank_country = (
await get_score_position_by_id(
session,
score.map_md5,
score.id,
score.gamemode,
score.user,
)
or None
)
return s
async def get_leaderboard(
session: AsyncSession,
beatmap_md5: str,
mode: GameMode,
type: LeaderboardType = LeaderboardType.GLOBAL,
mods: list[APIMod] | None = None,
user: User | None = None,
limit: int = 50,
) -> list[Score]:
scores = []
if type == LeaderboardType.GLOBAL:
query = (
select(Score)
.where(
col(Beatmap.beatmap_status).in_(
[
BeatmapRankStatus.RANKED,
BeatmapRankStatus.LOVED,
BeatmapRankStatus.QUALIFIED,
BeatmapRankStatus.APPROVED,
]
),
Score.map_md5 == beatmap_md5,
Score.gamemode == mode,
col(Score.passed).is_(True),
Score.mods == mods if user and user.is_supporter else false(),
)
.limit(limit)
.order_by(
col(Score.total_score).desc(),
)
)
result = await session.exec(query)
scores = list[Score](result.all())
elif type == LeaderboardType.FRIENDS and user and user.is_supporter:
# TODO
...
elif type == LeaderboardType.TEAM and user and user.team_membership:
team_id = user.team_membership.team_id
query = (
select(Score)
.join(Beatmap)
.options(joinedload(Score.user)) # pyright: ignore[reportArgumentType]
.where(
Score.map_md5 == beatmap_md5,
Score.gamemode == mode,
col(Score.passed).is_(True),
col(Score.user.team_membership).is_not(None),
Score.user.team_membership.team_id == team_id, # pyright: ignore[reportOptionalMemberAccess]
Score.mods == mods if user and user.is_supporter else false(),
)
.limit(limit)
.order_by(
col(Score.total_score).desc(),
)
)
result = await session.exec(query)
scores = list[Score](result.all())
if user:
user_score = (
await session.exec(
select(Score).where(
Score.map_md5 == beatmap_md5,
Score.gamemode == mode,
Score.user_id == user.id,
col(Score.passed).is_(True),
)
)
).first()
if user_score and user_score not in scores:
scores.append(user_score)
return scores
async def get_score_position_by_user(
session: AsyncSession,
beatmap_md5: str,
user: User,
mode: GameMode,
type: LeaderboardType = LeaderboardType.GLOBAL,
mods: list[APIMod] | None = None,
) -> int:
where_clause = [
Score.map_md5 == beatmap_md5,
Score.gamemode == mode,
col(Score.passed).is_(True),
col(Beatmap.beatmap_status).in_(
[
BeatmapRankStatus.RANKED,
BeatmapRankStatus.LOVED,
BeatmapRankStatus.QUALIFIED,
BeatmapRankStatus.APPROVED,
]
),
]
if mods and user.is_supporter:
where_clause.append(Score.mods == mods)
else:
where_clause.append(false())
if type == LeaderboardType.FRIENDS and user.is_supporter:
# TODO
...
elif type == LeaderboardType.TEAM and user.team_membership:
team_id = user.team_membership.team_id
where_clause.append(
col(Score.user.team_membership).is_not(None),
)
where_clause.append(
Score.user.team_membership.team_id == team_id, # pyright: ignore[reportOptionalMemberAccess]
)
rownum = (
func.row_number()
.over(
partition_by=Score.map_md5,
order_by=col(Score.total_score).desc(),
)
.label("row_number")
)
subq = select(Score, rownum).join(Beatmap).where(*where_clause).subquery()
stmt = select(subq.c.row_number).where(subq.c.user == user)
result = await session.exec(stmt)
s = result.one_or_none()
return s if s else 0
async def get_score_position_by_id(
session: AsyncSession,
beatmap_md5: str,
score_id: int,
mode: GameMode,
user: User | None = None,
type: LeaderboardType = LeaderboardType.GLOBAL,
mods: list[APIMod] | None = None,
) -> int:
where_clause = [
Score.map_md5 == beatmap_md5,
Score.id == score_id,
Score.gamemode == mode,
col(Score.passed).is_(True),
col(Beatmap.beatmap_status).in_(
[
BeatmapRankStatus.RANKED,
BeatmapRankStatus.LOVED,
BeatmapRankStatus.QUALIFIED,
BeatmapRankStatus.APPROVED,
]
),
]
if mods and user and user.is_supporter:
where_clause.append(Score.mods == mods)
elif mods:
where_clause.append(false())
rownum = (
func.row_number()
.over(
partition_by=[col(Score.user_id), col(Score.map_md5)],
order_by=col(Score.total_score).desc(),
)
.label("rownum")
)
subq = (
select(Score.user_id, Score.id, Score.total_score, rownum)
.join(Beatmap)
.where(*where_clause)
.subquery()
)
best_scores = aliased(subq)
overall_rank = (
func.rank().over(order_by=best_scores.c.total_score.desc()).label("global_rank")
)
final_q = (
select(best_scores.c.id, overall_rank)
.select_from(best_scores)
.where(best_scores.c.rownum == 1)
.subquery()
)
stmt = select(final_q.c.global_rank).where(final_q.c.id == score_id)
result = await session.exec(stmt)
s = result.one_or_none()
return s if s else 0

View File

@@ -0,0 +1,50 @@
from datetime import datetime
from app.models.score import GameMode
from .beatmap import Beatmap
from .user import User
from sqlalchemy import Column, DateTime, Index
from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel
class ScoreTokenBase(SQLModel):
score_id: int | None = Field(sa_column=Column(BigInteger), default=None)
ruleset_id: GameMode
playlist_item_id: int | None = Field(default=None) # playlist
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 ScoreToken(ScoreTokenBase, table=True):
__tablename__ = "score_tokens" # pyright: ignore[reportAssignmentType]
__table_args__ = (Index("idx_user_playlist", "user_id", "playlist_item_id"),)
id: int | None = Field(
default=None,
sa_column=Column(
BigInteger,
primary_key=True,
index=True,
autoincrement=True,
),
)
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
beatmap_id: int = Field(foreign_key="beatmaps.id")
user: "User" = Relationship()
beatmap: "Beatmap" = Relationship()
class ScoreTokenResp(ScoreTokenBase):
id: int
user_id: int
beatmap_id: int
@classmethod
def from_db(cls, obj: ScoreToken) -> "ScoreTokenResp":
return cls.model_validate(obj)

View File

@@ -2,8 +2,7 @@ from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import Column, DateTime
from sqlalchemy.orm import Mapped
from sqlmodel import Field, Relationship, SQLModel
from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel
if TYPE_CHECKING:
from .user import User
@@ -20,18 +19,18 @@ class Team(SQLModel, table=True):
default_factory=datetime.utcnow, sa_column=Column(DateTime)
)
members: Mapped[list["TeamMember"]] = Relationship(back_populates="team")
members: list["TeamMember"] = Relationship(back_populates="team")
class TeamMember(SQLModel, table=True):
__tablename__ = "team_members" # pyright: ignore[reportAssignmentType]
id: int | None = Field(default=None, primary_key=True, index=True)
user_id: int = Field(foreign_key="users.id")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
team_id: int = Field(foreign_key="teams.id")
joined_at: datetime = Field(
default_factory=datetime.utcnow, sa_column=Column(DateTime)
)
user: Mapped["User"] = Relationship(back_populates="team_membership")
team: Mapped["Team"] = Relationship(back_populates="members")
user: "User" = Relationship(back_populates="team_membership")
team: "Team" = Relationship(back_populates="members")

View File

@@ -7,14 +7,16 @@ from .team import TeamMember
from sqlalchemy import DECIMAL, JSON, Column, Date, DateTime, Text
from sqlalchemy.dialects.mysql import VARCHAR
from sqlmodel import BigInteger, Field, Relationship, SQLModel
from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel
class User(SQLModel, table=True):
__tablename__ = "users" # pyright: ignore[reportAssignmentType]
# 主键
id: int = Field(default=None, primary_key=True, index=True, nullable=False)
id: int = Field(
default=None, sa_column=Column(BigInteger, primary_key=True, index=True)
)
# 基本信息(匹配 migrations 中的结构)
name: str = Field(max_length=32, unique=True, index=True) # 用户名
@@ -65,6 +67,10 @@ class User(SQLModel, table=True):
latest_activity = getattr(self, "latest_activity", 0)
return datetime.fromtimestamp(latest_activity) if latest_activity > 0 else None
@property
def is_supporter(self):
return self.lazer_profile.is_supporter if self.lazer_profile else False
# 关联关系
lazer_profile: Optional["LazerUserProfile"] = Relationship(back_populates="user")
lazer_statistics: list["LazerUserStatistics"] = Relationship(back_populates="user")
@@ -76,7 +82,7 @@ class User(SQLModel, table=True):
back_populates="user"
)
statistics: list["LegacyUserStatistics"] = Relationship(back_populates="user")
team_membership: list["TeamMember"] = Relationship(back_populates="user")
team_membership: Optional["TeamMember"] = Relationship(back_populates="user")
daily_challenge_stats: Optional["DailyChallengeStats"] = Relationship(
back_populates="user"
)
@@ -103,7 +109,14 @@ class User(SQLModel, table=True):
class LazerUserProfile(SQLModel, table=True):
__tablename__ = "lazer_user_profiles" # pyright: ignore[reportAssignmentType]
user_id: int = Field(foreign_key="users.id", primary_key=True)
user_id: int = Field(
default=None,
sa_column=Column(
BigInteger,
ForeignKey("users.id"),
primary_key=True,
),
)
# 基本状态字段
is_active: bool = Field(default=True)
@@ -159,7 +172,7 @@ 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")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
section_name: str = Field(sa_column=Column(VARCHAR(50)))
display_order: int | None = Field(default=None)
@@ -176,7 +189,14 @@ class LazerUserProfileSections(SQLModel, table=True):
class LazerUserCountry(SQLModel, table=True):
__tablename__ = "lazer_user_countries" # pyright: ignore[reportAssignmentType]
user_id: int = Field(foreign_key="users.id", primary_key=True)
user_id: int = Field(
default=None,
sa_column=Column(
BigInteger,
ForeignKey("users.id"),
primary_key=True,
),
)
code: str = Field(max_length=2)
name: str = Field(max_length=100)
@@ -191,7 +211,14 @@ class LazerUserCountry(SQLModel, table=True):
class LazerUserKudosu(SQLModel, table=True):
__tablename__ = "lazer_user_kudosu" # pyright: ignore[reportAssignmentType]
user_id: int = Field(foreign_key="users.id", primary_key=True)
user_id: int = Field(
default=None,
sa_column=Column(
BigInteger,
ForeignKey("users.id"),
primary_key=True,
),
)
available: int = Field(default=0)
total: int = Field(default=0)
@@ -206,7 +233,14 @@ class LazerUserKudosu(SQLModel, table=True):
class LazerUserCounts(SQLModel, table=True):
__tablename__ = "lazer_user_counts" # pyright: ignore[reportAssignmentType]
user_id: int = Field(foreign_key="users.id", primary_key=True)
user_id: int = Field(
default=None,
sa_column=Column(
BigInteger,
ForeignKey("users.id"),
primary_key=True,
),
)
# 统计计数字段
beatmap_playcounts_count: int = Field(default=0)
@@ -241,7 +275,14 @@ class LazerUserCounts(SQLModel, table=True):
class LazerUserStatistics(SQLModel, table=True):
__tablename__ = "lazer_user_statistics" # pyright: ignore[reportAssignmentType]
user_id: int = Field(foreign_key="users.id", primary_key=True)
user_id: int = Field(
default=None,
sa_column=Column(
BigInteger,
ForeignKey("users.id"),
primary_key=True,
),
)
mode: str = Field(default="osu", max_length=10, primary_key=True)
# 基本命中统计
@@ -302,7 +343,7 @@ 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")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
tournament_id: int
image_url: str = Field(sa_column=Column(VARCHAR(500)))
is_active: bool | None = Field(default=None)
@@ -315,7 +356,7 @@ 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")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
achievement_id: int
achieved_at: datetime = Field(
default_factory=datetime.utcnow, sa_column=Column(DateTime)
@@ -328,7 +369,7 @@ 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")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("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))
@@ -349,7 +390,7 @@ 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")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
start_date: datetime = Field(sa_column=Column(Date))
play_count: int = Field(default=0)
@@ -367,7 +408,7 @@ 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")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
username: str = Field(max_length=32)
changed_at: datetime = Field(sa_column=Column(DateTime))
@@ -385,7 +426,7 @@ 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")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
start_date: datetime = Field(sa_column=Column(Date))
count: int = Field(default=0)
@@ -410,7 +451,9 @@ 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)
user_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("users.id"), unique=True)
)
daily_streak_best: int = Field(default=0)
daily_streak_current: int = Field(default=0)
@@ -431,7 +474,7 @@ 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")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
mode: str = Field(max_length=10)
rank_data: list = Field(sa_column=Column(JSON)) # Array of ranks
date_recorded: datetime = Field(
@@ -445,7 +488,7 @@ 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")
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id")))
filename: str = Field(max_length=255)
original_filename: str = Field(max_length=255)
file_size: int