feat(profile): avatar presets, upload, nickname editing

- FastAPI: preset assets 01–08, GET list/static, PUT /me/avatar/preset,
  safer uploaded-avatar path validation, preset_avatars + HTTP tests.
- Expo: personal-info (library + presets), profile tab avatar,
  resolveApiMediaUrl, auth hooks cache sync, Web multipart helper,
  partial-save messaging + profile i18n.
- Includes existing edits to conversation screen and voice use-player.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-06 13:51:43 +08:00
parent 59d4b19d7d
commit 7ad52fce89
27 changed files with 1271 additions and 270 deletions

View File

@@ -9,7 +9,15 @@ from app.core.config import settings
from app.core.dependencies import get_current_user
from app.core.logging import get_logger
from app.features.auth.deps import get_auth_service
from app.features.auth.preset_avatars import (
avatar_url_for_preset_filename,
list_preset_items,
preset_filename_for_id,
preset_file_path,
safe_avatar_upload_path,
)
from app.features.auth.schemas import (
AvatarPresetItem,
ChangePasswordRequest,
ChangePhoneRequest,
LoginRequest,
@@ -18,6 +26,7 @@ from app.features.auth.schemas import (
RegisterRequest,
ResetPasswordRequest,
SendSmsRequest,
SetAvatarPresetRequest,
SmsLoginRequest,
SmsRegisterRequest,
TokenResponse,
@@ -329,14 +338,72 @@ async def upload_avatar(
) from e
@router.get(
"/avatar-presets",
response_model=list[AvatarPresetItem],
summary="预设头像列表",
)
async def list_avatar_presets():
return [
AvatarPresetItem(id=item_id, url=item_url)
for item_id, item_url in list_preset_items()
]
@router.put(
"/me/avatar/preset",
response_model=UserResponse,
summary="使用预设头像",
responses={400: {"description": "无效的预设编号"}},
)
async def set_avatar_preset(
request: SetAvatarPresetRequest,
current_user: User = Depends(get_current_user),
service: AuthService = Depends(get_auth_service),
):
filename = preset_filename_for_id(request.preset_id)
if filename is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无效的预设头像编号",
)
path = preset_file_path(filename)
if path is None or not path.exists():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="预设头像不可用",
)
avatar_url = avatar_url_for_preset_filename(filename)
try:
user = await service.update_avatar_url(current_user.id, avatar_url)
except AuthError as e:
raise _map_auth_error(e)
return _user_response(user)
@router.get(
"/avatar-presets/{filename}",
summary="获取预设头像图片",
responses={404: {"description": "预设不存在"}},
)
async def get_avatar_preset(filename: str):
path = preset_file_path(filename)
if path is None or not path.exists():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="预设头像不存在",
)
return FileResponse(path, media_type="image/png")
@router.get(
"/avatars/{filename}",
summary="获取头像图片",
responses={404: {"description": "头像不存在"}},
)
async def get_avatar(filename: str):
file_path = AVATAR_DIR / filename
if not file_path.exists():
file_path = safe_avatar_upload_path(filename, AVATAR_DIR)
if file_path is None or not file_path.exists():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="头像不存在",