kol-insight/backend/app/services/session_pool.py
zfc f123f68be3 feat(video-analysis): 完成视频分析模块迭代任务
Bug 修复:
- T-019: 修复品牌API响应解析,正确解析 data[0].brand_name
- T-020: 添加品牌API Bearer Token认证

视频分析功能:
- T-021: SessionID池服务,从内部API获取Cookie列表
- T-022: SessionID自动重试,失效时自动切换重试
- T-023: 巨量云图API封装,支持超时和错误处理
- T-024: 视频分析数据接口 GET /api/v1/videos/{item_id}/analysis
- T-025: 数据库A3指标更新
- T-026: 视频分析前端页面,展示6大类25+指标

测试覆盖率:
- brand_api.py: 100%
- session_pool.py: 100%
- yuntu_api.py: 100%
- video_analysis.py: 99%

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:51:35 +08:00

142 lines
4.3 KiB
Python
Raw 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.

"""
SessionID池服务 (T-021)
从内部API获取Cookie列表随机选取sessionid用于巨量云图API调用。
"""
import asyncio
import random
import logging
from typing import List, Optional
import httpx
from app.config import settings
logger = logging.getLogger(__name__)
class SessionPool:
"""SessionID池管理器"""
def __init__(self):
self._sessions: List[str] = []
self._lock = asyncio.Lock()
async def refresh(self) -> bool:
"""
从内部API刷新SessionID列表。
Returns:
bool: 刷新是否成功
"""
async with self._lock:
try:
headers = {}
if settings.YUNTU_API_TOKEN:
headers["Authorization"] = f"Bearer {settings.YUNTU_API_TOKEN}"
async with httpx.AsyncClient(
timeout=settings.YUNTU_API_TIMEOUT
) as client:
response = await client.get(
f"{settings.BRAND_API_BASE_URL}/v1/yuntu/get_cookie",
params={"page": 1, "page_size": 100},
headers=headers,
)
if response.status_code == 200:
data = response.json()
# 响应格式: {"data": [{"sessionid": "xxx", ...}, ...]}
if isinstance(data, dict):
cookie_list = data.get("data", [])
if isinstance(cookie_list, list):
self._sessions = [
item.get("sessionid")
for item in cookie_list
if isinstance(item, dict) and item.get("sessionid")
]
logger.info(
f"SessionPool refreshed: {len(self._sessions)} sessions"
)
return len(self._sessions) > 0
logger.warning(
f"Failed to refresh session pool: status={response.status_code}"
)
return False
except httpx.TimeoutException:
logger.error("SessionPool refresh timeout")
return False
except httpx.RequestError as e:
logger.error(f"SessionPool refresh request error: {e}")
return False
except Exception as e:
logger.error(f"SessionPool refresh unexpected error: {e}")
return False
def get_random(self) -> Optional[str]:
"""
随机获取一个SessionID。
Returns:
Optional[str]: SessionID池为空时返回None
"""
if not self._sessions:
return None
return random.choice(self._sessions)
def remove(self, session_id: str) -> None:
"""
从池中移除失效的SessionID。
Args:
session_id: 要移除的SessionID
"""
try:
self._sessions.remove(session_id)
logger.info(f"Removed invalid session: {session_id[:8]}...")
except ValueError:
pass # 已经被移除
@property
def size(self) -> int:
"""返回池中SessionID数量"""
return len(self._sessions)
@property
def is_empty(self) -> bool:
"""检查池是否为空"""
return len(self._sessions) == 0
# 全局单例
session_pool = SessionPool()
async def get_session_with_retry(max_retries: int = 3) -> Optional[str]:
"""
获取SessionID必要时刷新池 (T-022 支持)。
Args:
max_retries: 最大重试次数
Returns:
Optional[str]: SessionID获取失败返回None
"""
for attempt in range(max_retries):
# 如果池为空,尝试刷新
if session_pool.is_empty:
success = await session_pool.refresh()
if not success:
logger.warning(f"Session pool refresh failed, attempt {attempt + 1}")
continue
session_id = session_pool.get_random()
if session_id:
return session_id
logger.error("Failed to get session after all retries")
return None