bea288e414
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
79 lines
3.0 KiB
Python
79 lines
3.0 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — 头像代理 API
|
||
# =============================================================================
|
||
# 说明:代理企微头像图片,解决 COEP/Mixed Content/CSP 跨域问题。
|
||
# 前端通过 /api/avatar/proxy?url=<encoded_url> 访问企微头像,
|
||
# 后端服务器请求图片并返回,避免浏览器直接请求 wework.qpic.cn。
|
||
# =============================================================================
|
||
|
||
import logging
|
||
from urllib.parse import unquote
|
||
|
||
import httpx
|
||
from fastapi import APIRouter, Query, Response
|
||
|
||
from app.utils.response import AppException
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter()
|
||
|
||
# 允许代理的域名白名单
|
||
ALLOWED_AVATAR_DOMAINS = ["wework.qpic.cn"]
|
||
|
||
|
||
@router.get("/avatar/proxy")
|
||
async def proxy_avatar(
|
||
url: str = Query(..., description="要代理的头像 URL(URL 编码)"),
|
||
):
|
||
"""代理企微头像图片。
|
||
|
||
解决浏览器 COEP/CSP/Mixed Content 策略导致 wework.qpic.cn 图片无法加载的问题。
|
||
后端服务器请求图片后返回给前端,代理 URL 为同源 HTTPS,不受跨域策略限制。
|
||
|
||
Args:
|
||
url: 要代理的头像 URL(URL 编码后的)
|
||
|
||
Returns:
|
||
Response: 图片二进制数据,带 Content-Type 和 Cache-Control 头
|
||
|
||
Raises:
|
||
AppException: URL 不在白名单中或图片获取失败
|
||
"""
|
||
# 解码 URL
|
||
avatar_url = unquote(url)
|
||
|
||
# 安全校验:只允许代理白名单域名的图片
|
||
if not any(domain in avatar_url for domain in ALLOWED_AVATAR_DOMAINS):
|
||
logger.warning(f"头像代理被拒绝(域名不在白名单): {avatar_url[:80]}")
|
||
raise AppException(code=403, message="只允许代理企微头像")
|
||
|
||
# 确保 HTTPS
|
||
if avatar_url.startswith("http://"):
|
||
avatar_url = "https://" + avatar_url[7:]
|
||
|
||
# 请求图片
|
||
try:
|
||
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
||
resp = await client.get(avatar_url)
|
||
if resp.status_code == 200:
|
||
content_type = resp.headers.get("content-type", "image/jpeg")
|
||
return Response(
|
||
content=resp.content,
|
||
media_type=content_type,
|
||
headers={
|
||
"Cache-Control": "public, max-age=86400", # 浏览器缓存 1 天
|
||
},
|
||
)
|
||
else:
|
||
logger.warning(
|
||
f"头像代理获取失败: url={avatar_url[:80]}, status={resp.status_code}"
|
||
)
|
||
raise AppException(code=404, message="头像获取失败")
|
||
except httpx.TimeoutException:
|
||
logger.warning(f"头像代理超时: url={avatar_url[:80]}")
|
||
raise AppException(code=504, message="头像获取超时")
|
||
except Exception as e:
|
||
logger.warning(f"头像代理异常: url={avatar_url[:80]}, error={e}")
|
||
raise AppException(code=500, message="头像代理失败")
|