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>
This commit is contained in:
125
apps/backend/app/routers/xcx_avatar.py
Normal file
125
apps/backend/app/routers/xcx_avatar.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# -*- 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"},
|
||||
)
|
||||
Reference in New Issue
Block a user