dacb37e3e1
- onboarding_completion 表(user_id+device_id 唯一约束)+ model + repository - POST /api/v1/user/onboarding/complete 标记完成;登录响应 TokenWithUser.onboarding_completed (按登录请求带的 device_id 判定是否已走过引导,跨卸载重装稳定) - alembic 迁移 onboarding_completion(rebase 到最新 main 后已链在 coupon_daily_completion 之后,单 head) - docs/database 文档 + tests/test_onboarding.py + run.bat(本地启动脚本) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
3.0 KiB
Python
72 lines
3.0 KiB
Python
"""用户资料 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 onboarding as onboarding_repo
|
|
from app.repositories import user as user_repo
|
|
from app.schemas.auth import UserOut
|
|
from app.schemas.user import OkResponse, OnboardingCompleteRequest, 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.post("/onboarding/complete", response_model=OkResponse, summary="标记新手引导完成(按 设备+账号)")
|
|
def complete_onboarding(
|
|
req: OnboardingCompleteRequest, user: CurrentUser, db: DbSession
|
|
) -> OkResponse:
|
|
"""走完新手引导时调一次。按 (当前账号, device_id) 落一条完成标记,跨卸载重装持久。
|
|
幂等:重复调用不报错。device_id 取客户端硬件级 ANDROID_ID,与登录请求一致。"""
|
|
onboarding_repo.mark_completed(db, user_id=user.id, device_id=req.device_id)
|
|
logger.info("onboarding complete user_id=%d device_len=%d", user.id, len(req.device_id))
|
|
return OkResponse()
|
|
|
|
|
|
@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()
|