230 lines
6.6 KiB
Python
230 lines
6.6 KiB
Python
|
|
"""
|
|||
|
|
数据分析 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}
|