geo/backend/app/services/analytics/tracker.py

230 lines
8.6 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.

"""效果追踪服务"""
import logging
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.analytics import PublishRecord, ContentMetrics
logger = logging.getLogger(__name__)
class AnalyticsTracker:
def __init__(self, session: AsyncSession):
self.session = session
async def record_publish(self, organization_id: str, data: dict) -> PublishRecord:
"""记录发布事件"""
record = PublishRecord(
organization_id=organization_id,
content_title=data["content_title"],
content_id=data.get("content_id"),
platform=data["platform"],
published_url=data.get("published_url"),
status=data.get("status", "published"),
published_at=data.get("published_at") or datetime.utcnow(),
)
self.session.add(record)
await self.session.commit()
await self.session.refresh(record)
logger.info("记录发布事件 id=%s platform=%s", record.id, record.platform)
return record
async def update_metrics(self, publish_record_id: str, metrics: dict) -> ContentMetrics:
"""更新/添加效果指标(追加一条快照记录)"""
entry = ContentMetrics(
publish_record_id=publish_record_id,
views=metrics.get("views", 0),
likes=metrics.get("likes", 0),
comments=metrics.get("comments", 0),
shares=metrics.get("shares", 0),
bookmarks=metrics.get("bookmarks", 0),
ai_citation_count=metrics.get("ai_citation_count", 0),
search_impressions=metrics.get("search_impressions", 0),
search_clicks=metrics.get("search_clicks", 0),
avg_read_duration=metrics.get("avg_read_duration", 0.0),
read_completion_rate=metrics.get("read_completion_rate", 0.0),
)
self.session.add(entry)
await self.session.commit()
await self.session.refresh(entry)
return entry
async def get_overview(self, organization_id: str) -> dict:
"""
获取全局概览统计
返回:
total_published, total_views, total_interactions,
total_ai_citations, avg_engagement_rate, platform_distribution
"""
# 取各 publish_record 的最新一条 metrics 做聚合
# 先找该组织所有发布记录
pr_stmt = select(PublishRecord.id, PublishRecord.platform).where(
PublishRecord.organization_id == organization_id
)
pr_result = await self.session.execute(pr_stmt)
records = pr_result.all()
total_published = len(records)
platform_distribution: dict[str, int] = {}
record_ids = []
for r in records:
record_ids.append(r.id)
platform_distribution[r.platform] = platform_distribution.get(r.platform, 0) + 1
if not record_ids:
return {
"total_published": 0,
"total_views": 0,
"total_interactions": 0,
"total_ai_citations": 0,
"avg_engagement_rate": 0.0,
"platform_distribution": {},
}
# 每条发布记录取最新快照
subq = (
select(
ContentMetrics.publish_record_id,
func.max(ContentMetrics.recorded_at).label("latest"),
)
.where(ContentMetrics.publish_record_id.in_(record_ids))
.group_by(ContentMetrics.publish_record_id)
.subquery()
)
metrics_stmt = (
select(ContentMetrics)
.join(
subq,
(ContentMetrics.publish_record_id == subq.c.publish_record_id)
& (ContentMetrics.recorded_at == subq.c.latest),
)
)
metrics_result = await self.session.execute(metrics_stmt)
latest_metrics = metrics_result.scalars().all()
total_views = sum(m.views for m in latest_metrics)
total_interactions = sum(m.likes + m.comments + m.shares for m in latest_metrics)
total_ai_citations = sum(m.ai_citation_count for m in latest_metrics)
# 互动率 = (likes+comments+shares) / views取平均
engagement_rates = []
for m in latest_metrics:
if m.views > 0:
engagement_rates.append((m.likes + m.comments + m.shares) / m.views)
avg_engagement_rate = (
round(sum(engagement_rates) / len(engagement_rates), 4)
if engagement_rates
else 0.0
)
return {
"total_published": total_published,
"total_views": total_views,
"total_interactions": total_interactions,
"total_ai_citations": total_ai_citations,
"avg_engagement_rate": avg_engagement_rate,
"platform_distribution": platform_distribution,
}
async def get_content_performance(self, publish_record_id: str) -> dict:
"""获取单条内容的详细表现(含历史指标快照列表)"""
pr_stmt = select(PublishRecord).where(PublishRecord.id == publish_record_id)
pr_result = await self.session.execute(pr_stmt)
record = pr_result.scalar_one_or_none()
if not record:
return {}
metrics_stmt = (
select(ContentMetrics)
.where(ContentMetrics.publish_record_id == publish_record_id)
.order_by(ContentMetrics.recorded_at.asc())
)
metrics_result = await self.session.execute(metrics_stmt)
metrics_list = metrics_result.scalars().all()
latest = metrics_list[-1] if metrics_list else None
return {
"id": record.id,
"content_title": record.content_title,
"platform": record.platform,
"published_url": record.published_url,
"status": record.status,
"published_at": record.published_at.isoformat() if record.published_at else None,
"latest_metrics": _metrics_to_dict(latest) if latest else None,
"metrics_history": [_metrics_to_dict(m) for m in metrics_list],
}
async def get_top_performing(
self, organization_id: str, limit: int = 10, sort_by: str = "views"
) -> list[dict]:
"""获取表现最好的内容排行"""
ALLOWED_SORT = {
"views", "likes", "comments", "shares",
"ai_citation_count", "read_completion_rate",
}
if sort_by not in ALLOWED_SORT:
sort_by = "views"
pr_stmt = select(PublishRecord.id).where(
PublishRecord.organization_id == organization_id
)
pr_result = await self.session.execute(pr_stmt)
record_ids = [r.id for r in pr_result.all()]
if not record_ids:
return []
# 取每条记录最新快照
subq = (
select(
ContentMetrics.publish_record_id,
func.max(ContentMetrics.recorded_at).label("latest"),
)
.where(ContentMetrics.publish_record_id.in_(record_ids))
.group_by(ContentMetrics.publish_record_id)
.subquery()
)
metrics_stmt = (
select(ContentMetrics, PublishRecord)
.join(
subq,
(ContentMetrics.publish_record_id == subq.c.publish_record_id)
& (ContentMetrics.recorded_at == subq.c.latest),
)
.join(PublishRecord, PublishRecord.id == ContentMetrics.publish_record_id)
.order_by(getattr(ContentMetrics, sort_by).desc())
.limit(limit)
)
result = await self.session.execute(metrics_stmt)
rows = result.all()
output = []
for metrics, pr in rows:
d = _metrics_to_dict(metrics)
d["content_title"] = pr.content_title
d["platform"] = pr.platform
d["published_url"] = pr.published_url
d["publish_record_id"] = pr.id
output.append(d)
return output
def _metrics_to_dict(m: ContentMetrics) -> dict:
return {
"id": m.id,
"publish_record_id": m.publish_record_id,
"recorded_at": m.recorded_at.isoformat() if m.recorded_at else None,
"views": m.views,
"likes": m.likes,
"comments": m.comments,
"shares": m.shares,
"bookmarks": m.bookmarks,
"ai_citation_count": m.ai_citation_count,
"search_impressions": m.search_impressions,
"search_clicks": m.search_clicks,
"avg_read_duration": m.avg_read_duration,
"read_completion_rate": m.read_completion_rate,
}