- 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>
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}
|