chore: resolve WIP after merging internal/development

- .gitignore: keep api/uploads ignore and copyright_source_listing.pdf path

- auth: keep COS avatar upload URL; delete prior COS object when applying preset

- i18n: regenerate resources.ts (includes profile tapAwayToClose)

- Avatar/COS tests and personal-info remain from prior local work

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-18 15:34:50 +08:00
parent 98802240ac
commit eabda2c6a9
12 changed files with 350 additions and 97 deletions

View File

@@ -7,6 +7,11 @@ from fastapi.responses import FileResponse
from PIL import Image
from app.core.config import settings
from app.core.cos_url_keys import (
avatar_url_for_api_response,
best_effort_delete_cos_object_for_url,
extract_cos_object_key_if_owned,
)
from app.core.dependencies import get_current_user
from app.core.logging import get_logger
from app.features.auth.deps import get_auth_service
@@ -46,7 +51,6 @@ router = APIRouter(
)
AVATAR_DIR = Path("uploads/avatars")
AVATAR_DIR.mkdir(parents=True, exist_ok=True)
# ── helpers ──────────────────────────────────────────────────
@@ -75,7 +79,7 @@ def _user_response(user: User) -> UserResponse:
phone=user.phone,
email=user.email,
nickname=user.nickname,
avatar_url=user.avatar_url,
avatar_url=avatar_url_for_api_response(user.avatar_url),
subscription_type=user.subscription_type,
created_at=user.created_at.isoformat(),
language_preference=lang,
@@ -278,9 +282,17 @@ async def upload_avatar(
len(file_content),
)
try:
AVATAR_DIR.mkdir(parents=True, exist_ok=True)
if not (
(settings.tencent_cos_secret_id or "").strip()
and (settings.tencent_cos_secret_key or "").strip()
and (settings.tencent_cos_bucket or "").strip()
):
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="头像存储服务未配置,请稍后再试",
)
try:
image_bytes = io.BytesIO(file_content)
image_bytes.seek(0)
@@ -326,14 +338,28 @@ async def upload_avatar(
if size > 512:
image = image.resize((512, 512), Image.Resampling.LANCZOS)
file_extension = "jpg"
filename = f"{current_user.id}.{file_extension}"
file_path = AVATAR_DIR / filename
jpeg_buffer = io.BytesIO()
image.save(jpeg_buffer, format="JPEG", quality=85, optimize=True)
jpeg_bytes = jpeg_buffer.getvalue()
image.save(file_path, "JPEG", quality=85, optimize=True)
cos_key = f"avatars/{current_user.id}.jpg"
old_url = current_user.avatar_url
old_key = extract_cos_object_key_if_owned(old_url) if old_url else None
if old_key and old_key != cos_key:
best_effort_delete_cos_object_for_url(old_url)
from app.core.dependencies import get_object_storage
storage = get_object_storage()
try:
avatar_url = storage.upload(cos_key, jpeg_bytes, "image/jpeg")
except Exception as exc:
logger.exception("COS 头像上传失败: {}", exc)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="头像存储暂时不可用,请稍后再试",
) from exc
# 路径固定为 {user_id}.jpg客户端会缓存每次写入新文件后 bump URL 以绕过缓存。
avatar_url = f"/api/auth/avatars/{filename}?v={time.time_ns()}"
user = await service.update_avatar_url(current_user.id, avatar_url)
return _user_response(user)
except HTTPException:
@@ -381,6 +407,8 @@ async def set_avatar_preset(
status_code=status.HTTP_400_BAD_REQUEST,
detail="预设头像不可用",
)
best_effort_delete_cos_object_for_url(current_user.avatar_url)
avatar_url = f"{avatar_url_for_preset_filename(filename)}?v={time.time_ns()}"
try:
user = await service.update_avatar_url(current_user.id, avatar_url)
@@ -410,6 +438,7 @@ async def get_avatar_preset(filename: str):
responses={404: {"description": "头像不存在"}},
)
async def get_avatar(filename: str):
AVATAR_DIR.mkdir(parents=True, exist_ok=True)
file_path = safe_avatar_upload_path(filename, AVATAR_DIR)
if file_path is None or not file_path.exists():
raise HTTPException(