新增头像接口:POST /profiles/me/avatar 上传、DELETE /profiles/me/avatar 删除
This commit is contained in:
+34
-1
@@ -1,7 +1,8 @@
|
||||
import math
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from fastapi import APIRouter, File, HTTPException, Query, UploadFile, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import joinedload
|
||||
@@ -10,6 +11,7 @@ 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=["用户资料"])
|
||||
@@ -38,6 +40,37 @@ async def update_my_profile(
|
||||
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,
|
||||
|
||||
@@ -12,6 +12,7 @@ from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ALLOWED_FORMATS = {"JPEG": "jpg", "PNG": "png", "WEBP": "webp"}
|
||||
AVATAR_MAX_EDGE = 512
|
||||
|
||||
|
||||
def _process_image(raw: bytes) -> tuple[bytes, str, bytes]:
|
||||
@@ -105,3 +106,38 @@ async def delete_files(paths: list[str]) -> None:
|
||||
logger.exception("删除图片失败:%s", relative_path)
|
||||
|
||||
await asyncio.to_thread(remove)
|
||||
|
||||
|
||||
async def save_avatar(file: UploadFile, user_id: uuid.UUID) -> str:
|
||||
raw = await file.read(settings.max_upload_bytes + 1)
|
||||
if not raw:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="图片不能为空")
|
||||
if len(raw) > settings.max_upload_bytes:
|
||||
raise HTTPException(status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="图片文件过大")
|
||||
|
||||
def process() -> str:
|
||||
Image.MAX_IMAGE_PIXELS = settings.max_image_pixels
|
||||
try:
|
||||
with Image.open(io.BytesIO(raw)) as source:
|
||||
if source.format not in ALLOWED_FORMATS:
|
||||
raise ValueError("仅支持 JPEG、PNG 和 WebP 图片")
|
||||
source = source.convert("RGB")
|
||||
source.thumbnail(
|
||||
(AVATAR_MAX_EDGE, AVATAR_MAX_EDGE),
|
||||
Image.Resampling.LANCZOS,
|
||||
)
|
||||
relative_path = Path("avatars") / f"{user_id}.jpg"
|
||||
target = settings.upload_dir / relative_path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
source.save(target, format="JPEG", quality=85, optimize=True)
|
||||
return relative_path.as_posix()
|
||||
except (UnidentifiedImageError, OSError, Image.DecompressionBombError, ValueError) as exc:
|
||||
raise ValueError(str(exc) or "图片内容无效") from exc
|
||||
|
||||
try:
|
||||
return await asyncio.to_thread(process)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"图片无法处理:{exc}",
|
||||
) from exc
|
||||
|
||||
Reference in New Issue
Block a user