Files
opencloud-backend/app/routers/profiles.py
T

145 lines
4.9 KiB
Python

import math
import uuid
from typing import Annotated
from fastapi import APIRouter, File, HTTPException, Query, UploadFile, status
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload
from app.deps import AdminUser, CurrentUser, DbSession, OptionalUser
from app.models import Cloud, Profile, User, UserCollection
from app.schemas import CloudOut, PageOut, ProfileOut, ProfileStatsOut, ProfileUpdateIn, PublicProfileOut
from app.serializers import cloud_out, profile_out, public_profile_out
from app.services.storage import delete_files, save_avatar
router = APIRouter(prefix="/profiles", tags=["用户资料"])
@router.get("/{user_id}", response_model=PublicProfileOut)
async def get_profile(user_id: uuid.UUID, db: DbSession) -> PublicProfileOut:
profile = await db.get(Profile, user_id)
if not profile:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
return public_profile_out(profile)
@router.patch("/me", response_model=ProfileOut)
async def update_my_profile(
payload: ProfileUpdateIn,
user: CurrentUser,
db: DbSession,
) -> ProfileOut:
user.profile.username = payload.username
try:
await db.commit()
except IntegrityError as exc:
await db.rollback()
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="这个昵称已经被使用") from exc
return profile_out(user.profile)
@router.post("/me/avatar", response_model=ProfileOut)
async def upload_avatar(
image: Annotated[UploadFile, File()],
user: CurrentUser,
db: DbSession,
) -> ProfileOut:
if user.profile.avatar_path:
old_path = user.profile.avatar_path
user.profile.avatar_path = None
await db.commit()
await delete_files([old_path])
avatar_path = await save_avatar(image, user.id)
user.profile.avatar_path = avatar_path
await db.commit()
return profile_out(user.profile)
@router.delete("/me/avatar", response_model=ProfileOut)
async def delete_avatar(
user: CurrentUser,
db: DbSession,
) -> ProfileOut:
if not user.profile.avatar_path:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="没有头像")
old_path = user.profile.avatar_path
user.profile.avatar_path = None
await db.commit()
await delete_files([old_path])
return profile_out(user.profile)
@router.get("/{user_id}/stats", response_model=ProfileStatsOut)
async def get_user_stats(
user_id: uuid.UUID,
_: AdminUser,
db: DbSession,
) -> ProfileStatsOut:
result = await db.execute(
select(User).options(joinedload(User.profile)).where(User.id == user_id)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
cloud_count = await db.scalar(
select(func.count()).select_from(Cloud).where(Cloud.user_id == user_id)
) or 0
public_cloud_count = await db.scalar(
select(func.count()).select_from(Cloud).where(
Cloud.user_id == user_id,
Cloud.status == "approved",
Cloud.is_hidden.is_(False),
)
) or 0
collection_count = await db.scalar(
select(func.count()).select_from(UserCollection).where(UserCollection.user_id == user_id)
) or 0
return ProfileStatsOut(
user_id=user.id,
username=user.profile.username,
email=user.email,
created_at=user.created_at,
cloud_count=cloud_count,
public_cloud_count=public_cloud_count,
collection_count=collection_count,
)
@router.get("/{user_id}/clouds", response_model=PageOut[CloudOut])
async def get_profile_clouds(
user_id: uuid.UUID,
db: DbSession,
viewer: OptionalUser,
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
) -> PageOut[CloudOut]:
if not await db.get(Profile, user_id):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
can_manage = bool(viewer and (viewer.id == user_id or viewer.profile.role == "admin"))
filters = [Cloud.user_id == user_id]
if not can_manage:
filters.extend([Cloud.status == "approved", Cloud.is_hidden.is_(False)])
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))
.where(*filters)
.order_by(Cloud.captured_at.desc().nulls_last(), Cloud.created_at.desc(), Cloud.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
items = [cloud_out(item, include_private=can_manage) 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)),
)