230 lines
8.6 KiB
Python
230 lines
8.6 KiB
Python
"""效果追踪服务"""
|
||
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,
|
||
}
|