feat(user): add monthly playcounts

This commit is contained in:
MingxuanGame
2025-07-31 02:13:18 +00:00
parent 9ce99398ab
commit a15c3cef04
5 changed files with 99 additions and 26 deletions

View File

@@ -7,6 +7,7 @@ from app.models.user import Country, Page, RankHistory
from .achievement import UserAchievement, UserAchievementResp
from .daily_challenge import DailyChallengeStats, DailyChallengeStatsResp
from .monthly_playcounts import MonthlyPlaycounts, MonthlyPlaycountsResp
from .statistics import UserStatistics, UserStatisticsResp
from .team import Team, TeamMember
from .user_account_history import UserAccountHistory, UserAccountHistoryResp
@@ -141,6 +142,7 @@ class User(UserBase, table=True):
daily_challenge_stats: DailyChallengeStats | None = Relationship(
back_populates="user"
)
monthly_playcounts: list[MonthlyPlaycounts] = Relationship(back_populates="user")
email: str = Field(max_length=254, unique=True, index=True, exclude=True)
priv: int = Field(default=1, exclude=True)
@@ -160,6 +162,7 @@ class User(UserBase, table=True):
selectinload(cls.achievement), # pyright: ignore[reportArgumentType]
joinedload(cls.team_membership).joinedload(TeamMember.team), # pyright: ignore[reportArgumentType]
joinedload(cls.daily_challenge_stats), # pyright: ignore[reportArgumentType]
selectinload(cls.monthly_playcounts), # pyright: ignore[reportArgumentType]
)
@@ -186,7 +189,7 @@ class UserResp(UserBase):
account_history: list[UserAccountHistoryResp] = []
active_tournament_banners: list[dict] = [] # TODO
kudosu: Kudosu = Field(default_factory=lambda: Kudosu(available=0, total=0)) # TODO
monthly_playcounts: list = Field(default_factory=list) # TODO
monthly_playcounts: list[MonthlyPlaycountsResp] = Field(default_factory=list)
unread_pm_count: int = 0 # TODO
rank_history: RankHistory | None = None # TODO
rank_highest: RankHighest | None = None # TODO
@@ -196,7 +199,7 @@ class UserResp(UserBase):
cover_url: str = "" # deprecated
team: Team | None = None
session_verified: bool = True
daily_challenge_user_stats: DailyChallengeStatsResp | None = None # TODO
daily_challenge_user_stats: DailyChallengeStatsResp | None = None
# TODO: monthly_playcounts, unread_pm_count rank_history, user_preferences
@@ -292,9 +295,36 @@ class UserResp(UserBase):
i.mode.value: UserStatisticsResp.from_db(i) for i in obj.statistics
}
if "monthly_playcounts" in include:
u.monthly_playcounts = [
MonthlyPlaycountsResp.from_db(pc) for pc in obj.monthly_playcounts
]
if "achievements" in include:
u.user_achievements = [
UserAchievementResp.from_db(ua) for ua in obj.achievement
]
return u
ALL_INCLUDED = [
"friends",
"team",
"account_history",
"daily_challenge_user_stats",
"statistics",
"statistics_rulesets",
"achievements",
"monthly_playcounts",
]
SEARCH_INCLUDED = [
"team",
"daily_challenge_user_stats",
"statistics",
"statistics_rulesets",
"achievements",
"monthly_playcounts",
]

View File

@@ -0,0 +1,43 @@
from datetime import date
from typing import TYPE_CHECKING
from sqlmodel import (
BigInteger,
Column,
Field,
ForeignKey,
Relationship,
SQLModel,
)
if TYPE_CHECKING:
from .lazer_user import User
class MonthlyPlaycounts(SQLModel, table=True):
__tablename__ = "monthly_playcounts" # pyright: ignore[reportAssignmentType]
id: int | None = Field(
default=None,
sa_column=Column(BigInteger, primary_key=True, autoincrement=True),
)
user_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)
)
year: int = Field(index=True)
month: int = Field(index=True)
playcount: int = Field(default=0)
user: "User" = Relationship(back_populates="monthly_playcounts")
class MonthlyPlaycountsResp(SQLModel):
start_date: date
count: int
@classmethod
def from_db(cls, db_model: MonthlyPlaycounts) -> "MonthlyPlaycountsResp":
return cls(
start_date=date(db_model.year, db_model.month, 1),
count=db_model.playcount,
)

View File

@@ -1,6 +1,6 @@
import asyncio
from collections.abc import Sequence
from datetime import UTC, datetime
from datetime import UTC, date, datetime
import math
from typing import TYPE_CHECKING
@@ -30,6 +30,7 @@ from .beatmap import Beatmap, BeatmapResp
from .beatmapset import Beatmapset, BeatmapsetResp
from .best_score import BestScore
from .lazer_user import User, UserResp
from .monthly_playcounts import MonthlyPlaycounts
from .score_token import ScoreToken
from redis import Redis
@@ -501,8 +502,22 @@ async def process_user(
previous_score_best = await get_user_best_score_in_beatmap(
session, score.beatmap_id, user.id, score.gamemode
)
statistics = None
add_to_db = False
mouthly_playcount = (
await session.exec(
select(MonthlyPlaycounts).where(
MonthlyPlaycounts.user_id == user.id,
MonthlyPlaycounts.year == date.today().year,
MonthlyPlaycounts.month == date.today().month,
)
)
).first()
if mouthly_playcount is None:
mouthly_playcount = MonthlyPlaycounts(
user_id=user.id, year=date.today().year, month=date.today().month
)
add_to_db = True
statistics = None
for i in user.statistics:
if i.mode == score.gamemode.value:
statistics = i
@@ -547,6 +562,7 @@ async def process_user(
statistics.level_current = calculate_score_to_level(statistics.ranked_score)
statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo)
statistics.play_count += 1
mouthly_playcount.playcount += 1
statistics.play_time += int((score.ended_at - score.started_at).total_seconds())
statistics.count_100 += score.n100 + score.nkatu
statistics.count_300 += score.n300 + score.ngeki
@@ -569,9 +585,8 @@ async def process_user(
acc_sum = clamp(acc_sum, 0.0, 100.0)
statistics.pp = pp_sum
statistics.hit_accuracy = acc_sum
if add_to_db:
session.add(statistics)
session.add(mouthly_playcount)
await session.commit()
await session.refresh(user)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from app.database import User, UserResp
from app.database.lazer_user import ALL_INCLUDED
from app.dependencies import get_current_user
from app.dependencies.database import get_db
from app.models.score import GameMode
@@ -21,14 +22,6 @@ async def get_user_info_default(
return await UserResp.from_db(
current_user,
session,
[
"friends",
"team",
"account_history",
"daily_challenge_user_stats",
"statistics",
"statistics_rulesets",
"achievements",
],
ALL_INCLUDED,
ruleset,
)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from app.database import User, UserResp
from app.database.lazer_user import SEARCH_INCLUDED
from app.dependencies.database import get_db
from app.models.score import GameMode
@@ -17,15 +18,6 @@ class BatchUserResponse(BaseModel):
users: list[UserResp]
SEARCH_INCLUDE = [
"team",
"daily_challenge_user_stats",
"statistics",
"statistics_rulesets",
"achievements",
]
@router.get("/users", response_model=BatchUserResponse)
@router.get("/users/lookup", response_model=BatchUserResponse)
@router.get("/users/lookup/", response_model=BatchUserResponse)
@@ -54,7 +46,7 @@ async def get_users(
await UserResp.from_db(
searched_user,
session,
include=SEARCH_INCLUDE,
include=SEARCH_INCLUDED,
)
for searched_user in searched_users
]
@@ -85,6 +77,6 @@ async def get_user_info(
return await UserResp.from_db(
searched_user,
session,
include=SEARCH_INCLUDE,
include=SEARCH_INCLUDED,
ruleset=ruleset,
)