"""仪表盘路由 仪表盘数据相关的 API 接口 """ from datetime import datetime, date, timedelta from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy import select, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models import ( Project, ProjectCostSummary, Competitor, CompetitorPrice, PricingPlan, ProfitSimulation, OperationLog, ) from app.schemas.common import ResponseModel from app.schemas.dashboard import ( DashboardSummaryResponse, ProjectOverview, CostOverview, CostProjectInfo, MarketOverview, PricingOverview, StrategiesDistribution, AIUsageOverview, RecentActivity, CostTrendResponse, MarketTrendResponse, TrendDataPoint, ) router = APIRouter() @router.get("/dashboard/summary", response_model=ResponseModel[DashboardSummaryResponse]) async def get_dashboard_summary( db: AsyncSession = Depends(get_db), ): """获取仪表盘概览数据""" # 项目概览 total_projects_result = await db.execute( select(func.count(Project.id)) ) total_projects = total_projects_result.scalar() or 0 active_projects_result = await db.execute( select(func.count(Project.id)).where(Project.is_active == True) ) active_projects = active_projects_result.scalar() or 0 projects_with_pricing_result = await db.execute( select(func.count(func.distinct(PricingPlan.project_id))) ) projects_with_pricing = projects_with_pricing_result.scalar() or 0 project_overview = ProjectOverview( total_projects=total_projects, active_projects=active_projects, projects_with_pricing=projects_with_pricing, ) # 成本概览 avg_cost_result = await db.execute( select(func.avg(ProjectCostSummary.total_cost)) ) avg_project_cost = float(avg_cost_result.scalar() or 0) # 最高成本项目 highest_cost_result = await db.execute( select(ProjectCostSummary).options( ).order_by(ProjectCostSummary.total_cost.desc()).limit(1) ) highest_cost_summary = highest_cost_result.scalar_one_or_none() highest_cost_project = None if highest_cost_summary: project_result = await db.execute( select(Project).where(Project.id == highest_cost_summary.project_id) ) project = project_result.scalar_one_or_none() if project: highest_cost_project = CostProjectInfo( id=project.id, name=project.project_name, cost=float(highest_cost_summary.total_cost), ) # 最低成本项目 lowest_cost_result = await db.execute( select(ProjectCostSummary).where( ProjectCostSummary.total_cost > 0 ).order_by(ProjectCostSummary.total_cost.asc()).limit(1) ) lowest_cost_summary = lowest_cost_result.scalar_one_or_none() lowest_cost_project = None if lowest_cost_summary: project_result = await db.execute( select(Project).where(Project.id == lowest_cost_summary.project_id) ) project = project_result.scalar_one_or_none() if project: lowest_cost_project = CostProjectInfo( id=project.id, name=project.project_name, cost=float(lowest_cost_summary.total_cost), ) cost_overview = CostOverview( avg_project_cost=round(avg_project_cost, 2), highest_cost_project=highest_cost_project, lowest_cost_project=lowest_cost_project, ) # 市场概览 competitors_result = await db.execute( select(func.count(Competitor.id)).where(Competitor.is_active == True) ) competitors_tracked = competitors_result.scalar() or 0 # 本月价格记录数 this_month_start = date.today().replace(day=1) price_records_result = await db.execute( select(func.count(CompetitorPrice.id)).where( CompetitorPrice.collected_at >= this_month_start ) ) price_records_this_month = price_records_result.scalar() or 0 # 市场平均价 avg_market_price_result = await db.execute( select(func.avg(CompetitorPrice.original_price)) ) avg_market_price = avg_market_price_result.scalar() market_overview = MarketOverview( competitors_tracked=competitors_tracked, price_records_this_month=price_records_this_month, avg_market_price=float(avg_market_price) if avg_market_price else None, ) # 定价概览 pricing_plans_result = await db.execute( select(func.count(PricingPlan.id)) ) pricing_plans_count = pricing_plans_result.scalar() or 0 avg_margin_result = await db.execute( select(func.avg(PricingPlan.target_margin)) ) avg_target_margin = avg_margin_result.scalar() # 策略分布 traffic_count_result = await db.execute( select(func.count(PricingPlan.id)).where(PricingPlan.strategy_type == "traffic") ) profit_count_result = await db.execute( select(func.count(PricingPlan.id)).where(PricingPlan.strategy_type == "profit") ) premium_count_result = await db.execute( select(func.count(PricingPlan.id)).where(PricingPlan.strategy_type == "premium") ) pricing_overview = PricingOverview( pricing_plans_count=pricing_plans_count, avg_target_margin=float(avg_target_margin) if avg_target_margin else None, strategies_distribution=StrategiesDistribution( traffic=traffic_count_result.scalar() or 0, profit=profit_count_result.scalar() or 0, premium=premium_count_result.scalar() or 0, ), ) # 最近活动(从操作日志获取) recent_logs_result = await db.execute( select(OperationLog).order_by( OperationLog.created_at.desc() ).limit(10) ) recent_logs = recent_logs_result.scalars().all() recent_activities = [] for log in recent_logs: recent_activities.append(RecentActivity( type=f"{log.module}_{log.action}", project_name=log.target_type, user=None, # 简化处理 time=log.created_at, )) return ResponseModel(data=DashboardSummaryResponse( project_overview=project_overview, cost_overview=cost_overview, market_overview=market_overview, pricing_overview=pricing_overview, ai_usage_this_month=None, # AI 使用统计需要从 ai_call_logs 表获取 recent_activities=recent_activities, )) @router.get("/dashboard/cost-trend", response_model=ResponseModel[CostTrendResponse]) async def get_cost_trend( period: str = Query("month", description="统计周期:week/month/quarter"), db: AsyncSession = Depends(get_db), ): """获取成本趋势数据""" # 根据周期确定时间范围 today = date.today() if period == "week": start_date = today - timedelta(days=7) elif period == "quarter": start_date = today - timedelta(days=90) else: # month start_date = today - timedelta(days=30) # 按日期分组统计平均成本 # 简化实现:返回最近的成本汇总数据 result = await db.execute( select(ProjectCostSummary).order_by( ProjectCostSummary.calculated_at.desc() ).limit(30) ) summaries = result.scalars().all() # 按日期聚合 date_costs = {} for summary in summaries: # 检查 calculated_at 是否为 None if summary.calculated_at is None: continue day = summary.calculated_at.strftime("%Y-%m-%d") if day not in date_costs: date_costs[day] = [] date_costs[day].append(float(summary.total_cost)) data = [] total_cost = 0 for day in sorted(date_costs.keys()): avg = sum(date_costs[day]) / len(date_costs[day]) data.append(TrendDataPoint(date=day, value=round(avg, 2))) total_cost += avg avg_cost = total_cost / len(data) if data else 0 return ResponseModel(data=CostTrendResponse( period=period, data=data, avg_cost=round(avg_cost, 2), )) @router.get("/dashboard/market-trend", response_model=ResponseModel[MarketTrendResponse]) async def get_market_trend( period: str = Query("month", description="统计周期:week/month/quarter"), db: AsyncSession = Depends(get_db), ): """获取市场价格趋势数据""" # 根据周期确定时间范围 today = date.today() if period == "week": start_date = today - timedelta(days=7) elif period == "quarter": start_date = today - timedelta(days=90) else: # month start_date = today - timedelta(days=30) # 获取价格记录 result = await db.execute( select(CompetitorPrice).where( CompetitorPrice.collected_at >= start_date ).order_by(CompetitorPrice.collected_at.desc()) ) prices = result.scalars().all() # 按日期聚合 date_prices = {} for price in prices: # 检查 collected_at 是否为 None if price.collected_at is None: continue day = price.collected_at.strftime("%Y-%m-%d") if day not in date_prices: date_prices[day] = [] date_prices[day].append(float(price.original_price)) data = [] total_price = 0 for day in sorted(date_prices.keys()): avg = sum(date_prices[day]) / len(date_prices[day]) data.append(TrendDataPoint(date=day, value=round(avg, 2))) total_price += avg avg_price = total_price / len(data) if data else 0 return ResponseModel(data=MarketTrendResponse( period=period, data=data, avg_price=round(avg_price, 2), ))