重构表结构:合并 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:
+86
-16
@@ -7,10 +7,10 @@ from zoneinfo import ZoneInfo
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from sqlalchemy import case, func, select, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app.deps import AdminUser, DbSession
|
||||
from app.models import Cloud, Profile, User
|
||||
from app.models import Cloud, User
|
||||
from app.schemas import (
|
||||
AdminCloudStatusIn,
|
||||
AdminCloudVisibilityIn,
|
||||
@@ -25,7 +25,7 @@ from app.schemas import (
|
||||
PageOut,
|
||||
)
|
||||
from app.security import hash_password, utc_now
|
||||
from app.serializers import cloud_out, profile_out, user_out
|
||||
from app.serializers import cloud_out, user_out
|
||||
from app.services.storage import delete_files
|
||||
|
||||
|
||||
@@ -71,12 +71,11 @@ async def list_users(
|
||||
total = await db.scalar(select(func.count()).select_from(User)) or 0
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.options(selectinload(User.profile))
|
||||
.order_by(User.created_at.desc(), User.id.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
items = [AdminUserOut(user=user_out(user), profile=profile_out(user.profile)) for user in result.scalars()]
|
||||
items = [AdminUserOut(user=user_out(user)) for user in result.scalars()]
|
||||
return PageOut(
|
||||
items=items,
|
||||
page=page,
|
||||
@@ -97,8 +96,9 @@ async def create_user(
|
||||
email=email,
|
||||
password_hash=hash_password(payload.password),
|
||||
email_verified_at=utc_now(),
|
||||
username=payload.username,
|
||||
role=payload.role,
|
||||
)
|
||||
user.profile = Profile(username=payload.username, role=payload.role)
|
||||
db.add(user)
|
||||
try:
|
||||
await db.commit()
|
||||
@@ -108,8 +108,8 @@ async def create_user(
|
||||
if "email" in message:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="该邮箱已被注册") from exc
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="这个昵称已经被使用") from exc
|
||||
await db.refresh(user, ["profile"])
|
||||
return AdminUserOut(user=user_out(user), profile=profile_out(user.profile))
|
||||
await db.refresh(user)
|
||||
return AdminUserOut(user=user_out(user))
|
||||
|
||||
|
||||
@router.patch("/users/{user_id}", response_model=AdminUserOut)
|
||||
@@ -119,10 +119,7 @@ async def update_user(
|
||||
admin: AdminUser,
|
||||
db: DbSession,
|
||||
) -> AdminUserOut:
|
||||
result = await db.execute(
|
||||
select(User).options(selectinload(User.profile)).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
user = await db.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||
if user.id == admin.id and (
|
||||
@@ -130,11 +127,11 @@ async def update_user(
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能禁用自己或移除自己的管理员权限")
|
||||
if payload.role is not None:
|
||||
user.profile.role = payload.role
|
||||
user.role = payload.role
|
||||
if payload.is_disabled is not None:
|
||||
user.profile.is_disabled = payload.is_disabled
|
||||
user.is_disabled = payload.is_disabled
|
||||
await db.commit()
|
||||
return AdminUserOut(user=user_out(user), profile=profile_out(user.profile))
|
||||
return AdminUserOut(user=user_out(user))
|
||||
|
||||
|
||||
@router.get("/clouds", response_model=PageOut[CloudOut])
|
||||
@@ -154,7 +151,7 @@ async def list_admin_clouds(
|
||||
total = await db.scalar(select(func.count()).select_from(Cloud).where(*filters)) or 0
|
||||
result = await db.execute(
|
||||
select(Cloud)
|
||||
.options(joinedload(Cloud.user).joinedload(User.profile), joinedload(Cloud.cloud_type))
|
||||
.options(joinedload(Cloud.user), joinedload(Cloud.cloud_type))
|
||||
.where(*filters)
|
||||
.order_by(Cloud.created_at.desc(), Cloud.id.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
@@ -182,6 +179,37 @@ async def update_cloud_status(
|
||||
db: DbSession,
|
||||
) -> BatchUpdateOut:
|
||||
await _require_all_cloud_ids(db, payload.ids)
|
||||
|
||||
if payload.status == "approved":
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id.in_(
|
||||
select(Cloud.user_id).where(
|
||||
Cloud.id.in_(payload.ids),
|
||||
Cloud.status != "approved",
|
||||
Cloud.is_hidden.is_(False),
|
||||
)
|
||||
))
|
||||
.values(public_cloud_count=User.public_cloud_count + 1)
|
||||
)
|
||||
else:
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id.in_(
|
||||
select(Cloud.user_id).where(
|
||||
Cloud.id.in_(payload.ids),
|
||||
Cloud.status == "approved",
|
||||
Cloud.is_hidden.is_(False),
|
||||
)
|
||||
))
|
||||
.values(
|
||||
public_cloud_count=case(
|
||||
(User.public_cloud_count > 0, User.public_cloud_count - 1),
|
||||
else_=0,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
update(Cloud)
|
||||
.where(Cloud.id.in_(payload.ids))
|
||||
@@ -198,6 +226,37 @@ async def update_cloud_visibility(
|
||||
db: DbSession,
|
||||
) -> BatchUpdateOut:
|
||||
await _require_all_cloud_ids(db, payload.ids)
|
||||
|
||||
if payload.is_hidden:
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id.in_(
|
||||
select(Cloud.user_id).where(
|
||||
Cloud.id.in_(payload.ids),
|
||||
Cloud.status == "approved",
|
||||
Cloud.is_hidden.is_(False),
|
||||
)
|
||||
))
|
||||
.values(
|
||||
public_cloud_count=case(
|
||||
(User.public_cloud_count > 0, User.public_cloud_count - 1),
|
||||
else_=0,
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id.in_(
|
||||
select(Cloud.user_id).where(
|
||||
Cloud.id.in_(payload.ids),
|
||||
Cloud.status == "approved",
|
||||
Cloud.is_hidden.is_(True),
|
||||
)
|
||||
))
|
||||
.values(public_cloud_count=User.public_cloud_count + 1)
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
update(Cloud)
|
||||
.where(Cloud.id.in_(payload.ids))
|
||||
@@ -218,8 +277,19 @@ async def delete_admin_clouds(
|
||||
if len(clouds) != len(payload.ids):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="部分图片不存在")
|
||||
paths = [path for cloud in clouds for path in (cloud.image_path, cloud.thumbnail_path)]
|
||||
|
||||
user_ids: dict[uuid.UUID, int] = {}
|
||||
for cloud in clouds:
|
||||
user_ids[cloud.user_id] = user_ids.get(cloud.user_id, 0) + 1
|
||||
await db.delete(cloud)
|
||||
|
||||
for uid, count in user_ids.items():
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == uid)
|
||||
.values(cloud_count=User.cloud_count - count)
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await delete_files(paths)
|
||||
return DeleteResultOut(deleted=len(clouds))
|
||||
|
||||
Reference in New Issue
Block a user