"""用户资料 endpoint。 路由前缀 `/api/v1/user`,全部需 Bearer 鉴权(用户级数据,不同于 MVP 透传接口): PATCH /profile 修改昵称 POST /avatar 上传头像(multipart,字段名 file) DELETE / 注销账号(软删除 + 匿名化) 昵称/头像后端持久化到 user 表,登录后经 /auth/me 与登录响应回传客户端——这才是 "改完重登仍生效"的唯一数据源(此前客户端只存本地、退登即丢)。 """ from __future__ import annotations import logging from fastapi import APIRouter, File, HTTPException, UploadFile from app.api.deps import CurrentUser, DbSession from app.core import media from app.repositories import user as user_repo from app.schemas.auth import UserOut from app.schemas.user import OkResponse, ProfileUpdateRequest logger = logging.getLogger("shagua.user") router = APIRouter(prefix="/api/v1/user", tags=["user"]) @router.patch("/profile", response_model=UserOut, summary="修改昵称") def update_profile(req: ProfileUpdateRequest, user: CurrentUser, db: DbSession) -> UserOut: updated = user_repo.update_nickname(db, user, nickname=req.nickname) logger.info("update nickname user_id=%d", user.id) return UserOut.model_validate(updated) @router.post("/avatar", response_model=UserOut, summary="上传头像") async def upload_avatar( user: CurrentUser, db: DbSession, file: UploadFile = File(..., description="头像图片(jpeg/png/webp,≤5MB)"), ) -> UserOut: data = await file.read() try: url = media.save_avatar(user.id, data) except media.MediaError as e: raise HTTPException(status_code=400, detail=str(e)) from e old_url = user.avatar_url updated = user_repo.set_avatar_url(db, user, avatar_url=url) media.delete_avatar(old_url) # 落库成功后再删旧文件,避免删了新的没存上 logger.info("update avatar user_id=%d url=%s", user.id, url) return UserOut.model_validate(updated) @router.delete("", response_model=OkResponse, summary="注销账号(软删除)") def delete_account(user: CurrentUser, db: DbSession) -> OkResponse: media.delete_avatar(user.avatar_url) user_repo.soft_delete_account(db, user) logger.info("delete account user_id=%d", user.id) return OkResponse()