"""效果追踪服务""" 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, }