Files
Neo-ZQYY/apps/backend/app/routers/xcx_avatar.py
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

126 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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"},
)