216 lines
7.0 KiB
Python
216 lines
7.0 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 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=["用户资料"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# /me 路由(必须定义在 /{user_id} 之前,避免 "me" 被当作 UUID 解析)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.get("/me", response_model=PublicProfileOut)
|
|
async def get_my_profile(user: CurrentUser, db: DbSession) -> PublicProfileOut:
|
|
return public_profile_out(user.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("/me/stats", response_model=ProfileStatsOut)
|
|
async def get_my_stats(user: CurrentUser, db: DbSession) -> ProfileStatsOut:
|
|
return await _build_stats(user.id, db)
|
|
|
|
|
|
@router.get("/me/clouds", response_model=PageOut[CloudOut])
|
|
async def get_my_clouds(
|
|
user: CurrentUser,
|
|
db: DbSession,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
) -> PageOut[CloudOut]:
|
|
return await _list_profile_clouds(user.id, db, viewer_role=user.profile.role, viewer_id=user.id, page=page, page_size=page_size)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# /{user_id} 路由
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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.get("/{user_id}/stats", response_model=ProfileStatsOut)
|
|
async def get_user_stats(
|
|
user_id: uuid.UUID,
|
|
viewer: CurrentUser,
|
|
db: DbSession,
|
|
) -> ProfileStatsOut:
|
|
if viewer.id != user_id and viewer.profile.role != "admin":
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只能查看自己的统计数据")
|
|
return await _build_stats(user_id, db)
|
|
|
|
|
|
@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]:
|
|
viewer_role = viewer.profile.role if viewer else None
|
|
viewer_id = viewer.id if viewer else None
|
|
return await _list_profile_clouds(user_id, db, viewer_role=viewer_role, viewer_id=viewer_id, page=page, page_size=page_size)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
async def _build_stats(user_id: uuid.UUID, 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,
|
|
)
|
|
|
|
|
|
async def _list_profile_clouds(
|
|
user_id: uuid.UUID,
|
|
db: DbSession,
|
|
*,
|
|
viewer_role: str | None,
|
|
viewer_id: uuid.UUID | None,
|
|
page: int,
|
|
page_size: int,
|
|
) -> PageOut[CloudOut]:
|
|
if not await db.get(Profile, user_id):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
|
|
|
can_manage = bool(viewer_id and (viewer_id == user_id or viewer_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)),
|
|
)
|