9c37c9a574
Phase 4: org_id 注入 JWT/API 过滤/组织管理 CRUD/前端组织列 测试: tests/test_phase_upgrades.py 97项全覆盖 CSS: theme-modern.css 共享 mobile-card-list/status-dot/search-bar 等模式 修复: initial_data.py LLM配置 NOT NULL 约束, TopicResponse 含 org_id
305 lines
11 KiB
Python
305 lines
11 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
|
|
import logging
|
|
|
|
from ..database import get_db
|
|
from ..models import ContentMetrics, Topic, ContentCalendar
|
|
from ..schemas import (
|
|
ContentMetricsCreate, ContentMetricsUpdate, ContentMetricsResponse,
|
|
MetricsDashboard
|
|
)
|
|
from .auth import get_current_user, org_filter
|
|
|
|
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)
|
|
|
|
topic_base = db.query(Topic)
|
|
of = org_filter(current_user, Topic)
|
|
if of is not True:
|
|
topic_base = topic_base.filter(of)
|
|
|
|
total_topics = topic_base.count()
|
|
|
|
raw_status = topic_base.with_entities(Topic.status, func.count()).group_by(Topic.status).all()
|
|
topics_by_status = {}
|
|
for s, cnt in raw_status:
|
|
topics_by_status[s] = cnt
|
|
|
|
metrics_q = db.query(ContentMetrics).join(Topic, ContentMetrics.topic_id == Topic.id)
|
|
if of is not True:
|
|
metrics_q = metrics_q.filter(of)
|
|
total_published = metrics_q.filter(ContentMetrics.views > 0).count()
|
|
|
|
all_metrics = metrics_q.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_q = db.query(
|
|
ContentMetrics.topic_id,
|
|
func.sum(ContentMetrics.views).label("total_views"),
|
|
func.sum(ContentMetrics.likes).label("total_likes")
|
|
).join(Topic, ContentMetrics.topic_id == Topic.id)
|
|
if of is not True:
|
|
top_q = top_q.filter(of)
|
|
top_topics_data = top_q.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_q = db.query(ContentMetrics).join(Topic, ContentMetrics.topic_id == Topic.id)
|
|
if of is not True:
|
|
recent_q = recent_q.filter(of)
|
|
recent_metrics = recent_q.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)
|
|
|
|
q = 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")
|
|
).join(Topic, ContentMetrics.topic_id == Topic.id)
|
|
of_m = org_filter(current_user, Topic)
|
|
if of_m is not True:
|
|
q = q.filter(of_m)
|
|
rows = q.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).join(Topic, ContentMetrics.topic_id == Topic.id)
|
|
of_m = org_filter(current_user, Topic)
|
|
if of_m is not True:
|
|
query = query.filter(of_m)
|
|
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="选题不存在")
|
|
if current_user.role != "admin" and topic.org_id != current_user.org_id:
|
|
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="选题不存在")
|
|
if current_user.role != "admin" and topic.org_id != current_user.org_id:
|
|
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)
|
|
):
|
|
q = 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")
|
|
).join(Topic, ContentMetrics.topic_id == Topic.id)
|
|
of_m = org_filter(current_user, Topic)
|
|
if of_m is not True:
|
|
q = q.filter(of_m)
|
|
rows = q.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)
|
|
):
|
|
try:
|
|
hp_q = db.query(
|
|
ContentMetrics.topic_id,
|
|
func.avg(ContentMetrics.engagement_rate).label("avg_engagement"),
|
|
func.max(ContentMetrics.views).label("max_views")
|
|
).join(Topic, ContentMetrics.topic_id == Topic.id)
|
|
of_m = org_filter(current_user, Topic)
|
|
if of_m is not True:
|
|
hp_q = hp_q.filter(of_m)
|
|
high_performing = hp_q.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(float(row.avg_engagement), 2) if row.avg_engagement else 0,
|
|
"max_views": int(row.max_views) if row.max_views else 0,
|
|
"platforms": list(set(m.platform for m in metrics)),
|
|
"reason": f"平均互动率 {round(float(row.avg_engagement), 2) if row.avg_engagement else 0}%,最高阅读 {int(row.max_views) if row.max_views else 0}"
|
|
})
|
|
|
|
return recommendations[:limit]
|
|
except Exception as e:
|
|
logging.getLogger(__name__).exception(f"推荐选题失败: {e}")
|
|
return [] |