cf5103bbca
主要变更: - 数据库: SQLite → PostgreSQL (yzr_nr) - 选题系统: 硬编码字段 → 配置化 (TopicField/TopicConfigField/TopicStatusConfig) - 新增模型: ContentCalendar, ContentMetrics, MediaAsset, PlatformConfig, ContentTask - 新增 API: topic-config, calendar, metrics, assets, tasks, platform-config - 数据迁移: 现有选题数据迁移到新 schema (field_id/tags/custom_data/scoring_data) - 初始化数据: 10个领域, 5种状态, 3个平台配置 服务运行: http://localhost:8001 默认账号: admin / admin123
269 lines
8.8 KiB
Python
269 lines
8.8 KiB
Python
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] |