Files
aiagent/backend/app/api/analytics.py
renjianbo 876789fac1 feat: multi-tenant workspace isolation, RBAC, sidebar nav, billing, and Android enhancements
- Backend: workspace_id isolation for 14 model tables + safe migration/backfill
- Backend: RBAC system with 4 roles and 23 permissions, seeded on startup
- Backend: workspace admin endpoints (list/manage all workspaces)
- Backend: admin user management API (CRUD, reset password)
- Backend: billing API with subscription plans, usage tracking, rate limiting
- Backend: fix system_logs.py UNION query and wrong column references
- Backend: WebSocket JWT auth and workspace enforcement
- Frontend: sidebar navigation replacing top dropdown menu
- Frontend: user management page (Users.vue) for admins
- Frontend: enhanced Workspaces.vue with admin table view
- Frontend: workspace RBAC computed properties in user store
- Android: agent marketplace, billing/subscription UI, onboarding wizard
- Android: phone login, analytics tracker, crash handler, network diagnostics
- Android: splash screen, encrypted token storage, app update enhancements
- Docs: multi-tenant RBAC guide with 8 sections and role-permission matrix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-04 01:00:22 +08:00

230 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
数据分析 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}