feat(backend): 视频分析模块增加缓存优先策略和并发API调用
- SessionPool 新增 get_distinct_configs 方法,支持获取不同配置用于并发调用 - video_analysis 重构为缓存优先策略:数据库有 A3/Cost 数据时直接使用 - 并发 API 调用预分配不同 cookie,避免 session 冲突 - API 数据写回数据库,实现下次查询缓存命中 - 新增 heated_cost 字段追踪 - 测试全面重写,覆盖缓存/API/混合/降级场景
This commit is contained in:
@@ -11,8 +11,8 @@ T-027 修复:
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
from typing import Dict, Optional, Any, List
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -150,6 +150,46 @@ class SessionPool:
|
||||
"""检查池是否为空"""
|
||||
return len(self._configs) == 0
|
||||
|
||||
def get_distinct_configs(self, count: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取 count 个不同的配置,用于并发调用。
|
||||
|
||||
- 池中配置 >= count:随机抽样 count 个不重复的
|
||||
- 池中配置 < count:全部取出,循环复用补足
|
||||
- 池为空:返回空列表
|
||||
|
||||
Args:
|
||||
count: 需要的配置数量
|
||||
|
||||
Returns:
|
||||
List[Dict]: 配置字典列表
|
||||
"""
|
||||
if not self._configs or count <= 0:
|
||||
return []
|
||||
|
||||
def _to_dict(config: CookieConfig) -> Dict[str, Any]:
|
||||
return {
|
||||
"brand_id": config.brand_id,
|
||||
"aadvid": config.aadvid,
|
||||
"auth_token": config.auth_token,
|
||||
"industry_id": config.industry_id,
|
||||
"brand_name": config.brand_name,
|
||||
}
|
||||
|
||||
if len(self._configs) >= count:
|
||||
sampled = random.sample(self._configs, count)
|
||||
return [_to_dict(c) for c in sampled]
|
||||
|
||||
# 池中配置不足,全部取出后循环复用
|
||||
result = [_to_dict(c) for c in self._configs]
|
||||
shuffled = list(self._configs)
|
||||
random.shuffle(shuffled)
|
||||
idx = 0
|
||||
while len(result) < count:
|
||||
result.append(_to_dict(shuffled[idx % len(shuffled)]))
|
||||
idx += 1
|
||||
return result
|
||||
|
||||
# 兼容旧接口
|
||||
def get_random(self) -> Optional[str]:
|
||||
"""兼容旧接口:随机获取一个 SessionID"""
|
||||
@@ -218,6 +258,32 @@ async def get_session_with_retry(max_retries: int = 3) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
async def get_distinct_configs(count: int, max_retries: int = 3) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取 count 个不同的配置,必要时刷新池。
|
||||
|
||||
Args:
|
||||
count: 需要的配置数量
|
||||
max_retries: 最大重试次数
|
||||
|
||||
Returns:
|
||||
List[Dict]: 配置字典列表
|
||||
"""
|
||||
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
|
||||
|
||||
configs = session_pool.get_distinct_configs(count)
|
||||
if configs:
|
||||
return configs
|
||||
|
||||
logger.error("Failed to get distinct configs after all retries")
|
||||
return []
|
||||
|
||||
|
||||
async def get_config_for_brand(brand_id: str, max_retries: int = 3) -> Optional[Any]:
|
||||
"""
|
||||
兼容旧接口:获取品牌对应的配置。
|
||||
|
||||
@@ -4,25 +4,43 @@
|
||||
实现视频分析数据获取和成本指标计算。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, Any
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from sqlalchemy import update
|
||||
|
||||
from app.models.kol_video import KolVideo
|
||||
from app.services.session_pool import (
|
||||
get_distinct_configs,
|
||||
get_random_config,
|
||||
session_pool,
|
||||
)
|
||||
from app.services.yuntu_api import (
|
||||
SessionInvalidError,
|
||||
call_yuntu_api,
|
||||
parse_analysis_response,
|
||||
)
|
||||
from app.services.yuntu_api import (
|
||||
get_video_analysis as fetch_yuntu_analysis,
|
||||
parse_analysis_response,
|
||||
YuntuAPIError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _needs_api_call(video: KolVideo) -> bool:
|
||||
"""
|
||||
判断是否需要调用 Yuntu API 获取 A3/Cost 数据。
|
||||
|
||||
如果数据库中已有 A3 或 Cost 数据,直接使用数据库数据,不调 API。
|
||||
"""
|
||||
has_a3 = (video.total_new_a3_cnt or 0) > 0
|
||||
has_cost = (video.total_cost or 0) > 0
|
||||
return not (has_a3 or has_cost)
|
||||
|
||||
|
||||
def calculate_cost_metrics(
|
||||
cost: float,
|
||||
natural_play_cnt: int,
|
||||
@@ -151,33 +169,58 @@ async def get_video_analysis_data(
|
||||
brand_map = await get_brand_names([video.brand_id])
|
||||
brand_name = brand_map.get(video.brand_id, video.brand_id)
|
||||
|
||||
# 3. 调用巨量云图API获取实时 A3 数据和 cost
|
||||
# 3. 获取 A3 数据和 cost(缓存优先策略)
|
||||
a3_increase_cnt = 0
|
||||
ad_a3_increase_cnt = 0
|
||||
natural_a3_increase_cnt = 0
|
||||
api_cost = 0.0
|
||||
ad_cost = 0.0
|
||||
|
||||
try:
|
||||
publish_time = video.publish_time or datetime.now()
|
||||
industry_id = video.industry_id or ""
|
||||
|
||||
api_response = await fetch_yuntu_analysis(
|
||||
item_id=item_id,
|
||||
publish_time=publish_time,
|
||||
industry_id=industry_id,
|
||||
)
|
||||
analysis_data = parse_analysis_response(api_response)
|
||||
a3_increase_cnt = analysis_data.get("a3_increase_cnt", 0)
|
||||
ad_a3_increase_cnt = analysis_data.get("ad_a3_increase_cnt", 0)
|
||||
natural_a3_increase_cnt = analysis_data.get("natural_a3_increase_cnt", 0)
|
||||
api_cost = analysis_data.get("cost", 0)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"API failed for {item_id}: {e}, using DB data")
|
||||
if not _needs_api_call(video):
|
||||
# 数据库已有数据,直接使用
|
||||
logger.info(f"Using DB data for {item_id} (A3/Cost already cached)")
|
||||
a3_increase_cnt = video.total_new_a3_cnt or 0
|
||||
ad_a3_increase_cnt = video.heated_new_a3_cnt or 0
|
||||
natural_a3_increase_cnt = video.natural_new_a3_cnt or 0
|
||||
api_cost = video.total_cost or 0.0
|
||||
ad_cost = video.heated_cost or 0.0
|
||||
else:
|
||||
# 需要调用 API 获取数据
|
||||
try:
|
||||
publish_time = video.publish_time or datetime.now()
|
||||
industry_id = video.industry_id or ""
|
||||
|
||||
api_response = await fetch_yuntu_analysis(
|
||||
item_id=item_id,
|
||||
publish_time=publish_time,
|
||||
industry_id=industry_id,
|
||||
)
|
||||
analysis_data = parse_analysis_response(api_response)
|
||||
a3_increase_cnt = analysis_data.get("a3_increase_cnt", 0)
|
||||
ad_a3_increase_cnt = analysis_data.get("ad_a3_increase_cnt", 0)
|
||||
natural_a3_increase_cnt = analysis_data.get("natural_a3_increase_cnt", 0)
|
||||
api_cost = analysis_data.get("cost", 0)
|
||||
ad_cost = analysis_data.get("ad_cost", 0)
|
||||
|
||||
# 写回数据库
|
||||
await update_video_a3_metrics(
|
||||
session=session,
|
||||
item_id=item_id,
|
||||
total_new_a3_cnt=int(a3_increase_cnt),
|
||||
heated_new_a3_cnt=int(ad_a3_increase_cnt),
|
||||
natural_new_a3_cnt=int(natural_a3_increase_cnt),
|
||||
total_cost=float(api_cost),
|
||||
heated_cost=float(ad_cost),
|
||||
)
|
||||
logger.info(f"API data fetched and saved to DB for {item_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"API failed for {item_id}: {e}, using DB data")
|
||||
a3_increase_cnt = video.total_new_a3_cnt or 0
|
||||
ad_a3_increase_cnt = video.heated_new_a3_cnt or 0
|
||||
natural_a3_increase_cnt = video.natural_new_a3_cnt or 0
|
||||
api_cost = video.total_cost or 0.0
|
||||
ad_cost = video.heated_cost or 0.0
|
||||
|
||||
# 4. 数据库字段
|
||||
estimated_video_cost = video.estimated_video_cost or 0.0
|
||||
@@ -187,8 +230,7 @@ async def get_video_analysis_data(
|
||||
after_view_search_uv = video.after_view_search_uv or 0
|
||||
|
||||
# 5. 计算成本指标
|
||||
# 预估加热费用 = max(total_cost - estimated_video_cost, 0)
|
||||
heated_cost = max(api_cost - estimated_video_cost, 0) if api_cost > estimated_video_cost else 0
|
||||
heated_cost = ad_cost
|
||||
|
||||
# 预估自然看后搜人数
|
||||
estimated_natural_search_uv = None
|
||||
@@ -271,9 +313,10 @@ async def update_video_a3_metrics(
|
||||
heated_new_a3_cnt: int,
|
||||
natural_new_a3_cnt: int,
|
||||
total_cost: float,
|
||||
heated_cost: float = 0.0,
|
||||
) -> bool:
|
||||
"""
|
||||
更新数据库中的A3指标 (T-025)。
|
||||
更新数据库中的A3指标和费用数据 (T-025)。
|
||||
|
||||
Args:
|
||||
session: 数据库会话
|
||||
@@ -281,7 +324,8 @@ async def update_video_a3_metrics(
|
||||
total_new_a3_cnt: 总新增A3
|
||||
heated_new_a3_cnt: 加热新增A3
|
||||
natural_new_a3_cnt: 自然新增A3
|
||||
total_cost: 总花费
|
||||
total_cost: 预估总费用
|
||||
heated_cost: 预估加热费用
|
||||
|
||||
Returns:
|
||||
bool: 更新是否成功
|
||||
@@ -295,6 +339,7 @@ async def update_video_a3_metrics(
|
||||
heated_new_a3_cnt=heated_new_a3_cnt,
|
||||
natural_new_a3_cnt=natural_new_a3_cnt,
|
||||
total_cost=total_cost,
|
||||
heated_cost=heated_cost,
|
||||
)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
@@ -313,39 +358,6 @@ async def update_video_a3_metrics(
|
||||
return False
|
||||
|
||||
|
||||
async def get_and_update_video_analysis(
|
||||
session: AsyncSession, item_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取视频分析数据并更新数据库中的A3指标 (T-024 + T-025 组合)。
|
||||
|
||||
Args:
|
||||
session: 数据库会话
|
||||
item_id: 视频ID
|
||||
|
||||
Returns:
|
||||
Dict: 完整的视频分析数据
|
||||
"""
|
||||
# 获取分析数据
|
||||
result = await get_video_analysis_data(session, item_id)
|
||||
|
||||
# 提取A3指标
|
||||
a3_metrics = result.get("a3_metrics", {})
|
||||
cost_raw = result.get("cost_metrics_raw", {})
|
||||
|
||||
# 更新数据库
|
||||
await update_video_a3_metrics(
|
||||
session=session,
|
||||
item_id=item_id,
|
||||
total_new_a3_cnt=a3_metrics.get("a3_increase_cnt", 0),
|
||||
heated_new_a3_cnt=a3_metrics.get("ad_a3_increase_cnt", 0),
|
||||
natural_new_a3_cnt=a3_metrics.get("natural_a3_increase_cnt", 0),
|
||||
total_cost=cost_raw.get("cost", 0),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def search_videos_by_star_id(
|
||||
session: AsyncSession, star_id: str
|
||||
) -> list[KolVideo]:
|
||||
@@ -373,11 +385,60 @@ async def search_videos_by_nickname(
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
def _build_video_list_item(
|
||||
video: KolVideo,
|
||||
a3_increase_cnt: int,
|
||||
ad_a3_increase_cnt: int,
|
||||
natural_a3_increase_cnt: int,
|
||||
api_cost: float,
|
||||
brand_name: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""构建视频列表项的结果字典。"""
|
||||
estimated_video_cost = video.estimated_video_cost or 0.0
|
||||
natural_play_cnt = video.natural_play_cnt or 0
|
||||
total_play_cnt = video.total_play_cnt or 0
|
||||
after_view_search_uv = video.after_view_search_uv or 0
|
||||
|
||||
estimated_natural_search_uv = None
|
||||
if total_play_cnt > 0 and after_view_search_uv > 0:
|
||||
estimated_natural_search_uv = (natural_play_cnt / total_play_cnt) * after_view_search_uv
|
||||
|
||||
estimated_natural_cpm = round((estimated_video_cost / natural_play_cnt) * 1000, 2) if natural_play_cnt > 0 else None
|
||||
estimated_cp_a3 = round(api_cost / a3_increase_cnt, 2) if a3_increase_cnt > 0 else None
|
||||
estimated_natural_cp_a3 = round(estimated_video_cost / natural_a3_increase_cnt, 2) if natural_a3_increase_cnt > 0 else None
|
||||
estimated_cp_search = round(api_cost / after_view_search_uv, 2) if after_view_search_uv > 0 else None
|
||||
estimated_natural_cp_search = round(estimated_video_cost / estimated_natural_search_uv, 2) if estimated_natural_search_uv and estimated_natural_search_uv > 0 else None
|
||||
|
||||
return {
|
||||
"item_id": video.item_id,
|
||||
"star_nickname": video.star_nickname or "",
|
||||
"title": video.title or "",
|
||||
"video_url": video.video_url or "",
|
||||
"create_date": video.publish_time.isoformat() if video.publish_time else None,
|
||||
"hot_type": video.viral_type or "",
|
||||
"industry_id": video.industry_id or "",
|
||||
"brand_id": video.brand_id or "",
|
||||
"brand_name": brand_name,
|
||||
"total_new_a3_cnt": a3_increase_cnt,
|
||||
"heated_new_a3_cnt": ad_a3_increase_cnt,
|
||||
"natural_new_a3_cnt": natural_a3_increase_cnt,
|
||||
"estimated_natural_cpm": estimated_natural_cpm,
|
||||
"estimated_cp_a3": estimated_cp_a3,
|
||||
"estimated_natural_cp_a3": estimated_natural_cp_a3,
|
||||
"estimated_cp_search": estimated_cp_search,
|
||||
"estimated_natural_cp_search": estimated_natural_cp_search,
|
||||
}
|
||||
|
||||
|
||||
async def get_video_list_with_a3(
|
||||
session: AsyncSession, videos: list[KolVideo]
|
||||
) -> list[Dict[str, Any]]:
|
||||
"""
|
||||
获取视频列表的摘要数据(实时调用云图API获取A3数据)。
|
||||
获取视频列表的摘要数据。
|
||||
|
||||
缓存优先策略:
|
||||
- 数据库有 A3/Cost 数据 → 直接使用
|
||||
- 数据库无数据 → 并发调用云图 API(预分配不同 cookie)→ 写回数据库
|
||||
"""
|
||||
from app.services.brand_api import get_brand_names
|
||||
|
||||
@@ -385,73 +446,149 @@ async def get_video_list_with_a3(
|
||||
brand_ids = [video.brand_id for video in videos if video.brand_id]
|
||||
brand_map = await get_brand_names(brand_ids) if brand_ids else {}
|
||||
|
||||
result = []
|
||||
for video in videos:
|
||||
# 实时调用云图 API 获取 A3 数据和 cost
|
||||
a3_increase_cnt = 0
|
||||
ad_a3_increase_cnt = 0
|
||||
natural_a3_increase_cnt = 0
|
||||
api_cost = 0.0
|
||||
# 分组:已有数据 vs 需要 API 调用
|
||||
cached_videos: list[tuple[int, KolVideo]] = [] # (原始索引, video)
|
||||
api_videos: list[tuple[int, KolVideo]] = [] # (原始索引, video)
|
||||
|
||||
try:
|
||||
publish_time = video.publish_time or datetime.now()
|
||||
industry_id = video.industry_id or ""
|
||||
for idx, video in enumerate(videos):
|
||||
if _needs_api_call(video):
|
||||
api_videos.append((idx, video))
|
||||
else:
|
||||
cached_videos.append((idx, video))
|
||||
|
||||
api_response = await fetch_yuntu_analysis(
|
||||
item_id=video.item_id,
|
||||
publish_time=publish_time,
|
||||
industry_id=industry_id,
|
||||
logger.info(
|
||||
f"Video list: {len(cached_videos)} cached, {len(api_videos)} need API"
|
||||
)
|
||||
|
||||
# 结果数组(按原始索引填充)
|
||||
results: list[Optional[Dict[str, Any]]] = [None] * len(videos)
|
||||
|
||||
# 组 A:直接用数据库数据
|
||||
for idx, video in cached_videos:
|
||||
brand_name = brand_map.get(video.brand_id, video.brand_id or "") if video.brand_id else ""
|
||||
results[idx] = _build_video_list_item(
|
||||
video=video,
|
||||
a3_increase_cnt=video.total_new_a3_cnt or 0,
|
||||
ad_a3_increase_cnt=video.heated_new_a3_cnt or 0,
|
||||
natural_a3_increase_cnt=video.natural_new_a3_cnt or 0,
|
||||
api_cost=video.total_cost or 0.0,
|
||||
brand_name=brand_name,
|
||||
)
|
||||
|
||||
# 组 B:并发调用 API(预分配不同 cookie)
|
||||
if api_videos:
|
||||
configs = await get_distinct_configs(len(api_videos))
|
||||
semaphore = asyncio.Semaphore(5)
|
||||
# 收集需要写回 DB 的数据(避免并发 session 操作)
|
||||
pending_updates: list[Dict[str, Any]] = []
|
||||
|
||||
async def _fetch_single(
|
||||
idx: int, video: KolVideo, config: Dict[str, Any]
|
||||
) -> None:
|
||||
a3_increase_cnt = 0
|
||||
ad_a3_increase_cnt = 0
|
||||
natural_a3_increase_cnt = 0
|
||||
api_cost = 0.0
|
||||
ad_cost_val = 0.0
|
||||
api_success = False
|
||||
|
||||
async with semaphore:
|
||||
try:
|
||||
publish_time = video.publish_time or datetime.now()
|
||||
industry_id = video.industry_id or ""
|
||||
|
||||
api_response = await call_yuntu_api(
|
||||
item_id=video.item_id,
|
||||
publish_time=publish_time,
|
||||
industry_id=industry_id,
|
||||
aadvid=config["aadvid"],
|
||||
auth_token=config["auth_token"],
|
||||
)
|
||||
api_data = parse_analysis_response(api_response)
|
||||
a3_increase_cnt = api_data.get("a3_increase_cnt", 0)
|
||||
ad_a3_increase_cnt = api_data.get("ad_a3_increase_cnt", 0)
|
||||
natural_a3_increase_cnt = api_data.get("natural_a3_increase_cnt", 0)
|
||||
api_cost = api_data.get("cost", 0)
|
||||
ad_cost_val = api_data.get("ad_cost", 0)
|
||||
api_success = True
|
||||
|
||||
except SessionInvalidError:
|
||||
# Session 失效,从池中移除,重新获取随机 config 重试
|
||||
session_pool.remove_by_auth_token(config["auth_token"])
|
||||
logger.warning(f"Session invalid for {video.item_id}, retrying")
|
||||
retry_config = await get_random_config()
|
||||
if retry_config:
|
||||
try:
|
||||
publish_time = video.publish_time or datetime.now()
|
||||
industry_id = video.industry_id or ""
|
||||
api_response = await call_yuntu_api(
|
||||
item_id=video.item_id,
|
||||
publish_time=publish_time,
|
||||
industry_id=industry_id,
|
||||
aadvid=retry_config["aadvid"],
|
||||
auth_token=retry_config["auth_token"],
|
||||
)
|
||||
api_data = parse_analysis_response(api_response)
|
||||
a3_increase_cnt = api_data.get("a3_increase_cnt", 0)
|
||||
ad_a3_increase_cnt = api_data.get("ad_a3_increase_cnt", 0)
|
||||
natural_a3_increase_cnt = api_data.get("natural_a3_increase_cnt", 0)
|
||||
api_cost = api_data.get("cost", 0)
|
||||
ad_cost_val = api_data.get("ad_cost", 0)
|
||||
api_success = True
|
||||
except Exception as e2:
|
||||
logger.warning(f"Retry failed for {video.item_id}: {e2}")
|
||||
a3_increase_cnt = video.total_new_a3_cnt or 0
|
||||
ad_a3_increase_cnt = video.heated_new_a3_cnt or 0
|
||||
natural_a3_increase_cnt = video.natural_new_a3_cnt or 0
|
||||
api_cost = video.total_cost or 0.0
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"API failed for {video.item_id}: {e}")
|
||||
a3_increase_cnt = video.total_new_a3_cnt or 0
|
||||
ad_a3_increase_cnt = video.heated_new_a3_cnt or 0
|
||||
natural_a3_increase_cnt = video.natural_new_a3_cnt or 0
|
||||
api_cost = video.total_cost or 0.0
|
||||
|
||||
# 收集待写回 DB 的数据(不在并发中操作 session)
|
||||
if api_success:
|
||||
pending_updates.append({
|
||||
"item_id": video.item_id,
|
||||
"total_new_a3_cnt": int(a3_increase_cnt),
|
||||
"heated_new_a3_cnt": int(ad_a3_increase_cnt),
|
||||
"natural_new_a3_cnt": int(natural_a3_increase_cnt),
|
||||
"total_cost": float(api_cost),
|
||||
"heated_cost": float(ad_cost_val),
|
||||
})
|
||||
|
||||
brand_name = brand_map.get(video.brand_id, video.brand_id or "") if video.brand_id else ""
|
||||
results[idx] = _build_video_list_item(
|
||||
video=video,
|
||||
a3_increase_cnt=a3_increase_cnt,
|
||||
ad_a3_increase_cnt=ad_a3_increase_cnt,
|
||||
natural_a3_increase_cnt=natural_a3_increase_cnt,
|
||||
api_cost=api_cost,
|
||||
brand_name=brand_name,
|
||||
)
|
||||
api_data = parse_analysis_response(api_response)
|
||||
a3_increase_cnt = api_data.get("a3_increase_cnt", 0)
|
||||
ad_a3_increase_cnt = api_data.get("ad_a3_increase_cnt", 0)
|
||||
natural_a3_increase_cnt = api_data.get("natural_a3_increase_cnt", 0)
|
||||
api_cost = api_data.get("cost", 0)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"API failed for {video.item_id}: {e}")
|
||||
a3_increase_cnt = video.total_new_a3_cnt or 0
|
||||
ad_a3_increase_cnt = video.heated_new_a3_cnt or 0
|
||||
natural_a3_increase_cnt = video.natural_new_a3_cnt or 0
|
||||
api_cost = video.total_cost or 0.0
|
||||
# 为每个视频分配一个独立的 config,并发执行
|
||||
tasks = []
|
||||
for i, (idx, video) in enumerate(api_videos):
|
||||
config = configs[i] if i < len(configs) else configs[i % len(configs)] if configs else {}
|
||||
tasks.append(_fetch_single(idx, video, config))
|
||||
|
||||
# 数据库字段
|
||||
estimated_video_cost = video.estimated_video_cost or 0.0
|
||||
natural_play_cnt = video.natural_play_cnt or 0
|
||||
total_play_cnt = video.total_play_cnt or 0
|
||||
after_view_search_uv = video.after_view_search_uv or 0
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# 计算成本指标
|
||||
estimated_natural_search_uv = None
|
||||
if total_play_cnt > 0 and after_view_search_uv > 0:
|
||||
estimated_natural_search_uv = (natural_play_cnt / total_play_cnt) * after_view_search_uv
|
||||
# 顺序写回 DB(避免并发 session 操作导致状态损坏)
|
||||
for upd in pending_updates:
|
||||
await update_video_a3_metrics(
|
||||
session=session,
|
||||
item_id=upd["item_id"],
|
||||
total_new_a3_cnt=upd["total_new_a3_cnt"],
|
||||
heated_new_a3_cnt=upd["heated_new_a3_cnt"],
|
||||
natural_new_a3_cnt=upd["natural_new_a3_cnt"],
|
||||
total_cost=upd["total_cost"],
|
||||
heated_cost=upd["heated_cost"],
|
||||
)
|
||||
|
||||
estimated_natural_cpm = round((estimated_video_cost / natural_play_cnt) * 1000, 2) if natural_play_cnt > 0 else None
|
||||
estimated_cp_a3 = round(api_cost / a3_increase_cnt, 2) if a3_increase_cnt > 0 else None
|
||||
estimated_natural_cp_a3 = round(estimated_video_cost / natural_a3_increase_cnt, 2) if natural_a3_increase_cnt > 0 else None
|
||||
estimated_cp_search = round(api_cost / after_view_search_uv, 2) if after_view_search_uv > 0 else None
|
||||
estimated_natural_cp_search = round(estimated_video_cost / estimated_natural_search_uv, 2) if estimated_natural_search_uv and estimated_natural_search_uv > 0 else None
|
||||
|
||||
brand_name = brand_map.get(video.brand_id, video.brand_id) if video.brand_id else ""
|
||||
|
||||
result.append({
|
||||
"item_id": video.item_id,
|
||||
"star_nickname": video.star_nickname or "",
|
||||
"title": video.title or "",
|
||||
"video_url": video.video_url or "",
|
||||
"create_date": video.publish_time.isoformat() if video.publish_time else None,
|
||||
"hot_type": video.viral_type or "",
|
||||
"industry_id": video.industry_id or "",
|
||||
"brand_id": video.brand_id or "",
|
||||
"brand_name": brand_name,
|
||||
"total_new_a3_cnt": a3_increase_cnt,
|
||||
"heated_new_a3_cnt": ad_a3_increase_cnt,
|
||||
"natural_new_a3_cnt": natural_a3_increase_cnt,
|
||||
"estimated_natural_cpm": estimated_natural_cpm,
|
||||
"estimated_cp_a3": estimated_cp_a3,
|
||||
"estimated_natural_cp_a3": estimated_natural_cp_a3,
|
||||
"estimated_cp_search": estimated_cp_search,
|
||||
"estimated_natural_cp_search": estimated_natural_cp_search,
|
||||
})
|
||||
|
||||
return result
|
||||
# 过滤 None(不应发生,防御性编程)
|
||||
return [r for r in results if r is not None]
|
||||
|
||||
Reference in New Issue
Block a user