feat(team): add playmode, description, website and statistics
This commit is contained in:
@@ -55,7 +55,7 @@ from .statistics import (
|
||||
UserStatistics,
|
||||
UserStatisticsResp,
|
||||
)
|
||||
from .team import Team, TeamMember, TeamRequest
|
||||
from .team import Team, TeamMember, TeamRequest, TeamResp
|
||||
from .total_score_best_scores import TotalScoreBestScore
|
||||
from .user import (
|
||||
MeResp,
|
||||
@@ -131,6 +131,7 @@ __all__ = [
|
||||
"Team",
|
||||
"TeamMember",
|
||||
"TeamRequest",
|
||||
"TeamResp",
|
||||
"TotalScoreBestScore",
|
||||
"TotpKeys",
|
||||
"TrustedDevice",
|
||||
|
||||
@@ -2,18 +2,18 @@ from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from app.models.model import UTCBaseModel
|
||||
from app.models.score import GameMode
|
||||
from app.utils import utcnow
|
||||
|
||||
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:
|
||||
from .user import User
|
||||
|
||||
|
||||
class Team(SQLModel, UTCBaseModel, table=True):
|
||||
__tablename__: str = "teams"
|
||||
|
||||
class TeamBase(SQLModel, UTCBaseModel):
|
||||
id: int = Field(default=None, primary_key=True, index=True)
|
||||
name: str = Field(max_length=100)
|
||||
short_name: str = Field(max_length=10)
|
||||
@@ -21,11 +21,107 @@ class Team(SQLModel, UTCBaseModel, table=True):
|
||||
cover_url: str | None = Field(default=None)
|
||||
created_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime))
|
||||
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()
|
||||
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):
|
||||
__tablename__: str = "team_members"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import hashlib
|
||||
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.dependencies.database import Database, Redis
|
||||
from app.dependencies.storage import StorageService
|
||||
@@ -11,13 +11,14 @@ from app.models.notification import (
|
||||
TeamApplicationReject,
|
||||
TeamApplicationStore,
|
||||
)
|
||||
from app.models.score import GameMode
|
||||
from app.router.notification import server
|
||||
from app.service.ranking_cache_service import get_ranking_cache_service
|
||||
from app.utils import check_image, utcnow
|
||||
|
||||
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 sqlmodel import col, exists, select
|
||||
|
||||
@@ -32,6 +33,9 @@ async def create_team(
|
||||
name: Annotated[str, Form(max_length=100, description="战队名称")],
|
||||
short_name: Annotated[str, Form(max_length=10, description="战队缩写")],
|
||||
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)
|
||||
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()
|
||||
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)
|
||||
await session.commit()
|
||||
await session.refresh(team)
|
||||
@@ -95,6 +110,9 @@ async def update_team(
|
||||
name: Annotated[str | None, Form(max_length=100, description="战队名称")] = None,
|
||||
short_name: Annotated[str | None, Form(max_length=10, description="战队缩写")] = 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:
|
||||
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:
|
||||
format_ = check_image(flag, 2 * 1024 * 1024, 240, 120)
|
||||
|
||||
@@ -190,7 +215,7 @@ async def delete_team(
|
||||
|
||||
|
||||
class TeamQueryResp(BaseModel):
|
||||
team: Team
|
||||
team: TeamResp
|
||||
members: list[UserResp]
|
||||
|
||||
|
||||
@@ -198,6 +223,7 @@ class TeamQueryResp(BaseModel):
|
||||
async def get_team(
|
||||
session: Database,
|
||||
team_id: Annotated[int, Path(..., description="战队 ID")],
|
||||
gamemode: Annotated[GameMode | None, Query(description="游戏模式")] = None,
|
||||
):
|
||||
members = (
|
||||
await session.exec(
|
||||
@@ -208,7 +234,7 @@ async def get_team(
|
||||
)
|
||||
).all()
|
||||
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],
|
||||
)
|
||||
|
||||
|
||||
@@ -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 ###
|
||||
Reference in New Issue
Block a user