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 .achievement import UserAchievement, UserAchievementResp
from .daily_challenge import DailyChallengeStats, DailyChallengeStatsResp from .daily_challenge import DailyChallengeStats, DailyChallengeStatsResp
from .monthly_playcounts import MonthlyPlaycounts, MonthlyPlaycountsResp
from .statistics import UserStatistics, UserStatisticsResp from .statistics import UserStatistics, UserStatisticsResp
from .team import Team, TeamMember from .team import Team, TeamMember
from .user_account_history import UserAccountHistory, UserAccountHistoryResp from .user_account_history import UserAccountHistory, UserAccountHistoryResp
@@ -141,6 +142,7 @@ class User(UserBase, table=True):
daily_challenge_stats: DailyChallengeStats | None = Relationship( daily_challenge_stats: DailyChallengeStats | None = Relationship(
back_populates="user" back_populates="user"
) )
monthly_playcounts: list[MonthlyPlaycounts] = Relationship(back_populates="user")
email: str = Field(max_length=254, unique=True, index=True, exclude=True) email: str = Field(max_length=254, unique=True, index=True, exclude=True)
priv: int = Field(default=1, exclude=True) priv: int = Field(default=1, exclude=True)
@@ -160,6 +162,7 @@ class User(UserBase, table=True):
selectinload(cls.achievement), # pyright: ignore[reportArgumentType] selectinload(cls.achievement), # pyright: ignore[reportArgumentType]
joinedload(cls.team_membership).joinedload(TeamMember.team), # pyright: ignore[reportArgumentType] joinedload(cls.team_membership).joinedload(TeamMember.team), # pyright: ignore[reportArgumentType]
joinedload(cls.daily_challenge_stats), # 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] = [] account_history: list[UserAccountHistoryResp] = []
active_tournament_banners: list[dict] = [] # TODO active_tournament_banners: list[dict] = [] # TODO
kudosu: Kudosu = Field(default_factory=lambda: Kudosu(available=0, total=0)) # 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 unread_pm_count: int = 0 # TODO
rank_history: RankHistory | None = None # TODO rank_history: RankHistory | None = None # TODO
rank_highest: RankHighest | None = None # TODO rank_highest: RankHighest | None = None # TODO
@@ -196,7 +199,7 @@ class UserResp(UserBase):
cover_url: str = "" # deprecated cover_url: str = "" # deprecated
team: Team | None = None team: Team | None = None
session_verified: bool = True 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 # 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 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: if "achievements" in include:
u.user_achievements = [ u.user_achievements = [
UserAchievementResp.from_db(ua) for ua in obj.achievement UserAchievementResp.from_db(ua) for ua in obj.achievement
] ]
return u 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 import asyncio
from collections.abc import Sequence from collections.abc import Sequence
from datetime import UTC, datetime from datetime import UTC, date, datetime
import math import math
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -30,6 +30,7 @@ from .beatmap import Beatmap, BeatmapResp
from .beatmapset import Beatmapset, BeatmapsetResp from .beatmapset import Beatmapset, BeatmapsetResp
from .best_score import BestScore from .best_score import BestScore
from .lazer_user import User, UserResp from .lazer_user import User, UserResp
from .monthly_playcounts import MonthlyPlaycounts
from .score_token import ScoreToken from .score_token import ScoreToken
from redis import Redis from redis import Redis
@@ -501,8 +502,22 @@ async def process_user(
previous_score_best = await get_user_best_score_in_beatmap( previous_score_best = await get_user_best_score_in_beatmap(
session, score.beatmap_id, user.id, score.gamemode session, score.beatmap_id, user.id, score.gamemode
) )
statistics = None
add_to_db = False 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: for i in user.statistics:
if i.mode == score.gamemode.value: if i.mode == score.gamemode.value:
statistics = i statistics = i
@@ -547,6 +562,7 @@ async def process_user(
statistics.level_current = calculate_score_to_level(statistics.ranked_score) statistics.level_current = calculate_score_to_level(statistics.ranked_score)
statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo) statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo)
statistics.play_count += 1 statistics.play_count += 1
mouthly_playcount.playcount += 1
statistics.play_time += int((score.ended_at - score.started_at).total_seconds()) statistics.play_time += int((score.ended_at - score.started_at).total_seconds())
statistics.count_100 += score.n100 + score.nkatu statistics.count_100 += score.n100 + score.nkatu
statistics.count_300 += score.n300 + score.ngeki statistics.count_300 += score.n300 + score.ngeki
@@ -569,9 +585,8 @@ async def process_user(
acc_sum = clamp(acc_sum, 0.0, 100.0) acc_sum = clamp(acc_sum, 0.0, 100.0)
statistics.pp = pp_sum statistics.pp = pp_sum
statistics.hit_accuracy = acc_sum statistics.hit_accuracy = acc_sum
if add_to_db: if add_to_db:
session.add(statistics) session.add(mouthly_playcount)
await session.commit() await session.commit()
await session.refresh(user) await session.refresh(user)

View File

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

View File

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