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>
95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
import asyncio
|
|
from typing import Dict, List, Tuple
|
|
import httpx
|
|
import logging
|
|
|
|
from app.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def fetch_brand_name(
|
|
brand_id: str,
|
|
semaphore: asyncio.Semaphore,
|
|
) -> Tuple[str, str]:
|
|
"""
|
|
获取单个品牌名称.
|
|
|
|
Args:
|
|
brand_id: 品牌ID
|
|
semaphore: 并发控制信号量
|
|
|
|
Returns:
|
|
(brand_id, brand_name) 元组, 失败时 brand_name 为 brand_id
|
|
"""
|
|
async with semaphore:
|
|
try:
|
|
# 构建请求头,包含 Bearer Token 认证 (T-020)
|
|
headers = {}
|
|
if settings.BRAND_API_TOKEN:
|
|
headers["Authorization"] = f"Bearer {settings.BRAND_API_TOKEN}"
|
|
|
|
async with httpx.AsyncClient(
|
|
timeout=settings.BRAND_API_TIMEOUT
|
|
) as client:
|
|
response = await client.get(
|
|
f"{settings.BRAND_API_BASE_URL}/v1/yuntu/brands/{brand_id}",
|
|
headers=headers,
|
|
)
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
# T-019: 正确解析品牌API响应
|
|
# 响应格式: {"total": 1, "data": [{"brand_id": xxx, "brand_name": "xxx"}]}
|
|
if isinstance(data, dict):
|
|
data_list = data.get("data", [])
|
|
if isinstance(data_list, list) and len(data_list) > 0:
|
|
first_item = data_list[0]
|
|
if isinstance(first_item, dict):
|
|
name = first_item.get("brand_name")
|
|
if name:
|
|
return brand_id, name
|
|
except httpx.TimeoutException:
|
|
logger.warning(f"Brand API timeout for brand_id: {brand_id}")
|
|
except httpx.RequestError as e:
|
|
logger.warning(f"Brand API request error for brand_id: {brand_id}, error: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error fetching brand {brand_id}: {e}")
|
|
|
|
# 失败时降级返回 brand_id
|
|
return brand_id, brand_id
|
|
|
|
|
|
async def get_brand_names(brand_ids: List[str]) -> Dict[str, str]:
|
|
"""
|
|
批量获取品牌名称.
|
|
|
|
Args:
|
|
brand_ids: 品牌ID列表
|
|
|
|
Returns:
|
|
brand_id -> brand_name 映射字典
|
|
"""
|
|
# 过滤空值并去重
|
|
unique_ids = list(set(filter(None, brand_ids)))
|
|
|
|
if not unique_ids:
|
|
return {}
|
|
|
|
# 创建并发控制信号量
|
|
semaphore = asyncio.Semaphore(settings.BRAND_API_CONCURRENCY)
|
|
|
|
# 批量并发请求
|
|
tasks = [fetch_brand_name(brand_id, semaphore) for brand_id in unique_ids]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
# 构建映射表
|
|
brand_map: Dict[str, str] = {}
|
|
for result in results:
|
|
if isinstance(result, tuple):
|
|
brand_id, brand_name = result
|
|
brand_map[brand_id] = brand_name
|
|
elif isinstance(result, Exception):
|
|
logger.error(f"Error in batch brand fetch: {result}")
|
|
|
|
return brand_map
|