# -*- coding: utf-8 -*- """ 小程序头像上传路由。 端点清单: - POST /api/xcx/avatar/upload — 上传头像(chooseAvatar 临时文件 → 服务器持久化) - GET /api/xcx/avatar/{user_id} — 获取头像文件(静态文件服务) """ from __future__ import annotations import logging from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status from fastapi.responses import FileResponse from app import config from app.auth.dependencies import CurrentUser, get_current_user_or_limited from app.database import get_connection from app.schemas.base import CamelModel from app.trace.decorators import trace_service logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/xcx/avatar", tags=["小程序头像"]) def _get_avatar_dir() -> Path: """获取头像存储目录,不存在则创建。""" avatar_path = config.AVATAR_EXPORT_PATH if not avatar_path: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="AVATAR_EXPORT_PATH 未配置", ) p = Path(avatar_path) p.mkdir(parents=True, exist_ok=True) return p class AvatarUploadResponse(CamelModel): """头像上传响应。""" avatar_url: str # ── POST /api/xcx/avatar/upload ────────────────────────── @router.post("/upload", response_model=AvatarUploadResponse) @trace_service("上传头像", "Upload avatar") async def upload_avatar( file: UploadFile = File(...), user: CurrentUser = Depends(get_current_user_or_limited), ): """ 接收小程序 chooseAvatar 上传的头像文件。 流程: 1. 读取上传文件内容 2. 保存到 AVATAR_EXPORT_PATH/{user_id}.jpg(覆盖式,幂等) 3. 更新 auth.users.avatar_url 4. 返回相对路径 """ avatar_dir = _get_avatar_dir() # 固定 jpg 后缀,覆盖式保存 filename = f"{user.user_id}.jpg" filepath = avatar_dir / filename relative_url = f"avatars/{filename}" # 读取并保存文件 content = await file.read() if len(content) == 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="上传文件为空", ) # 限制 2MB if len(content) > 2 * 1024 * 1024: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="头像文件不能超过 2MB", ) filepath.write_bytes(content) logger.info("头像已保存: user_id=%s, path=%s", user.user_id, filepath) # 更新数据库 conn = get_connection() try: with conn.cursor() as cur: cur.execute( "UPDATE auth.users SET avatar_url = %s WHERE id = %s", (relative_url, user.user_id), ) conn.commit() finally: conn.close() return AvatarUploadResponse(avatar_url=relative_url) # ── GET /api/xcx/avatar/{user_id} ──────────────────────── @router.get("/{user_id}") async def get_avatar(user_id: int): """ 获取用户头像文件。 无需鉴权(头像为公开资源,通过 user_id 访问)。 文件不存在时返回 404。 """ avatar_dir = _get_avatar_dir() filepath = avatar_dir / f"{user_id}.jpg" if not filepath.exists(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="头像不存在", ) return FileResponse( path=str(filepath), media_type="image/jpeg", headers={"Cache-Control": "public, max-age=3600"}, )