feat(team): add playmode, description, website and statistics
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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