""" 数据分析 API — 从 user_behavior_logs 表聚合 提供概览、日活趋势、Agent 排行榜 """ from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from typing import Optional, List, Any, Dict from sqlalchemy.orm import Session from sqlalchemy import func as sa_func, text, desc from datetime import datetime, timedelta from app.core.database import get_db from app.api.auth import get_current_user from app.models.user import User from app.models.user_behavior import UserBehaviorLog from app.models.chat_message import ChatMessage router = APIRouter(prefix="/api/v1/analytics", tags=["analytics"]) # ─────────── Response Models ─────────── class OverviewResponse(BaseModel): total_users: int = 0 active_users_today: int = 0 messages_today: int = 0 sessions_today: int = 0 class DailyStatItem(BaseModel): date: str active_users: int = 0 messages: int = 0 new_registrations: int = 0 class DailyStatsResponse(BaseModel): stats: List[DailyStatItem] class TopAgentItem(BaseModel): agent_id: str message_count: int = 0 session_count: int = 0 class TopAgentsResponse(BaseModel): agents: List[TopAgentItem] # ─────────── Endpoints ─────────── @router.get("/overview", response_model=OverviewResponse) async def get_overview( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """运营概览:总用户数、今日活跃、今日消息数/会话数""" today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) total_users = db.query(sa_func.count(User.id)).scalar() or 0 active_users_today = db.query(sa_func.count(sa_func.distinct(UserBehaviorLog.user_id))).filter( UserBehaviorLog.created_at >= today ).scalar() or 0 messages_today = db.query(sa_func.count(ChatMessage.id)).filter( ChatMessage.created_at >= today ).scalar() or 0 sessions_today = db.query(sa_func.count(sa_func.distinct(ChatMessage.session_id))).filter( ChatMessage.created_at >= today ).scalar() or 0 return OverviewResponse( total_users=total_users, active_users_today=active_users_today, messages_today=messages_today, sessions_today=sessions_today, ) @router.get("/daily-stats", response_model=DailyStatsResponse) async def get_daily_stats( days: int = Query(default=30, ge=1, le=365), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """每日趋势:UV、消息数、新注册数""" since = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=days - 1) # 每日活跃用户 uv_rows = ( db.query( sa_func.date(UserBehaviorLog.created_at).label("date"), sa_func.count(sa_func.distinct(UserBehaviorLog.user_id)).label("uv"), ) .filter(UserBehaviorLog.created_at >= since) .group_by(text("date")) .order_by("date") .all() ) uv_map = {str(row.date): row.uv for row in uv_rows} # 每日消息 msg_rows = ( db.query( sa_func.date(ChatMessage.created_at).label("date"), sa_func.count(ChatMessage.id).label("cnt"), ) .filter(ChatMessage.created_at >= since) .group_by(text("date")) .order_by("date") .all() ) msg_map = {str(row.date): row.cnt for row in msg_rows} # 每日新注册 reg_rows = ( db.query( sa_func.date(User.created_at).label("date"), sa_func.count(User.id).label("cnt"), ) .filter(User.created_at >= since) .group_by(text("date")) .order_by("date") .all() ) reg_map = {str(row.date): row.cnt for row in reg_rows} # 构建日期序列 stats = [] for i in range(days): d = (since + timedelta(days=i)).strftime("%Y-%m-%d") stats.append(DailyStatItem( date=d, active_users=uv_map.get(d, 0), messages=msg_map.get(d, 0), new_registrations=reg_map.get(d, 0), )) return DailyStatsResponse(stats=stats) @router.get("/top-agents", response_model=TopAgentsResponse) async def get_top_agents( days: int = Query(default=30, ge=1, le=365), limit: int = Query(default=10, ge=1, le=100), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """最活跃 Agent 排行""" since = datetime.now() - timedelta(days=days) rows = ( db.query( ChatMessage.agent_id, sa_func.count(ChatMessage.id).label("message_count"), sa_func.count(sa_func.distinct(ChatMessage.session_id)).label("session_count"), ) .filter( ChatMessage.created_at >= since, ChatMessage.agent_id.isnot(None), ) .group_by(ChatMessage.agent_id) .order_by(desc("message_count")) .limit(limit) .all() ) agents = [ TopAgentItem( agent_id=row.agent_id, message_count=row.message_count, session_count=row.session_count, ) for row in rows ] return TopAgentsResponse(agents=agents) # ─────────── Mobile Events (v1.1.0) ─────────── class AnalyticsEventItem(BaseModel): event_type: str = Field(..., description="事件类型: screen_view / user_action") screen_name: Optional[str] = None category: Optional[str] = None action: Optional[str] = None label: Optional[str] = None agent_id: Optional[str] = None timestamp: Optional[str] = None class AnalyticsEventsRequest(BaseModel): events: List[AnalyticsEventItem] @router.post("/events") async def receive_events( req: AnalyticsEventsRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """接收移动端批量埋点事件""" count = 0 for evt in req.events: try: t = datetime.fromisoformat(evt.timestamp.replace("Z", "+00:00")) if evt.timestamp else datetime.now() except Exception: t = datetime.now() log = UserBehaviorLog( user_id=current_user.id, category=evt.category or "mobile", action=evt.action or evt.event_type, context={ "screen": evt.screen_name, "label": evt.label, "agent_id": evt.agent_id, }, source="android", created_at=t, ) db.add(log) count += 1 db.commit() return {"received": count}