geo/backend/app/api/dashboard.py

210 lines
7.3 KiB
Python

"""Dashboard API endpoints."""
import uuid
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select, func, Integer
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.database import get_db
from app.models.user import User
from app.models.brand import Brand
from app.models.competitor import Competitor
from app.models.query import Query as QueryModel
from app.models.citation_record import CitationRecord
from app.schemas.dashboard import (
DashboardStatsResponse,
DimensionScoreItem,
PlatformScoreItem,
RecentQueryItem,
)
from app.services.scoring.scoring_service import get_health_level
from app.services.scoring.brand_scoring_data_service import (
get_brand_scoring_data_service,
REQUIRED_PLATFORMS,
)
from app.services.cache import get_cache_service, TTL_DASHBOARD
router = APIRouter()
def _to_uuid(value: str | uuid.UUID) -> uuid.UUID:
if isinstance(value, uuid.UUID):
return value
return uuid.UUID(str(value))
@router.get("/stats", response_model=DashboardStatsResponse)
async def get_dashboard_stats(
brand_id: uuid.UUID | None = Query(None),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
获取看板统计数据
包括:
- 综合评分(V2)和较昨日变化
- 健康等级
- 五维度评分详情
- 各平台评分列表(含竞品对比)
- 竞品地位(领先/落后数量)
- 最近查询记录
"""
cache = get_cache_service()
if brand_id is None:
brand_stmt = select(Brand).where(Brand.user_id == _to_uuid(current_user.id)).limit(1)
brand_result = await db.execute(brand_stmt)
brand = brand_result.scalar_one_or_none()
if brand:
brand_id = brand.id
else:
return DashboardStatsResponse(
overall_score=0.0,
health_level="danger",
score_change=0.0,
platform_scores=[],
recent_queries=[],
dimensions=[],
competitors_ahead=0,
competitors_behind=0,
monitored_platforms=0,
total_platforms=7,
)
cache_key = f"dashboard:stats:{current_user.id}:{brand_id}"
cached = await cache.get_json(cache_key)
if cached is not None:
return cached
brand_stmt = select(Brand).where(Brand.id == brand_id)
brand_result = await db.execute(brand_stmt)
brand = brand_result.scalar_one_or_none()
brand_name = brand.name if brand else None
scoring_data_service = get_brand_scoring_data_service()
scoring_data = await scoring_data_service.get_brand_scoring_data(
db, _to_uuid(current_user.id), brand
)
overall_score = scoring_data.v2_result.overall_score
score_change = scoring_data.change_from_yesterday
dimensions = [
DimensionScoreItem(
name=scoring_data.v2_result.mention_rate.name,
score=round(scoring_data.v2_result.mention_rate.score, 2),
max_score=scoring_data.v2_result.mention_rate.max_score,
percentage=round(scoring_data.v2_result.mention_rate.percentage, 2),
),
DimensionScoreItem(
name=scoring_data.v2_result.recommendation_rank.name,
score=round(scoring_data.v2_result.recommendation_rank.score, 2),
max_score=scoring_data.v2_result.recommendation_rank.max_score,
percentage=round(scoring_data.v2_result.recommendation_rank.percentage, 2),
),
DimensionScoreItem(
name=scoring_data.v2_result.sentiment_score.name,
score=round(scoring_data.v2_result.sentiment_score.score, 2),
max_score=scoring_data.v2_result.sentiment_score.max_score,
percentage=round(scoring_data.v2_result.sentiment_score.percentage, 2),
),
DimensionScoreItem(
name=scoring_data.v2_result.citation_quality.name,
score=round(scoring_data.v2_result.citation_quality.score, 2),
max_score=scoring_data.v2_result.citation_quality.max_score,
percentage=round(scoring_data.v2_result.citation_quality.percentage, 2),
),
DimensionScoreItem(
name=scoring_data.v2_result.competitive_position.name,
score=round(scoring_data.v2_result.competitive_position.score, 2),
max_score=scoring_data.v2_result.competitive_position.max_score,
percentage=round(scoring_data.v2_result.competitive_position.percentage, 2),
),
]
ahead_count = scoring_data.competitor_data.get("ahead_count", 0)
behind_count = scoring_data.competitor_data.get("behind_count", 0)
platform_scores_dict = scoring_data.platform_scores
competitor_scores_dict = await scoring_data_service.get_competitor_platform_scores(
db, _to_uuid(current_user.id), brand_id
)
competitor_stmt = select(Competitor).where(
Competitor.brand_id == brand_id
).limit(1)
competitor_result = await db.execute(competitor_stmt)
first_competitor = competitor_result.scalar_one_or_none()
competitor_name = first_competitor.name if first_competitor else None
platform_scores = [
PlatformScoreItem(
platform=platform,
score=score,
competitor_score=competitor_scores_dict.get(platform),
competitor_name=competitor_name if competitor_scores_dict.get(platform, 0) > 0 else None,
)
for platform, score in platform_scores_dict.items()
]
monitored = sum(1 for s in platform_scores_dict.values() if s > 0)
recent_queries_stmt = (
select(QueryModel)
.where(QueryModel.user_id == current_user.id)
.order_by(QueryModel.created_at.desc())
.limit(10)
)
recent_queries_result = await db.execute(recent_queries_stmt)
recent_queries_list = list(recent_queries_result.scalars().all())
recent_queries = []
for query in recent_queries_list:
citation_count_stmt = select(
func.count().label("total"),
func.sum(
func.cast(
func.case((CitationRecord.cited == True, 1), else_=0),
Integer
)
).label("cited")
).where(CitationRecord.query_id == query.id)
count_result = await db.execute(citation_count_stmt)
count_row = count_result.one()
recent_queries.append(RecentQueryItem(
id=str(query.id),
keyword=query.keyword,
target_brand=query.target_brand,
citation_count=count_row.cited or 0,
queried_at=query.last_queried_at or query.created_at,
))
health_level = get_health_level(overall_score)
response = DashboardStatsResponse(
overall_score=round(overall_score, 2),
health_level=health_level,
score_change=score_change,
platform_scores=platform_scores,
recent_queries=recent_queries,
dimensions=dimensions,
competitors_ahead=ahead_count,
competitors_behind=behind_count,
monitored_platforms=monitored,
total_platforms=7,
brand_name=brand_name,
)
await cache.set_json(
cache_key,
response.model_dump(mode="json"),
expire=TTL_DASHBOARD,
)
return response