Files
opencloud-backend/app/routers/favorites.py
T
Mplan e75833ee4b 重构表结构:合并 profiles→users,展平 API 响应,新增计数器字段
- models: 删除 Profile 模型,字段合并到 User;User 新增云朵/公开/收藏计数器;Cloud 新增 favorite_count
- schemas/serializers: 展平 UserOut/AuthOut/MeOut/AdminUserOut,移除嵌套 profile
- deps: user.profile.role→user.role,移除 selectinload profile
- auth/register: 注册时直接设置 user 字段,不再创建 Profile
- clouds: 创建/删除云朵时同步 user.cloud_count 计数器,状态变化时同步 public_cloud_count
- favorites: 点赞/取消时同步 cloud.favorite_count 计数器
- admin: 审批/隐藏/批量删时同步 public_cloud_count 计数器
- profiles/stats: 用计数器替代实时 COUNT 查询
- alembic: 新增 migration 合并 profiles 到 users,初始化 counters
2026-07-28 23:48:57 +08:00

183 lines
5.5 KiB
Python

import math
import uuid
from fastapi import APIRouter, HTTPException, Query, status
from sqlalchemy import case, delete, func, select, update
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from sqlalchemy.orm import joinedload
from app.deps import CurrentUser, DbSession
from app.models import Cloud, CloudFavorite, User
from app.schemas import CloudOut, FavoriteIn, FavoriteOut, PageOut
from app.serializers import cloud_out
router = APIRouter(tags=["点赞收藏"])
def _cloud_options():
return (
joinedload(Cloud.user),
joinedload(Cloud.cloud_type),
)
async def _get_visible_cloud(
db: DbSession,
cloud_id: uuid.UUID,
user: CurrentUser,
) -> Cloud:
result = await db.execute(
select(Cloud).options(*_cloud_options()).where(Cloud.id == cloud_id)
)
cloud = result.scalar_one_or_none()
if not cloud:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片不存在")
can_manage = cloud.user_id == user.id or user.role == "admin"
if not can_manage and (cloud.status != "approved" or cloud.is_hidden):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片不存在")
return cloud
async def _is_favorited(db: DbSession, user_id: uuid.UUID, cloud_id: uuid.UUID) -> bool:
favorite_id = await db.scalar(
select(CloudFavorite.id).where(
CloudFavorite.user_id == user_id,
CloudFavorite.cloud_id == cloud_id,
)
)
return favorite_id is not None
async def _favorite_out(
db: DbSession,
*,
cloud_id: uuid.UUID,
user_id: uuid.UUID,
favorited: bool | None = None,
) -> FavoriteOut:
if favorited is None:
favorited = await _is_favorited(db, user_id, cloud_id)
cloud = await db.get(Cloud, cloud_id)
return FavoriteOut(
cloud_id=cloud_id,
user_id=user_id,
favorited=favorited,
favorite_count=cloud.favorite_count if cloud else 0,
)
@router.get("/clouds/{cloud_id}/favorite", response_model=FavoriteOut)
async def get_cloud_favorite(
cloud_id: uuid.UUID,
user: CurrentUser,
db: DbSession,
user_id: uuid.UUID | None = Query(
None,
description="要查询的用户 ID;默认当前登录用户。仅本人或管理员可查他人。",
),
) -> FavoriteOut:
await _get_visible_cloud(db, cloud_id, user)
target_user_id = user_id or user.id
if target_user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只能查询自己的喜欢状态",
)
return await _favorite_out(db, cloud_id=cloud_id, user_id=target_user_id)
@router.put("/clouds/{cloud_id}/favorite", response_model=FavoriteOut)
async def set_cloud_favorite(
cloud_id: uuid.UUID,
payload: FavoriteIn,
user: CurrentUser,
db: DbSession,
) -> FavoriteOut:
await _get_visible_cloud(db, cloud_id, user)
if payload.favorited:
insert_factory = (
pg_insert if db.bind and db.bind.dialect.name == "postgresql" else sqlite_insert
)
result = await db.execute(
insert_factory(CloudFavorite)
.values(user_id=user.id, cloud_id=cloud_id)
.on_conflict_do_nothing(
index_elements=[CloudFavorite.user_id, CloudFavorite.cloud_id]
)
)
if result.rowcount and result.rowcount > 0:
await db.execute(
update(Cloud)
.where(Cloud.id == cloud_id)
.values(favorite_count=Cloud.favorite_count + 1)
)
else:
result = await db.execute(
delete(CloudFavorite).where(
CloudFavorite.user_id == user.id,
CloudFavorite.cloud_id == cloud_id,
)
)
if result.rowcount and result.rowcount > 0:
await db.execute(
update(Cloud)
.where(Cloud.id == cloud_id)
.values(
favorite_count=case(
(Cloud.favorite_count > 0, Cloud.favorite_count - 1),
else_=0,
)
)
)
await db.commit()
return await _favorite_out(
db,
cloud_id=cloud_id,
user_id=user.id,
favorited=payload.favorited,
)
@router.get("/favorites/me", response_model=PageOut[CloudOut])
async def list_my_favorites(
user: CurrentUser,
db: DbSession,
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
) -> PageOut[CloudOut]:
total = (
await db.scalar(
select(func.count())
.select_from(CloudFavorite)
.where(CloudFavorite.user_id == user.id)
)
or 0
)
result = await db.execute(
select(Cloud)
.join(CloudFavorite, CloudFavorite.cloud_id == Cloud.id)
.options(*_cloud_options())
.where(CloudFavorite.user_id == user.id)
.order_by(CloudFavorite.created_at.desc(), CloudFavorite.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
items = [
cloud_out(item, include_private=item.user_id == user.id or user.role == "admin")
for item in result.scalars().unique().all()
]
return PageOut(
items=items,
page=page,
page_size=page_size,
total=total,
total_pages=max(1, math.ceil(total / page_size)) if total else 1,
)