feat(core): 完成 Phase 2 核心功能开发
- 实现查询API (query.py): 支持star_id/unique_id/nickname三种查询方式 - 实现计算模块 (calculator.py): CPM/自然搜索UV/搜索成本计算 - 实现品牌API集成 (brand_api.py): 批量并发调用,10并发限制 - 实现导出服务 (export_service.py): Excel/CSV导出 - 前端组件: QueryForm/ResultTable/ExportButton - 主页面集成: 支持6种页面状态 - 测试: 44个测试全部通过,覆盖率88% Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from io import BytesIO
|
||||
|
||||
from app.services.export_service import generate_excel, generate_csv
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 存储最近的查询结果 (简化实现, 生产环境应使用 Redis 等缓存)
|
||||
_cached_data: list = []
|
||||
|
||||
|
||||
def set_export_data(data: list):
|
||||
"""设置导出数据缓存."""
|
||||
global _cached_data
|
||||
_cached_data = data
|
||||
|
||||
|
||||
def get_export_data() -> list:
|
||||
"""获取导出数据缓存."""
|
||||
return _cached_data
|
||||
|
||||
|
||||
@router.get("/export")
|
||||
async def export_data(
|
||||
format: Literal["xlsx", "csv"] = Query("xlsx", description="导出格式"),
|
||||
):
|
||||
"""
|
||||
导出查询结果.
|
||||
|
||||
Args:
|
||||
format: 导出格式 (xlsx 或 csv)
|
||||
|
||||
Returns:
|
||||
文件下载响应
|
||||
"""
|
||||
data = get_export_data()
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
if format == "xlsx":
|
||||
content = generate_excel(data)
|
||||
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
filename = f"kol_data_{timestamp}.xlsx"
|
||||
else:
|
||||
content = generate_csv(data)
|
||||
media_type = "text/csv; charset=utf-8"
|
||||
filename = f"kol_data_{timestamp}.csv"
|
||||
|
||||
return StreamingResponse(
|
||||
BytesIO(content),
|
||||
media_type=media_type,
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"',
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.schemas.query import QueryRequest, QueryResponse, VideoData
|
||||
from app.services.query_service import query_videos
|
||||
from app.services.calculator import calculate_metrics
|
||||
from app.services.brand_api import get_brand_names
|
||||
from app.api.v1.export import set_export_data
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/query", response_model=QueryResponse)
|
||||
async def query(
|
||||
request: QueryRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> QueryResponse:
|
||||
"""
|
||||
批量查询 KOL 视频数据.
|
||||
|
||||
支持三种查询方式:
|
||||
- star_id: 按星图ID精准匹配
|
||||
- unique_id: 按达人unique_id精准匹配
|
||||
- nickname: 按达人昵称模糊匹配
|
||||
"""
|
||||
try:
|
||||
# 1. 查询数据库
|
||||
videos = await query_videos(db, request.type, request.values)
|
||||
|
||||
if not videos:
|
||||
return QueryResponse(success=True, data=[], total=0)
|
||||
|
||||
# 2. 提取品牌ID并批量获取品牌名称
|
||||
brand_ids = [v.brand_id for v in videos if v.brand_id]
|
||||
brand_map = await get_brand_names(brand_ids) if brand_ids else {}
|
||||
|
||||
# 3. 转换为响应模型并计算指标
|
||||
data = []
|
||||
for video in videos:
|
||||
video_data = VideoData.model_validate(video)
|
||||
|
||||
# 填充品牌名称
|
||||
if video.brand_id:
|
||||
video_data.brand_name = brand_map.get(video.brand_id, video.brand_id)
|
||||
|
||||
# 计算预估指标
|
||||
metrics = calculate_metrics(
|
||||
estimated_video_cost=video.estimated_video_cost,
|
||||
natural_play_cnt=video.natural_play_cnt,
|
||||
total_play_cnt=video.total_play_cnt,
|
||||
after_view_search_uv=video.after_view_search_uv,
|
||||
)
|
||||
video_data.estimated_natural_cpm = metrics["estimated_natural_cpm"]
|
||||
video_data.estimated_natural_search_uv = metrics["estimated_natural_search_uv"]
|
||||
video_data.estimated_natural_search_cost = metrics["estimated_natural_search_cost"]
|
||||
|
||||
data.append(video_data)
|
||||
|
||||
# 缓存数据供导出使用
|
||||
set_export_data([d.model_dump() for d in data])
|
||||
|
||||
return QueryResponse(success=True, data=data, total=len(data))
|
||||
|
||||
except Exception as e:
|
||||
return QueryResponse(success=False, data=[], total=0, error=str(e))
|
||||
Reference in New Issue
Block a user