From 3ae63ff27a4e0ba6829072dfcc4576e250c2534f Mon Sep 17 00:00:00 2001 From: zfc Date: Wed, 28 Jan 2026 22:06:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(backend):=20=E4=BF=AE=E5=A4=8D=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84=E4=B8=8E?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E4=B8=8D=E5=8C=B9=E9=85=8D=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 get_video_analysis_data 返回的字段名改为匹配前端 VideoAnalysisData 类型: - cost_metrics_raw -> cost_metrics - cost_metrics_calculated -> calculated_metrics - 字段名统一使用前端期望的命名 Co-Authored-By: Claude Opus 4.5 --- backend/app/services/video_analysis.py | 178 ++++++++++++++----------- 1 file changed, 102 insertions(+), 76 deletions(-) diff --git a/backend/app/services/video_analysis.py b/backend/app/services/video_analysis.py index 8aa6aaf..b6cf419 100644 --- a/backend/app/services/video_analysis.py +++ b/backend/app/services/video_analysis.py @@ -120,13 +120,13 @@ async def get_video_analysis_data( """ 获取视频分析数据(T-024主接口)。 - 包含: - - 基础信息(从数据库) - - 触达指标(从巨量云图API) - - A3指标 - - 搜索指标 - - 费用指标 - - 成本指标(计算得出) + 返回6大类指标(匹配前端 VideoAnalysisData 类型): + - base_info: 基础信息 + - reach_metrics: 触达指标 + - a3_metrics: A3指标 + - search_metrics: 搜索指标 + - cost_metrics: 费用指标 + - calculated_metrics: 成本指标(实时计算) Args: session: 数据库会话 @@ -137,26 +137,26 @@ async def get_video_analysis_data( Raises: ValueError: 视频不存在时抛出 - YuntuAPIError: API调用失败时抛出 """ + from app.services.brand_api import get_brand_names + # 1. 从数据库获取基础信息 video = await get_video_base_info(session, item_id) if video is None: raise ValueError(f"Video not found: {item_id}") - # 2. 构建基础信息 - base_info = { - "item_id": video.item_id, - "title": video.title, - "video_url": video.video_url, - "star_id": video.star_id, - "star_unique_id": video.star_unique_id, - "star_nickname": video.star_nickname, - "publish_time": video.publish_time.isoformat() if video.publish_time else None, - "industry_name": video.industry_name, - } + # 2. 获取品牌名称 + brand_name = "" + if video.brand_id: + brand_map = await get_brand_names([video.brand_id]) + brand_name = brand_map.get(video.brand_id, video.brand_id) + + # 3. 调用巨量云图API获取实时 A3 数据和 cost + a3_increase_cnt = 0 + ad_a3_increase_cnt = 0 + natural_a3_increase_cnt = 0 + api_cost = 0.0 - # 3. 调用巨量云图API获取实时数据 try: publish_time = video.publish_time or datetime.now() industry_id = video.industry_id or "" @@ -166,75 +166,101 @@ async def get_video_analysis_data( publish_time=publish_time, industry_id=industry_id, ) - - # 4. 解析API响应 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 YuntuAPIError as e: - logger.error(f"Failed to get yuntu analysis for {item_id}: {e.message}") - # API失败时,使用数据库中的数据 - analysis_data = { - "total_show_cnt": video.total_play_cnt or 0, - "natural_show_cnt": video.natural_play_cnt or 0, - "ad_show_cnt": video.heated_play_cnt or 0, - "total_play_cnt": video.total_play_cnt or 0, - "natural_play_cnt": video.natural_play_cnt or 0, - "ad_play_cnt": video.heated_play_cnt or 0, - "effective_play_cnt": 0, - "a3_increase_cnt": 0, - "ad_a3_increase_cnt": 0, - "natural_a3_increase_cnt": 0, - "after_view_search_uv": video.after_view_search_uv or 0, - "after_view_search_pv": 0, - "brand_search_uv": 0, - "product_search_uv": 0, - "return_search_cnt": video.return_search_cnt or 0, - "cost": video.estimated_video_cost or 0, - "natural_cost": 0, - "ad_cost": 0, - } + 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 + + # 4. 数据库字段 + estimated_video_cost = video.estimated_video_cost or 0.0 + natural_play_cnt = video.natural_play_cnt or 0 + heated_play_cnt = video.heated_play_cnt or 0 + total_play_cnt = video.total_play_cnt or 0 + after_view_search_uv = video.after_view_search_uv or 0 # 5. 计算成本指标 - cost = analysis_data.get("cost", 0) or (video.estimated_video_cost or 0) - cost_metrics = calculate_cost_metrics( - cost=cost, - natural_play_cnt=analysis_data.get("natural_play_cnt", 0), - a3_increase_cnt=analysis_data.get("a3_increase_cnt", 0), - natural_a3_increase_cnt=analysis_data.get("natural_a3_increase_cnt", 0), - after_view_search_uv=analysis_data.get("after_view_search_uv", 0), - total_play_cnt=analysis_data.get("total_play_cnt", 0), - ) + # 预估加热费用 = max(total_cost - estimated_video_cost, 0) + heated_cost = max(api_cost - estimated_video_cost, 0) if api_cost > estimated_video_cost else 0 - # 6. 组装返回数据 + # 预估自然看后搜人数 + estimated_natural_search_uv = None + if total_play_cnt > 0 and after_view_search_uv > 0: + estimated_natural_search_uv = round((natural_play_cnt / total_play_cnt) * after_view_search_uv, 2) + + # 预估CPM = (total_cost / total_play_cnt) * 1000 + estimated_cpm = round((api_cost / total_play_cnt) * 1000, 2) if total_play_cnt > 0 else None + + # 预估自然CPM = (estimated_video_cost / natural_play_cnt) * 1000 + estimated_natural_cpm = round((estimated_video_cost / natural_play_cnt) * 1000, 2) if natural_play_cnt > 0 else None + + # 预估CPA3 = total_cost / a3_increase_cnt + estimated_cp_a3 = round(api_cost / a3_increase_cnt, 2) if a3_increase_cnt > 0 else None + + # 预估自然CPA3 = estimated_video_cost / natural_a3_increase_cnt + estimated_natural_cp_a3 = round(estimated_video_cost / natural_a3_increase_cnt, 2) if natural_a3_increase_cnt > 0 else None + + # 预估CPsearch = total_cost / after_view_search_uv + estimated_cp_search = round(api_cost / after_view_search_uv, 2) if after_view_search_uv > 0 else None + + # 自然CPsearch = estimated_video_cost / estimated_natural_search_uv + 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 + + # 6. 组装返回数据(匹配前端 VideoAnalysisData 类型) return { - "base_info": base_info, + "base_info": { + "star_nickname": video.star_nickname or "", + "star_unique_id": video.star_unique_id or "", + "vid": video.item_id, + "title": video.title 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, + "video_url": video.video_url or "", + }, "reach_metrics": { - "total_show_cnt": analysis_data.get("total_show_cnt", 0), - "natural_show_cnt": analysis_data.get("natural_show_cnt", 0), - "ad_show_cnt": analysis_data.get("ad_show_cnt", 0), - "total_play_cnt": analysis_data.get("total_play_cnt", 0), - "natural_play_cnt": analysis_data.get("natural_play_cnt", 0), - "ad_play_cnt": analysis_data.get("ad_play_cnt", 0), - "effective_play_cnt": analysis_data.get("effective_play_cnt", 0), + "natural_play_cnt": natural_play_cnt, + "heated_play_cnt": heated_play_cnt, + "total_play_cnt": total_play_cnt, + "total_interaction_cnt": video.total_interact or 0, + "digg_cnt": video.like_cnt or 0, + "share_cnt": video.share_cnt or 0, + "comment_cnt": video.comment_cnt or 0, }, "a3_metrics": { - "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), + "total_new_a3_cnt": a3_increase_cnt, + "heated_new_a3_cnt": ad_a3_increase_cnt, + "natural_new_a3_cnt": natural_a3_increase_cnt, }, "search_metrics": { - "after_view_search_uv": analysis_data.get("after_view_search_uv", 0), - "after_view_search_pv": analysis_data.get("after_view_search_pv", 0), - "brand_search_uv": analysis_data.get("brand_search_uv", 0), - "product_search_uv": analysis_data.get("product_search_uv", 0), - "return_search_cnt": analysis_data.get("return_search_cnt", 0), + "back_search_uv": video.return_search_cnt or 0, + "back_search_cnt": video.return_search_cnt or 0, + "after_view_search_uv": after_view_search_uv, + "after_view_search_cnt": after_view_search_uv, + "estimated_natural_search_uv": estimated_natural_search_uv, }, - "cost_metrics_raw": { - "cost": analysis_data.get("cost", 0), - "natural_cost": analysis_data.get("natural_cost", 0), - "ad_cost": analysis_data.get("ad_cost", 0), + "cost_metrics": { + "total_cost": api_cost, + "heated_cost": heated_cost, + "estimated_video_cost": estimated_video_cost, + }, + "calculated_metrics": { + "estimated_cpm": estimated_cpm, + "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, }, - "cost_metrics_calculated": cost_metrics, }