feat(team): add playmode, description, website and statistics

This commit is contained in:
MingxuanGame
2025-10-08 05:46:17 +00:00
parent e2f3c5099f
commit 45ed9e51a9
4 changed files with 176 additions and 10 deletions

View File

@@ -55,7 +55,7 @@ from .statistics import (
UserStatistics, UserStatistics,
UserStatisticsResp, UserStatisticsResp,
) )
from .team import Team, TeamMember, TeamRequest from .team import Team, TeamMember, TeamRequest, TeamResp
from .total_score_best_scores import TotalScoreBestScore from .total_score_best_scores import TotalScoreBestScore
from .user import ( from .user import (
MeResp, MeResp,
@@ -131,6 +131,7 @@ __all__ = [
"Team", "Team",
"TeamMember", "TeamMember",
"TeamRequest", "TeamRequest",
"TeamResp",
"TotalScoreBestScore", "TotalScoreBestScore",
"TotpKeys", "TotpKeys",
"TrustedDevice", "TrustedDevice",

View File

@@ -2,18 +2,18 @@ from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.models.score import GameMode
from app.utils import utcnow from app.utils import utcnow
from sqlalchemy import Column, DateTime from sqlalchemy import Column, DateTime
from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel, Text, col, func, select
from sqlmodel.ext.asyncio.session import AsyncSession
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User from .user import User
class Team(SQLModel, UTCBaseModel, table=True): class TeamBase(SQLModel, UTCBaseModel):
__tablename__: str = "teams"
id: int = Field(default=None, primary_key=True, index=True) id: int = Field(default=None, primary_key=True, index=True)
name: str = Field(max_length=100) name: str = Field(max_length=100)
short_name: str = Field(max_length=10) short_name: str = Field(max_length=10)
@@ -21,11 +21,107 @@ class Team(SQLModel, UTCBaseModel, table=True):
cover_url: str | None = Field(default=None) cover_url: str | None = Field(default=None)
created_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime)) created_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime))
leader_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"))) leader_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id")))
description: str | None = Field(default=None, sa_column=Column(Text))
playmode: GameMode = Field(default=GameMode.OSU)
website: str | None = Field(default=None, sa_column=Column(Text))
class Team(TeamBase, table=True):
__tablename__: str = "teams"
leader: "User" = Relationship() leader: "User" = Relationship()
members: list["TeamMember"] = Relationship(back_populates="team") members: list["TeamMember"] = Relationship(back_populates="team")
class TeamResp(TeamBase):
rank: int = 0
pp: float = 0.0
ranked_score: int = 0
total_play_count: int = 0
member_count: int = 0
@classmethod
async def from_db(cls, team: Team, session: AsyncSession, gamemode: GameMode | None = None) -> "TeamResp":
from .statistics import UserStatistics
from .user import User
playmode = gamemode or team.playmode
pp_expr = func.coalesce(func.sum(col(UserStatistics.pp)), 0.0)
ranked_score_expr = func.coalesce(func.sum(col(UserStatistics.ranked_score)), 0)
play_count_expr = func.coalesce(func.sum(col(UserStatistics.play_count)), 0)
member_count_expr = func.count(func.distinct(col(UserStatistics.user_id)))
team_stats_stmt = (
select(pp_expr, ranked_score_expr, play_count_expr, member_count_expr)
.select_from(UserStatistics)
.join(TeamMember, col(TeamMember.user_id) == col(UserStatistics.user_id))
.join(User, col(User.id) == col(UserStatistics.user_id))
.join(Team, col(Team.id) == col(TeamMember.team_id))
.where(
col(Team.id) == team.id,
col(Team.playmode) == playmode,
col(UserStatistics.mode) == playmode,
col(UserStatistics.pp) > 0,
col(UserStatistics.is_ranked).is_(True),
~User.is_restricted_query(col(UserStatistics.user_id)),
)
)
team_stats_result = await session.exec(team_stats_stmt)
stats_row = team_stats_result.one_or_none()
if stats_row is None:
total_pp = 0.0
total_ranked_score = 0
total_play_count = 0
active_member_count = 0
else:
total_pp, total_ranked_score, total_play_count, active_member_count = stats_row
total_pp = float(total_pp or 0.0)
total_ranked_score = int(total_ranked_score or 0)
total_play_count = int(total_play_count or 0)
active_member_count = int(active_member_count or 0)
total_pp_ranking_expr = func.coalesce(func.sum(col(UserStatistics.pp)), 0.0)
ranking_stmt = (
select(Team.id, total_pp_ranking_expr)
.select_from(Team)
.join(TeamMember, col(TeamMember.team_id) == col(Team.id))
.join(UserStatistics, col(UserStatistics.user_id) == col(TeamMember.user_id))
.join(User, col(User.id) == col(TeamMember.user_id))
.where(
col(Team.playmode) == playmode,
col(UserStatistics.mode) == playmode,
col(UserStatistics.pp) > 0,
col(UserStatistics.is_ranked).is_(True),
~User.is_restricted_query(col(UserStatistics.user_id)),
)
.group_by(col(Team.id))
.order_by(total_pp_ranking_expr.desc())
)
ranking_result = await session.exec(ranking_stmt)
ranking_rows = ranking_result.all()
rank = 0
for index, (team_id, _) in enumerate(ranking_rows, start=1):
if team_id == team.id:
rank = index
break
data = team.model_dump()
data.update(
{
"pp": total_pp,
"ranked_score": total_ranked_score,
"total_play_count": total_play_count,
"member_count": active_member_count,
"rank": rank,
}
)
return cls.model_validate(data)
class TeamMember(SQLModel, UTCBaseModel, table=True): class TeamMember(SQLModel, UTCBaseModel, table=True):
__tablename__: str = "team_members" __tablename__: str = "team_members"

View File

@@ -1,7 +1,7 @@
import hashlib import hashlib
from typing import Annotated from typing import Annotated
from app.database.team import Team, TeamMember, TeamRequest from app.database.team import Team, TeamMember, TeamRequest, TeamResp
from app.database.user import BASE_INCLUDES, User, UserResp from app.database.user import BASE_INCLUDES, User, UserResp
from app.dependencies.database import Database, Redis from app.dependencies.database import Database, Redis
from app.dependencies.storage import StorageService from app.dependencies.storage import StorageService
@@ -11,13 +11,14 @@ from app.models.notification import (
TeamApplicationReject, TeamApplicationReject,
TeamApplicationStore, TeamApplicationStore,
) )
from app.models.score import GameMode
from app.router.notification import server from app.router.notification import server
from app.service.ranking_cache_service import get_ranking_cache_service from app.service.ranking_cache_service import get_ranking_cache_service
from app.utils import check_image, utcnow from app.utils import check_image, utcnow
from .router import router from .router import router
from fastapi import File, Form, HTTPException, Path, Request from fastapi import File, Form, HTTPException, Path, Query, Request
from pydantic import BaseModel from pydantic import BaseModel
from sqlmodel import col, exists, select from sqlmodel import col, exists, select
@@ -32,6 +33,9 @@ async def create_team(
name: Annotated[str, Form(max_length=100, description="战队名称")], name: Annotated[str, Form(max_length=100, description="战队名称")],
short_name: Annotated[str, Form(max_length=10, description="战队缩写")], short_name: Annotated[str, Form(max_length=10, description="战队缩写")],
redis: Redis, redis: Redis,
playmode: Annotated[GameMode, Form(description="战队游戏模式")] = GameMode.OSU,
description: Annotated[str | None, Form(description="战队简介")] = None,
website: Annotated[str | None, Form(description="战队网址")] = None,
): ):
"""创建战队。 """创建战队。
@@ -55,8 +59,19 @@ async def create_team(
flag_format = check_image(flag, 2 * 1024 * 1024, 240, 120) flag_format = check_image(flag, 2 * 1024 * 1024, 240, 120)
cover_format = check_image(cover, 10 * 1024 * 1024, 3000, 2000) cover_format = check_image(cover, 10 * 1024 * 1024, 3000, 2000)
if website and not (website.startswith("http://") or website.startswith("https://")):
website = "https://" + website
now = utcnow() now = utcnow()
team = Team(name=name, short_name=short_name, leader_id=user_id, created_at=now) team = Team(
name=name,
short_name=short_name,
leader_id=user_id,
created_at=now,
playmode=playmode,
description=description,
website=website,
)
session.add(team) session.add(team)
await session.commit() await session.commit()
await session.refresh(team) await session.refresh(team)
@@ -95,6 +110,9 @@ async def update_team(
name: Annotated[str | None, Form(max_length=100, description="战队名称")] = None, name: Annotated[str | None, Form(max_length=100, description="战队名称")] = None,
short_name: Annotated[str | None, Form(max_length=10, description="战队缩写")] = None, short_name: Annotated[str | None, Form(max_length=10, description="战队缩写")] = None,
leader_id: Annotated[int | None, Form(description="战队队长 ID")] = None, leader_id: Annotated[int | None, Form(description="战队队长 ID")] = None,
playmode: Annotated[GameMode, Form(description="战队游戏模式")] = GameMode.OSU,
description: Annotated[str | None, Form(description="战队简介")] = None,
website: Annotated[str | None, Form(description="战队网址")] = None,
): ):
"""修改战队。 """修改战队。
@@ -122,6 +140,13 @@ async def update_team(
else: else:
team.short_name = short_name team.short_name = short_name
team.playmode = playmode or team.playmode
team.description = description
if website is not None:
if website and not (website.startswith("http://") or website.startswith("https://")):
website = "https://" + website
team.website = website
if flag: if flag:
format_ = check_image(flag, 2 * 1024 * 1024, 240, 120) format_ = check_image(flag, 2 * 1024 * 1024, 240, 120)
@@ -190,7 +215,7 @@ async def delete_team(
class TeamQueryResp(BaseModel): class TeamQueryResp(BaseModel):
team: Team team: TeamResp
members: list[UserResp] members: list[UserResp]
@@ -198,6 +223,7 @@ class TeamQueryResp(BaseModel):
async def get_team( async def get_team(
session: Database, session: Database,
team_id: Annotated[int, Path(..., description="战队 ID")], team_id: Annotated[int, Path(..., description="战队 ID")],
gamemode: Annotated[GameMode | None, Query(description="游戏模式")] = None,
): ):
members = ( members = (
await session.exec( await session.exec(
@@ -208,7 +234,7 @@ async def get_team(
) )
).all() ).all()
return TeamQueryResp( return TeamQueryResp(
team=members[0].team, team=await TeamResp.from_db(members[0].team, session, gamemode),
members=[await UserResp.from_db(m.user, session, include=BASE_INCLUDES) for m in members], members=[await UserResp.from_db(m.user, session, include=BASE_INCLUDES) for m in members],
) )

View File

@@ -0,0 +1,43 @@
"""team: add description, playmode and website
Revision ID: 48fb754416de
Revises: fa4952dc70df
Create Date: 2025-10-08 05:26:13.832105
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "48fb754416de"
down_revision: str | Sequence[str] | None = "fa4952dc70df"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("teams", sa.Column("description", sa.Text(), nullable=True))
op.add_column(
"teams",
sa.Column(
"playmode",
sa.Enum("OSU", "TAIKO", "FRUITS", "MANIA", "OSURX", "OSUAP", "TAIKORX", "FRUITSRX", name="gamemode"),
nullable=False,
),
)
op.add_column("teams", sa.Column("website", sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("teams", "website")
op.drop_column("teams", "playmode")
op.drop_column("teams", "description")
# ### end Alembic commands ###