import logging import uuid from datetime import datetime, timedelta, timezone from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.models.monitoring import MonitoringRecord, ContentBaseline from app.models.query import Query from app.models.citation_record import CitationRecord from app.models.brand import Brand logger = logging.getLogger(__name__) class MonitorService: async def create_monitoring_record( self, db: AsyncSession, brand_id: uuid.UUID, content_id: str | None = None, query_keywords: str | None = None, platform: str | None = None, check_interval_hours: int = 24, ) -> MonitoringRecord: now = datetime.now(timezone.utc) stmt = select(Brand).where(Brand.id == brand_id) result = await db.execute(stmt) brand = result.scalar_one_or_none() brand_name = brand.name if brand else "" baseline_data = await self._get_current_metrics(db, brand_id, query_keywords, platform) record = MonitoringRecord( brand_id=brand_id, content_id=content_id, query_keywords=query_keywords, platform=platform, baseline_citation_count=baseline_data.get("citation_count", 0), baseline_sentiment=baseline_data.get("positive_ratio"), baseline_rank=baseline_data.get("avg_rank"), current_citation_count=baseline_data.get("citation_count", 0), current_sentiment=baseline_data.get("positive_ratio"), current_rank=baseline_data.get("avg_rank"), change_type="neutral", change_details=None, check_interval_hours=check_interval_hours, last_checked_at=now, next_check_at=now + timedelta(hours=check_interval_hours), status="active", ) db.add(record) await db.flush() await self._create_baseline_snapshot( db=db, record_id=record.id, brand_name=brand_name, query_keywords=query_keywords, platform=platform, metrics=baseline_data, ) await db.commit() await db.refresh(record) return record async def _create_baseline_snapshot( self, db: AsyncSession, record_id: uuid.UUID, brand_name: str, query_keywords: str | None, platform: str | None, metrics: dict, ) -> ContentBaseline: baseline = ContentBaseline( monitoring_record_id=record_id, brand_name=brand_name, keyword=query_keywords or "", platform=platform or "", citation_count=metrics.get("citation_count", 0), sentiment_score=metrics.get("positive_ratio"), rank_position=metrics.get("avg_rank"), snapshot_data=metrics, ) db.add(baseline) await db.flush() return baseline async def get_brand_monitoring( self, db: AsyncSession, brand_id: uuid.UUID, skip: int = 0, limit: int = 20, ) -> tuple[list[MonitoringRecord], int]: count_stmt = select(func.count()).select_from(MonitoringRecord).where( MonitoringRecord.brand_id == brand_id, ) count_result = await db.execute(count_stmt) total = count_result.scalar_one() stmt = ( select(MonitoringRecord) .where(MonitoringRecord.brand_id == brand_id) .order_by(MonitoringRecord.created_at.desc()) .offset(skip) .limit(limit) ) result = await db.execute(stmt) records = list(result.scalars().all()) return records, total async def check_and_compare( self, db: AsyncSession, record_id: uuid.UUID, ) -> MonitoringRecord | None: stmt = select(MonitoringRecord).where(MonitoringRecord.id == record_id) result = await db.execute(stmt) record = result.scalar_one_or_none() if not record: return None current_data = await self._get_current_metrics( db, record.brand_id, record.query_keywords, record.platform, ) record.current_citation_count = current_data.get("citation_count", 0) record.current_sentiment = current_data.get("positive_ratio") record.current_rank = current_data.get("avg_rank") change_type = self.determine_change_type( baseline_citation=record.baseline_citation_count, current_citation=record.current_citation_count, baseline_sentiment=record.baseline_sentiment, current_sentiment=record.current_sentiment, baseline_rank=record.baseline_rank, current_rank=record.current_rank, ) record.change_type = change_type change_details = self._build_change_details(record, current_data) record.change_details = change_details now = datetime.now(timezone.utc) record.last_checked_at = now record.next_check_at = now + timedelta(hours=record.check_interval_hours) await db.commit() await db.refresh(record) return record def determine_change_type( self, baseline_citation: int, current_citation: int, baseline_sentiment: float | None = None, current_sentiment: float | None = None, baseline_rank: int | None = None, current_rank: int | None = None, ) -> str: positive_signals = 0 negative_signals = 0 if current_citation > baseline_citation: positive_signals += 1 elif current_citation < baseline_citation: negative_signals += 1 if baseline_sentiment is not None and current_sentiment is not None: if current_sentiment > baseline_sentiment: positive_signals += 1 elif current_sentiment < baseline_sentiment: negative_signals += 1 if baseline_rank is not None and current_rank is not None: if current_rank < baseline_rank: positive_signals += 1 elif current_rank > baseline_rank: negative_signals += 1 if positive_signals > negative_signals: return "positive" elif negative_signals > positive_signals: return "negative" return "neutral" def _build_change_details(self, record: MonitoringRecord, current_data: dict) -> dict: details = { "citation_change": { "baseline": record.baseline_citation_count, "current": record.current_citation_count, "delta": record.current_citation_count - record.baseline_citation_count, }, } if record.baseline_sentiment is not None and record.current_sentiment is not None: details["sentiment_change"] = { "baseline": record.baseline_sentiment, "current": record.current_sentiment, "delta": round(record.current_sentiment - record.baseline_sentiment, 4), } if record.baseline_rank is not None and record.current_rank is not None: details["rank_change"] = { "baseline": record.baseline_rank, "current": record.current_rank, "delta": record.current_rank - record.baseline_rank, } details["platform_data"] = current_data.get("platform_data", {}) details["checked_at"] = datetime.now(timezone.utc).isoformat() return details async def generate_change_report( self, db: AsyncSession, record_id: uuid.UUID, ) -> dict | None: stmt = select(MonitoringRecord).where(MonitoringRecord.id == record_id) result = await db.execute(stmt) record = result.scalar_one_or_none() if not record: return None recommendations = self._generate_recommendations(record) baseline = { "citation_count": record.baseline_citation_count, "sentiment": record.baseline_sentiment, "rank": record.baseline_rank, } current = { "citation_count": record.current_citation_count, "sentiment": record.current_sentiment, "rank": record.current_rank, } return { "monitoring_record_id": str(record.id), "brand_id": str(record.brand_id), "change_type": record.change_type, "change_details": record.change_details, "baseline": baseline, "current": current, "recommendations": recommendations, } def _generate_recommendations(self, record: MonitoringRecord) -> list[str]: recommendations = [] if record.change_type == "negative": if record.current_citation_count < record.baseline_citation_count: recommendations.append("引用量下降,建议增加高质量内容发布频率,提升品牌在AI搜索引擎中的曝光") if record.current_sentiment is not None and record.baseline_sentiment is not None: if record.current_sentiment < record.baseline_sentiment: recommendations.append("情感倾向下降,建议关注负面评价并优化品牌形象内容") if record.current_rank is not None and record.baseline_rank is not None: if record.current_rank > record.baseline_rank: recommendations.append("排名下降,建议优化GEO策略,提升内容在AI搜索中的引用优先级") elif record.change_type == "positive": if record.current_citation_count > record.baseline_citation_count: recommendations.append("引用量上升,建议继续保持当前内容策略") if record.current_sentiment is not None and record.baseline_sentiment is not None: if record.current_sentiment > record.baseline_sentiment: recommendations.append("情感倾向改善,当前品牌内容策略效果良好") else: recommendations.append("各项指标保持稳定,建议持续监测") return recommendations async def _get_current_metrics( self, db: AsyncSession, brand_id: uuid.UUID, query_keywords: str | None = None, platform: str | None = None, ) -> dict: stmt = select(Brand).where(Brand.id == brand_id) result = await db.execute(stmt) brand = result.scalar_one_or_none() if not brand: return { "citation_count": 0, "positive_ratio": 0.0, "avg_rank": 0, "platform_data": {}, } conditions = [Query.target_brand == brand.name] if query_keywords: conditions.append(Query.keyword.contains(query_keywords)) queries_stmt = select(Query).where(*conditions) queries_result = await db.execute(queries_stmt) queries = list(queries_result.scalars().all()) if not queries: return { "citation_count": 0, "positive_ratio": 0.0, "avg_rank": 0, "platform_data": {}, } query_ids = [q.id for q in queries] citation_conditions = [CitationRecord.query_id.in_(query_ids)] if platform: citation_conditions.append(CitationRecord.platform == platform) citations_stmt = select(CitationRecord).where(*citation_conditions) citations_result = await db.execute(citations_stmt) all_citations = list(citations_result.scalars().all()) brand_citations = [c for c in all_citations if c.cited] citation_count = len(brand_citations) sentiment_counts = {"positive": 0, "neutral": 0, "negative": 0} for citation in brand_citations: if citation.sentiment and citation.sentiment in sentiment_counts: sentiment_counts[citation.sentiment] += 1 else: sentiment_counts["neutral"] += 1 total_with_sentiment = sum(sentiment_counts.values()) positive_ratio = ( sentiment_counts["positive"] / total_with_sentiment if total_with_sentiment > 0 else 0.0 ) positions = [c.citation_position for c in brand_citations if c.citation_position is not None] avg_rank = int(sum(positions) / len(positions)) if positions else 0 platform_data = {} for citation in all_citations: p = citation.platform if p not in platform_data: platform_data[p] = {"total": 0, "cited": 0} platform_data[p]["total"] += 1 if citation.cited: platform_data[p]["cited"] += 1 platform_scores = {} for p, data in platform_data.items(): platform_scores[p] = round( (data["cited"] / data["total"] * 100) if data["total"] > 0 else 0.0, 2 ) return { "citation_count": citation_count, "positive_ratio": round(positive_ratio, 4), "avg_rank": avg_rank, "platform_data": platform_scores, }