feat: 全面升级项目架构 - PostgreSQL迁移 + 配置化改造
主要变更: - 数据库: 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
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
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]
|
||||
Reference in New Issue
Block a user