geo/backend/app/api/analytics.py

260 lines
8.3 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.

"""监测优化 Analytics API"""
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.database import get_db
from app.models.analytics import OptimizationInsight, PublishRecord
from app.models.user import User
from app.schemas.analytics import (
ContentPerformanceResponse,
InsightResponse,
MetricsResponse,
MetricsUpdateRequest,
OverviewStatsResponse,
PublishRecordCreate,
PublishRecordResponse,
TopContentResponse,
)
from app.services.analytics import AnalyticsTracker, InsightGenerator
logger = logging.getLogger(__name__)
router = APIRouter()
# ------------------------------------------------------------------ #
# 辅助获取当前用户所属组织ID
# ------------------------------------------------------------------ #
async def _get_org_id(current_user: User = Depends(get_current_user)) -> str | None:
org_id = getattr(current_user, "organization_id", None)
if not org_id:
return None
return str(org_id)
# ------------------------------------------------------------------ #
# POST /api/v1/analytics/publish - 记录发布
# ------------------------------------------------------------------ #
@router.post(
"/publish",
response_model=PublishRecordResponse,
status_code=status.HTTP_201_CREATED,
summary="记录内容发布",
)
async def record_publish(
body: PublishRecordCreate,
org_id: str | None = Depends(_get_org_id),
db: AsyncSession = Depends(get_db),
):
if not org_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="用户未关联组织,无法记录发布",
)
tracker = AnalyticsTracker(db)
record = await tracker.record_publish(org_id, body.model_dump())
return record
# ------------------------------------------------------------------ #
# PUT /api/v1/analytics/metrics/{publish_id} - 更新指标
# ------------------------------------------------------------------ #
@router.put(
"/metrics/{publish_id}",
response_model=MetricsResponse,
summary="更新内容效果指标(追加快照)",
)
async def update_metrics(
publish_id: str,
body: MetricsUpdateRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
# 检查发布记录是否属于当前组织
org_id = str(getattr(current_user, "organization_id", None) or "")
pr_stmt = select(PublishRecord).where(
PublishRecord.id == publish_id,
PublishRecord.organization_id == org_id,
)
pr_result = await db.execute(pr_stmt)
record = pr_result.scalar_one_or_none()
if not record:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="发布记录不存在或无权限",
)
tracker = AnalyticsTracker(db)
metrics = await tracker.update_metrics(publish_id, body.model_dump())
return metrics
# ------------------------------------------------------------------ #
# GET /api/v1/analytics/overview - 全局概览
# ------------------------------------------------------------------ #
@router.get(
"/overview",
response_model=OverviewStatsResponse,
summary="获取全局效果概览",
)
async def get_overview(
org_id: str | None = Depends(_get_org_id),
db: AsyncSession = Depends(get_db),
):
if not org_id:
return OverviewStatsResponse(
total_published=0,
total_views=0,
total_interactions=0,
total_ai_citations=0,
avg_engagement_rate=0.0,
platform_distribution={},
)
tracker = AnalyticsTracker(db)
overview = await tracker.get_overview(org_id)
return overview
# ------------------------------------------------------------------ #
# GET /api/v1/analytics/content/{publish_id} - 单内容详情
# ------------------------------------------------------------------ #
@router.get(
"/content/{publish_id}",
response_model=ContentPerformanceResponse,
summary="获取单条内容详细表现",
)
async def get_content_performance(
publish_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
org_id = str(getattr(current_user, "organization_id", None) or "")
pr_stmt = select(PublishRecord).where(
PublishRecord.id == publish_id,
PublishRecord.organization_id == org_id,
)
pr_result = await db.execute(pr_stmt)
if not pr_result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="发布记录不存在或无权限",
)
tracker = AnalyticsTracker(db)
performance = await tracker.get_content_performance(publish_id)
if not performance:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="未找到数据")
return performance
# ------------------------------------------------------------------ #
# GET /api/v1/analytics/top - 排行榜
# ------------------------------------------------------------------ #
@router.get(
"/top",
response_model=TopContentResponse,
summary="获取表现最好内容排行",
)
async def get_top_performing(
sort_by: str = Query(default="views", description="排序字段: views/likes/comments/shares/ai_citation_count/read_completion_rate"),
limit: int = Query(default=10, ge=1, le=50),
org_id: str | None = Depends(_get_org_id),
db: AsyncSession = Depends(get_db),
):
if not org_id:
return TopContentResponse(items=[], sort_by=sort_by, total=0)
tracker = AnalyticsTracker(db)
items = await tracker.get_top_performing(org_id, limit=limit, sort_by=sort_by)
return TopContentResponse(items=items, sort_by=sort_by, total=len(items))
# ------------------------------------------------------------------ #
# GET /api/v1/analytics/insights - 洞察列表
# ------------------------------------------------------------------ #
@router.get(
"/insights",
response_model=list[InsightResponse],
summary="获取洞察列表",
)
async def list_insights(
limit: int = Query(default=20, ge=1, le=100),
insight_type: Optional[str] = Query(default=None),
org_id: str | None = Depends(_get_org_id),
db: AsyncSession = Depends(get_db),
):
if not org_id:
return []
stmt = (
select(OptimizationInsight)
.where(OptimizationInsight.organization_id == org_id)
.order_by(OptimizationInsight.created_at.desc())
.limit(limit)
)
if insight_type:
stmt = stmt.where(OptimizationInsight.insight_type == insight_type)
result = await db.execute(stmt)
insights = result.scalars().all()
return insights
# ------------------------------------------------------------------ #
# POST /api/v1/analytics/insights/generate - 触发生成洞察
# ------------------------------------------------------------------ #
@router.post(
"/insights/generate",
response_model=list[dict],
summary="触发AI生成洞察建议",
)
async def generate_insights(
org_id: str | None = Depends(_get_org_id),
db: AsyncSession = Depends(get_db),
):
if not org_id:
return []
generator = InsightGenerator()
insights = await generator.generate_insights(org_id, db)
return insights
# ------------------------------------------------------------------ #
# POST /api/v1/analytics/insights/{insight_id}/apply - 标记洞察已应用
# ------------------------------------------------------------------ #
@router.post(
"/insights/{insight_id}/apply",
response_model=InsightResponse,
summary="标记洞察已应用",
)
async def apply_insight(
insight_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
org_id = str(getattr(current_user, "organization_id", None) or "")
stmt = select(OptimizationInsight).where(
OptimizationInsight.id == insight_id,
OptimizationInsight.organization_id == org_id,
)
result = await db.execute(stmt)
insight = result.scalar_one_or_none()
if not insight:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="洞察不存在或无权限")
insight.applied = True
await db.commit()
await db.refresh(insight)
return insight