Files
smart-project-pricing/后端服务/app/routers/dashboard.py
2026-01-31 21:33:06 +08:00

306 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""仪表盘路由
仪表盘数据相关的 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),
))