重构表结构:合并 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
This commit is contained in:
2026-07-28 23:48:57 +08:00
parent 118c5cbe96
commit e75833ee4b
14 changed files with 519 additions and 285 deletions
+26 -22
View File
@@ -2,7 +2,7 @@ import math
import uuid
from fastapi import APIRouter, HTTPException, Query, status
from sqlalchemy import delete, func, select
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
@@ -18,7 +18,7 @@ router = APIRouter(tags=["点赞收藏"])
def _cloud_options():
return (
joinedload(Cloud.user).joinedload(User.profile),
joinedload(Cloud.user),
joinedload(Cloud.cloud_type),
)
@@ -35,23 +35,12 @@ async def _get_visible_cloud(
if not cloud:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片不存在")
can_manage = cloud.user_id == user.id or user.profile.role == "admin"
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 _favorite_count(db: DbSession, cloud_id: uuid.UUID) -> int:
return (
await db.scalar(
select(func.count())
.select_from(CloudFavorite)
.where(CloudFavorite.cloud_id == cloud_id)
)
or 0
)
async def _is_favorited(db: DbSession, user_id: uuid.UUID, cloud_id: uuid.UUID) -> bool:
favorite_id = await db.scalar(
select(CloudFavorite.id).where(
@@ -71,11 +60,12 @@ async def _favorite_out(
) -> 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=await _favorite_count(db, cloud_id),
favorite_count=cloud.favorite_count if cloud else 0,
)
@@ -89,11 +79,10 @@ async def get_cloud_favorite(
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.profile.role != "admin":
if target_user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只能查询自己的喜欢状态",
@@ -109,27 +98,43 @@ async def set_cloud_favorite(
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
)
await db.execute(
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:
await db.execute(
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(
@@ -147,7 +152,6 @@ async def list_my_favorites(
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
) -> PageOut[CloudOut]:
"""当前用户点赞收藏的照片列表(按收藏时间倒序)。"""
total = (
await db.scalar(
select(func.count())
@@ -166,7 +170,7 @@ async def list_my_favorites(
.limit(page_size)
)
items = [
cloud_out(item, include_private=item.user_id == user.id or user.profile.role == "admin")
cloud_out(item, include_private=item.user_id == user.id or user.role == "admin")
for item in result.scalars().unique().all()
]
return PageOut(