From 3c5336ed6199790e4f7698cdfe800782fef388e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=99=8B=E7=91=AD?= Date: Thu, 28 Aug 2025 20:55:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B0=B1=E9=9D=A2=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=89=93=E5=88=86=EF=BC=88=E8=AF=84=E5=88=86=EF=BC=89?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(database): 添加 beatmap_ratings 表用于用户评分 - 新增 BeatmapRating 模型类,用于表示 beatmap_ratings 表 - 该表包含 id、beatmapset_id、user_id 字段 - 建立与 Beatmapset 和 User 表的关联关系 * feat(beatmapset_ratings): 添加判断用户是否可以对谱面集进行评分的接口 - 新增 /beatmapsets/{beatmapset_id}/can_rate 接口 - 判断用户是否能对谱面集进行过评分,返回True/False * feat(ratings): 添加为谱面评分的接口 - 新增 POST /beatmapsets/{beatmapset_id} 路由,用于用户给谱面集评分 - 实现谱面集评分的添加和更新逻辑 - 在 BeatmapRating 模型中添加 rating 字段 (漏了最重要的,我真tm丢脸) * chore(database): 添加alembic数据库迁移脚本 * fix(ratings): 更改上传谱面打分的api路径,防止冲突 * fix(ratings): add changes from pr review * refactor(ratings): remove swears from code * feat(ratings): 从beatmapset中移除ratings字段,并改由从beatmap_ratings表中直接计算评分 * chore(deps): 添加 git 包并更新依赖项 - 在 builder 阶段添加了 git 包的安装 * chore(database): 更新数据库连接地址并删除意外的迁移脚本 - 将 Alembic 配置文件中的数据库连接地址从本地地址改为 Docker Compose 中的 mysql 服务地址 - 删除了 migrations/versions 目录下的 dba1f8d9992e_add_beatmap_ratings_table.py 文件 * chore(database): generate alembic script for beatmap ratings * fix(ratings): apply changes suggested in review - revert changes to alembic.ini - add name to apis - modify migration scripts * chore: format server.py using ruff - who forgot to do this? * fix(migrate): fix remove achievement index * perf(rating): optimize SQL query * fix(rating): ensure user can rate beatmapset * fix(rating): add boundary check * chore(project): remove submodule --------- Co-authored-by: MingxuanGame --- Dockerfile | 2 +- app/database/__init__.py | 2 + app/database/beatmapset.py | 19 ++++- app/database/beatmapset_ratings.py | 20 +++++ app/router/notification/server.py | 4 +- app/router/private/__init__.py | 2 +- app/router/private/beatmapset_ratings.py | 77 +++++++++++++++++++ .../24a32515292d_add_beatmap_ratings.py | 51 ++++++++++++ 8 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 app/database/beatmapset_ratings.py create mode 100644 app/router/private/beatmapset_ratings.py create mode 100644 migrations/versions/24a32515292d_add_beatmap_ratings.py diff --git a/Dockerfile b/Dockerfile index 65e030d..5ec1525 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder WORKDIR /app RUN apt-get update \ - && apt-get install -y gcc pkg-config default-libmysqlclient-dev \ + && apt-get install -y git gcc pkg-config default-libmysqlclient-dev \ && rm -rf /var/lib/apt/lists/* \ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y diff --git a/app/database/__init__.py b/app/database/__init__.py index d64f1c9..2d5f155 100644 --- a/app/database/__init__.py +++ b/app/database/__init__.py @@ -9,6 +9,7 @@ from .beatmapset import ( Beatmapset, BeatmapsetResp, ) +from .beatmapset_ratings import BeatmapRating from .best_score import BestScore from .chat import ( ChannelType, @@ -71,6 +72,7 @@ __all__ = [ "Beatmap", "BeatmapPlaycounts", "BeatmapPlaycountsResp", + "BeatmapRating", "BeatmapResp", "Beatmapset", "BeatmapsetResp", diff --git a/app/database/beatmapset.py b/app/database/beatmapset.py index 32549f8..ded31ed 100644 --- a/app/database/beatmapset.py +++ b/app/database/beatmapset.py @@ -93,7 +93,6 @@ class BeatmapsetBase(SQLModel): # TODO: events: Optional[list[BeatmapsetEvent]] = None pack_tags: list[str] = Field(default=[], sa_column=Column(JSON)) - ratings: list[int] | None = Field(default=None, sa_column=Column(JSON)) # TODO: related_users: Optional[list[User]] = None # TODO: user: Optional[User] = Field(default=None) track_id: int | None = Field(default=None, index=True) # feature artist? @@ -259,9 +258,21 @@ class BeatmapsetResp(BeatmapsetBase): **beatmapset.model_dump(), } - # 确保 ratings 字段不为 null,避免客户端崩溃 - if update.get("ratings") is None: - update["ratings"] = [] + if session is not None: + # 从数据库读取对应谱面集的评分 + from .beatmapset_ratings import BeatmapRating + + beatmapset_all_ratings = ( + await session.exec(select(BeatmapRating).where(BeatmapRating.beatmapset_id == beatmapset.id)) + ).all() + ratings_list = [0] * 11 + for rating in beatmapset_all_ratings: + ratings_list[rating.rating] += 1 + update["ratings"] = ratings_list + else: + # 返回非空值避免客户端崩溃 + if update.get("ratings") is None: + update["ratings"] = [] beatmap_status = beatmapset.beatmap_status if settings.enable_all_beatmap_leaderboard and not beatmap_status.has_leaderboard(): diff --git a/app/database/beatmapset_ratings.py b/app/database/beatmapset_ratings.py new file mode 100644 index 0000000..07627b4 --- /dev/null +++ b/app/database/beatmapset_ratings.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from app.database.beatmapset import Beatmapset +from app.database.lazer_user import User + +from sqlmodel import BigInteger, Column, Field, ForeignKey, Relationship, SQLModel + + +class BeatmapRating(SQLModel, table=True): + __tablename__: str = "beatmap_ratings" + id: int | None = Field( + default=None, + sa_column=Column(BigInteger, primary_key=True, autoincrement=True), + ) + beatmapset_id: int = Field(foreign_key="beatmapsets.id", index=True) + user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)) + rating: int + + beatmapset: Beatmapset = Relationship() + user: User = Relationship() diff --git a/app/router/notification/server.py b/app/router/notification/server.py index b5f24c2..bec5b04 100644 --- a/app/router/notification/server.py +++ b/app/router/notification/server.py @@ -306,11 +306,11 @@ async def chat_websocket( auth_token = authorization[7:] else: auth_token = authorization - + if not auth_token: await websocket.close(code=1008, reason="Missing authentication token") return - + if (user := await get_current_user(session, SecurityScopes(scopes=["chat.read"]), token_pw=auth_token)) is None: await websocket.close(code=1008, reason="Invalid or expired token") return diff --git a/app/router/private/__init__.py b/app/router/private/__init__.py index dc291c6..b17c2f7 100644 --- a/app/router/private/__init__.py +++ b/app/router/private/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from . import avatar, cover, oauth, relationship, team, username # noqa: F401 +from . import avatar, beatmapset_ratings, cover, oauth, relationship, team, username # noqa: F401 from .router import router as private_router __all__ = [ diff --git a/app/router/private/beatmapset_ratings.py b/app/router/private/beatmapset_ratings.py new file mode 100644 index 0000000..2711d8a --- /dev/null +++ b/app/router/private/beatmapset_ratings.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from app.database.beatmap import Beatmap +from app.database.beatmapset import Beatmapset +from app.database.beatmapset_ratings import BeatmapRating +from app.database.lazer_user import User +from app.database.score import Score +from app.dependencies.database import Database +from app.dependencies.user import get_client_user + +from .router import router + +from fastapi import Body, HTTPException, Security +from sqlmodel import col, exists, select + + +@router.get("/beatmapsets/{beatmapset_id}/can_rate", name="判断用户能否为谱面集打分", response_model=bool) +async def can_rate_beatmapset( + beatmapset_id: int, + session: Database, + current_user: User = Security(get_client_user), +): + """检查用户是否可以评价谱面集 + + 检查当前用户是否可以对指定的谱面集进行评价 + 参数: + - beatmapset_id: 谱面集ID + + 错误情况: + - 404: 找不到指定谱面集 + + 返回: + - bool: 用户是否可以评价谱面集 + """ + user_id = current_user.id + prev_ratings = (await session.exec(select(BeatmapRating).where(BeatmapRating.user_id == user_id))).first() + if prev_ratings is not None: + return False + query = select(exists()).where( + Score.user_id == user_id, + col(Score.beatmap).has(col(Beatmap.beatmapset_id) == beatmapset_id), + col(Score.passed).is_(True), + ) + return (await session.exec(query)).first() or False + + +@router.post("/beatmapsets/{beatmapset_id}/ratings", name="上传对谱面集的打分", status_code=201) +async def rate_beatmaps( + beatmapset_id: int, + session: Database, + rating: int = Body(..., ge=0, le=10), + current_user: User = Security(get_client_user), +): + """为谱面集评分 + + 为指定的谱面集添加用户评分,并更新谱面集的评分统计信息 + + 参数: + - beatmapset_id: 谱面集ID + - rating: 评分 + + 错误情况: + - 404: 找不到指定谱面集 + + 返回: + - 成功: None + """ + user_id = current_user.id + current_beatmapset = (await session.exec(select(exists()).where(Beatmapset.id == beatmapset_id))).first() + if not current_beatmapset: + raise HTTPException(404, "Beatmapset Not Found") + can_rating = await can_rate_beatmapset(beatmapset_id, session, current_user) + if not can_rating: + raise HTTPException(403, "User Cannot Rate This Beatmapset") + new_rating: BeatmapRating = BeatmapRating(beatmapset_id=beatmapset_id, user_id=user_id, rating=rating) + session.add(new_rating) + await session.commit() diff --git a/migrations/versions/24a32515292d_add_beatmap_ratings.py b/migrations/versions/24a32515292d_add_beatmap_ratings.py new file mode 100644 index 0000000..e47e9a8 --- /dev/null +++ b/migrations/versions/24a32515292d_add_beatmap_ratings.py @@ -0,0 +1,51 @@ +"""add beatmap ratings + +Revision ID: 24a32515292d +Revises: af88493881eb +Create Date: 2025-08-28 11:36:17.874090 + +""" + +from __future__ import annotations + +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "24a32515292d" +down_revision: str | Sequence[str] | None = "af88493881eb" +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.create_table( + "beatmap_ratings", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("beatmapset_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=True), + sa.Column("rating", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["beatmapset_id"], + ["beatmapsets.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["lazer_users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_beatmap_ratings_beatmapset_id"), "beatmap_ratings", ["beatmapset_id"], unique=False) + op.create_index(op.f("ix_beatmap_ratings_user_id"), "beatmap_ratings", ["user_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("beatmap_ratings") + # ### end Alembic commands ###