from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import func, desc from typing import List, Optional from datetime import datetime, timedelta, date from ..database import get_db from ..models import ContentMetrics, Topic, ContentCalendar from ..schemas import ( ContentMetricsCreate, ContentMetricsUpdate, ContentMetricsResponse, MetricsDashboard ) from .auth import get_current_user router = APIRouter(prefix="/api/metrics", tags=["metrics"]) @router.get("/dashboard", response_model=MetricsDashboard) def get_dashboard( days: int = Query(30, ge=1, le=365), db: Session = Depends(get_db), current_user=Depends(get_current_user) ): since = datetime.now() - timedelta(days=days) total_topics = db.query(Topic).count() raw_status = db.query(Topic.status, func.count()).group_by(Topic.status).all() topics_by_status = {} for s, cnt in raw_status: topics_by_status[s] = cnt total_published = db.query(ContentMetrics).filter( ContentMetrics.views > 0 ).count() all_metrics = db.query(ContentMetrics).filter( ContentMetrics.created_at >= since ).all() total_views = sum(m.views for m in all_metrics) total_likes = sum(m.likes for m in all_metrics) engagement_rates = [m.engagement_rate for m in all_metrics if m.views > 0] avg_engagement = sum(engagement_rates) / len(engagement_rates) if engagement_rates else 0 top_topics_data = db.query( ContentMetrics.topic_id, func.sum(ContentMetrics.views).label("total_views"), func.sum(ContentMetrics.likes).label("total_likes") ).join(Topic).filter( ContentMetrics.created_at >= since ).group_by(ContentMetrics.topic_id).order_by(desc("total_views")).limit(10).all() top_topics = [] for row in top_topics_data: topic = db.query(Topic).filter(Topic.id == row.topic_id).first() top_topics.append({ "topic_id": row.topic_id, "title": topic.title if topic else row.topic_id, "total_views": row.total_views or 0, "total_likes": row.total_likes or 0, }) recent_metrics = db.query(ContentMetrics).order_by( ContentMetrics.created_at.desc() ).limit(10).all() return MetricsDashboard( total_topics=total_topics, topics_by_status=topics_by_status, total_published=total_published, total_views=total_views, total_likes=total_likes, avg_engagement_rate=round(avg_engagement, 2), top_topics=top_topics, recent_metrics=[ContentMetricsResponse.model_validate(m) for m in recent_metrics] ) @router.get("/trend") def get_trend( days: int = Query(30, ge=1, le=365), group_by: str = Query("day", enum=["day", "week", "month"]), db: Session = Depends(get_db), current_user=Depends(get_current_user) ): since = datetime.now() - timedelta(days=days) if group_by == "day": date_format = func.date(ContentMetrics.created_at) else: date_format = func.date_trunc(group_by, ContentMetrics.created_at) rows = db.query( date_format.label("period"), func.sum(ContentMetrics.views).label("views"), func.sum(ContentMetrics.likes).label("likes"), func.sum(ContentMetrics.comments).label("comments"), func.count(ContentMetrics.id).label("count") ).filter( ContentMetrics.created_at >= since ).group_by(date_format).order_by(date_format).all() return [ { "period": str(row.period), "views": row.views or 0, "likes": row.likes or 0, "comments": row.comments or 0, "count": row.count or 0 } for row in rows ] @router.get("/entries", response_model=List[ContentMetricsResponse]) def list_metrics( topic_id: Optional[str] = None, platform: Optional[str] = None, limit: int = Query(50, le=200), db: Session = Depends(get_db), current_user=Depends(get_current_user) ): query = db.query(ContentMetrics) if topic_id: query = query.filter(ContentMetrics.topic_id == topic_id) if platform: query = query.filter(ContentMetrics.platform == platform) return query.order_by(ContentMetrics.created_at.desc()).limit(limit).all() @router.post("/entries", response_model=ContentMetricsResponse) def create_metric( data: ContentMetricsCreate, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): topic = db.query(Topic).filter(Topic.id == data.topic_id).first() if not topic: raise HTTPException(status_code=404, detail="选题不存在") existing = db.query(ContentMetrics).filter( ContentMetrics.topic_id == data.topic_id, ContentMetrics.platform == data.platform ).first() if existing: for k, v in data.model_dump(exclude_unset=True).items(): if k != "topic_id" and k != "platform": setattr(existing, k, v) existing.last_fetched = datetime.now() db.commit() db.refresh(existing) return existing metric = ContentMetrics(**data.model_dump(), last_fetched=datetime.now()) db.add(metric) db.commit() db.refresh(metric) return metric @router.put("/entries/{metric_id}", response_model=ContentMetricsResponse) def update_metric( metric_id: int, data: ContentMetricsUpdate, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): metric = db.query(ContentMetrics).filter(ContentMetrics.id == metric_id).first() if not metric: raise HTTPException(status_code=404, detail="数据记录不存在") for k, v in data.model_dump(exclude_unset=True).items(): setattr(metric, k, v) metric.last_fetched = datetime.now() db.commit() db.refresh(metric) return metric @router.delete("/entries/{metric_id}") def delete_metric( metric_id: int, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): metric = db.query(ContentMetrics).filter(ContentMetrics.id == metric_id).first() if not metric: raise HTTPException(status_code=404, detail="数据记录不存在") db.delete(metric) db.commit() return {"ok": True} @router.get("/topics/{topic_id}", response_model=List[ContentMetricsResponse]) def get_topic_metrics( topic_id: str, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): topic = db.query(Topic).filter(Topic.id == topic_id).first() if not topic: raise HTTPException(status_code=404, detail="选题不存在") return db.query(ContentMetrics).filter( ContentMetrics.topic_id == topic_id ).order_by(ContentMetrics.created_at.desc()).all() @router.get("/by-platform") def get_metrics_by_platform( db: Session = Depends(get_db), current_user=Depends(get_current_user) ): rows = db.query( ContentMetrics.platform, func.sum(ContentMetrics.views).label("total_views"), func.sum(ContentMetrics.likes).label("total_likes"), func.sum(ContentMetrics.comments).label("total_comments"), func.count(ContentMetrics.id).label("count") ).group_by(ContentMetrics.platform).all() return [ { "platform": row.platform, "total_views": row.total_views or 0, "total_likes": row.total_likes or 0, "total_comments": row.total_comments or 0, "count": row.count or 0, "avg_views": (row.total_views or 0) / (row.count or 1), "avg_likes": (row.total_likes or 0) / (row.count or 1), } for row in rows ] @router.get("/recommend-topics") def recommend_topics_from_metrics( limit: int = Query(10, le=50), db: Session = Depends(get_db), current_user=Depends(get_current_user) ): high_performing = db.query( ContentMetrics.topic_id, func.avg(ContentMetrics.engagement_rate).label("avg_engagement"), func.max(ContentMetrics.views).label("max_views") ).group_by(ContentMetrics.topic_id).order_by(desc("avg_engagement")).limit(20).all() recommendations = [] for row in high_performing: topic = db.query(Topic).filter(Topic.id == row.topic_id).first() if not topic: continue metrics = db.query(ContentMetrics).filter( ContentMetrics.topic_id == row.topic_id ).all() recommendations.append({ "topic_id": row.topic_id, "title": topic.title, "field": topic.field_name, "status": topic.status, "avg_engagement": round(row.avg_engagement, 2) if row.avg_engagement else 0, "max_views": row.max_views or 0, "platforms": list(set(m.platform for m in metrics)), "reason": f"平均互动率 {round(row.avg_engagement, 2)}%,最高阅读 {row.max_views}" }) return recommendations[:limit]