306 lines
9.8 KiB
Python
306 lines
9.8 KiB
Python
"""仪表盘路由
|
||
|
||
仪表盘数据相关的 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),
|
||
))
|