重构表结构:合并 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:
+73
-11
@@ -5,13 +5,13 @@ from decimal import Decimal, ROUND_HALF_UP
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from fastapi import APIRouter, File, Form, HTTPException, Query, UploadFile, status
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy import func, or_, 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, OptionalUser
|
||||
from app.models import Cloud, CloudType, Profile, User, UserCollection
|
||||
from app.models import Cloud, CloudFavorite, CloudType, User, UserCollection
|
||||
from app.schemas import (
|
||||
BadgeOut,
|
||||
BatchIdsIn,
|
||||
@@ -30,7 +30,7 @@ router = APIRouter(prefix="/clouds", tags=["云图"])
|
||||
|
||||
def _cloud_options():
|
||||
return (
|
||||
joinedload(Cloud.user).joinedload(User.profile),
|
||||
joinedload(Cloud.user),
|
||||
joinedload(Cloud.cloud_type),
|
||||
)
|
||||
|
||||
@@ -93,9 +93,9 @@ async def list_gallery_clouds(
|
||||
username = term[1:].strip()
|
||||
if not username:
|
||||
return PageOut(items=[], page=1, page_size=page_size, total=0, total_pages=1)
|
||||
data_query = data_query.join(Cloud.user).join(User.profile)
|
||||
count_query = count_query.join(Cloud.user).join(User.profile)
|
||||
filters.append(Profile.username.ilike(f"%{username}%"))
|
||||
data_query = data_query.join(Cloud.user)
|
||||
count_query = count_query.join(Cloud.user)
|
||||
filters.append(User.username.ilike(f"%{username}%"))
|
||||
elif term:
|
||||
data_query = data_query.outerjoin(Cloud.cloud_type)
|
||||
count_query = count_query.outerjoin(Cloud.cloud_type)
|
||||
@@ -158,10 +158,22 @@ async def get_cloud(cloud_id: uuid.UUID, db: DbSession, viewer: OptionalUser) ->
|
||||
cloud = await _get_cloud(db, cloud_id)
|
||||
if not cloud:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片不存在")
|
||||
can_manage = bool(viewer and (viewer.id == cloud.user_id or viewer.profile.role == "admin"))
|
||||
can_manage = bool(viewer and (viewer.id == cloud.user_id or viewer.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_out(cloud, include_private=can_manage)
|
||||
|
||||
favorite_count = cloud.favorite_count
|
||||
is_favorited = False
|
||||
if viewer:
|
||||
fav_id = await db.scalar(
|
||||
select(CloudFavorite.id).where(
|
||||
CloudFavorite.user_id == viewer.id,
|
||||
CloudFavorite.cloud_id == cloud_id,
|
||||
)
|
||||
)
|
||||
is_favorited = fav_id is not None
|
||||
|
||||
return cloud_out(cloud, include_private=can_manage, favorite_count=favorite_count, is_favorited=is_favorited)
|
||||
|
||||
|
||||
@router.post("", response_model=CloudCreateOut, status_code=status.HTTP_201_CREATED)
|
||||
@@ -207,6 +219,13 @@ async def create_cloud(
|
||||
unlocked_badge: BadgeOut | None = None
|
||||
try:
|
||||
await db.flush()
|
||||
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == user.id)
|
||||
.values(cloud_count=User.cloud_count + 1)
|
||||
)
|
||||
|
||||
if cloud_type:
|
||||
insert_factory = pg_insert if db.bind and db.bind.dialect.name == "postgresql" else sqlite_insert
|
||||
insert_result = await db.execute(
|
||||
@@ -223,6 +242,11 @@ async def create_cloud(
|
||||
)
|
||||
unlocked_at = insert_result.scalar_one_or_none()
|
||||
if unlocked_at:
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == user.id)
|
||||
.values(collection_count=User.collection_count + 1)
|
||||
)
|
||||
unlocked_badge = BadgeOut(
|
||||
cloud_type_id=cloud_type.id,
|
||||
cloud_name=cloud_type.name,
|
||||
@@ -252,9 +276,10 @@ async def update_cloud(
|
||||
cloud = await _get_cloud(db, cloud_id)
|
||||
if not cloud:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片不存在")
|
||||
if cloud.user_id != user.id and user.profile.role != "admin":
|
||||
if cloud.user_id != user.id and user.role != "admin":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不能修改其他用户的图片")
|
||||
|
||||
previous_public = cloud.status == "approved" and not cloud.is_hidden
|
||||
changes = payload.model_dump(exclude_unset=True)
|
||||
final_type_id = changes.get("cloud_type_id", cloud.cloud_type_id)
|
||||
final_custom = _normalize_text(changes.get("custom_cloud_type", cloud.custom_cloud_type))
|
||||
@@ -284,8 +309,18 @@ async def update_cloud(
|
||||
cloud.captured_at = _normalize_datetime(changes["captured_at"])
|
||||
if "is_hidden" in changes:
|
||||
cloud.is_hidden = changes["is_hidden"]
|
||||
await db.commit()
|
||||
|
||||
await db.flush()
|
||||
new_public = cloud.status == "approved" and not cloud.is_hidden
|
||||
if previous_public != new_public:
|
||||
delta = 1 if new_public else -1
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == cloud.user_id)
|
||||
.values(public_cloud_count=User.public_cloud_count + delta)
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
updated = await _get_cloud(db, cloud.id)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片不存在")
|
||||
@@ -297,10 +332,25 @@ async def delete_cloud(cloud_id: uuid.UUID, db: DbSession, user: CurrentUser) ->
|
||||
cloud = await _get_cloud(db, cloud_id)
|
||||
if not cloud:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片不存在")
|
||||
if cloud.user_id != user.id and user.profile.role != "admin":
|
||||
if cloud.user_id != user.id and user.role != "admin":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不能删除其他用户的图片")
|
||||
paths = [cloud.image_path, cloud.thumbnail_path]
|
||||
|
||||
was_public = cloud.status == "approved" and not cloud.is_hidden
|
||||
await db.delete(cloud)
|
||||
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == cloud.user_id)
|
||||
.values(cloud_count=User.cloud_count - 1)
|
||||
)
|
||||
if was_public:
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == cloud.user_id)
|
||||
.values(public_cloud_count=User.public_cloud_count - 1)
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await delete_files(paths)
|
||||
return DeleteResultOut(deleted=1)
|
||||
@@ -318,9 +368,21 @@ async def batch_delete_clouds(
|
||||
clouds = result.scalars().all()
|
||||
if len(clouds) != len(payload.ids):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="部分图片不存在或不属于当前用户")
|
||||
|
||||
public_count = sum(1 for c in clouds if c.status == "approved" and not c.is_hidden)
|
||||
paths = [path for cloud in clouds for path in (cloud.image_path, cloud.thumbnail_path)]
|
||||
for cloud in clouds:
|
||||
await db.delete(cloud)
|
||||
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == user.id)
|
||||
.values(
|
||||
cloud_count=User.cloud_count - len(clouds),
|
||||
public_cloud_count=User.public_cloud_count - public_count,
|
||||
)
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await delete_files(paths)
|
||||
return DeleteResultOut(deleted=len(clouds))
|
||||
|
||||
Reference in New Issue
Block a user