"""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() @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 == 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, 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, 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