From b01884407887dca4ae3f637a6da12f7f199f7836 Mon Sep 17 00:00:00 2001 From: 111 Date: Fri, 23 Jan 2026 16:12:18 +0800 Subject: [PATCH] fix: add GET endpoints for stats summary and logs query --- backend/app/routers/logs.py | 74 ++++++++++++++++++++++++++++- backend/app/routers/stats.py | 92 +++++++++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 4 deletions(-) diff --git a/backend/app/routers/logs.py b/backend/app/routers/logs.py index cd458b2..d64458c 100644 --- a/backend/app/routers/logs.py +++ b/backend/app/routers/logs.py @@ -1,16 +1,27 @@ """日志路由""" -from fastapi import APIRouter, Depends, Header, HTTPException +from typing import Optional +from fastapi import APIRouter, Depends, Header, HTTPException, Query from sqlalchemy.orm import Session +from sqlalchemy import desc from ..database import get_db from ..config import get_settings from ..models.logs import PlatformLog from ..schemas.logs import LogCreate, LogResponse, BatchLogRequest +from ..services.auth import decode_token -router = APIRouter(prefix="/api/logs", tags=["logs"]) +router = APIRouter(prefix="/logs", tags=["logs"]) settings = get_settings() +def get_current_user_optional(authorization: Optional[str] = Header(None)): + """可选的用户认证""" + if authorization and authorization.startswith("Bearer "): + token = authorization[7:] + return decode_token(token) + return None + + def verify_api_key(x_api_key: str = Header(..., alias="X-API-Key")): """验证API Key""" if x_api_key != settings.API_KEY: @@ -43,3 +54,62 @@ async def batch_write_logs( db.add_all(logs) db.commit() return {"success": True, "count": len(logs)} + + +@router.get("") +async def query_logs( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + log_type: Optional[str] = None, + level: Optional[str] = None, + app_code: Optional[str] = None, + tenant_id: Optional[str] = None, + trace_id: Optional[str] = None, + keyword: Optional[str] = None, + db: Session = Depends(get_db), + user = Depends(get_current_user_optional) +): + """查询日志列表""" + query = db.query(PlatformLog) + + if log_type: + query = query.filter(PlatformLog.log_type == log_type) + if level: + query = query.filter(PlatformLog.level == level) + if app_code: + query = query.filter(PlatformLog.app_code == app_code) + if tenant_id: + query = query.filter(PlatformLog.tenant_id == tenant_id) + if trace_id: + query = query.filter(PlatformLog.trace_id == trace_id) + if keyword: + query = query.filter(PlatformLog.message.like(f"%{keyword}%")) + + total = query.count() + items = query.order_by(desc(PlatformLog.log_time)).offset((page-1)*size).limit(size).all() + + return { + "total": total, + "page": page, + "size": size, + "items": [ + { + "id": item.id, + "log_type": item.log_type, + "level": item.level, + "app_code": item.app_code, + "tenant_id": item.tenant_id, + "trace_id": item.trace_id, + "message": item.message, + "path": item.path, + "method": item.method, + "status_code": item.status_code, + "duration_ms": item.duration_ms, + "ip_address": item.ip_address, + "extra_data": item.extra_data, + "stack_trace": item.stack_trace, + "log_time": str(item.log_time) if item.log_time else None + } + for item in items + ] + } diff --git a/backend/app/routers/stats.py b/backend/app/routers/stats.py index f9b377d..5c9d7e5 100644 --- a/backend/app/routers/stats.py +++ b/backend/app/routers/stats.py @@ -1,16 +1,28 @@ """统计上报路由""" -from fastapi import APIRouter, Depends, Header, HTTPException +from datetime import datetime, timedelta +from typing import Optional +from fastapi import APIRouter, Depends, Header, HTTPException, Query from sqlalchemy.orm import Session +from sqlalchemy import func from ..database import get_db from ..config import get_settings from ..models.stats import AICallEvent from ..schemas.stats import AICallEventCreate, AICallEventResponse, BatchReportRequest +from ..services.auth import decode_token -router = APIRouter(prefix="/api/stats", tags=["stats"]) +router = APIRouter(prefix="/stats", tags=["stats"]) settings = get_settings() +def get_current_user_optional(authorization: Optional[str] = Header(None)): + """可选的用户认证""" + if authorization and authorization.startswith("Bearer "): + token = authorization[7:] + return decode_token(token) + return None + + def verify_api_key(x_api_key: str = Header(..., alias="X-API-Key")): """验证API Key""" if x_api_key != settings.API_KEY: @@ -43,3 +55,79 @@ async def batch_report_ai_calls( db.add_all(events) db.commit() return {"success": True, "count": len(events)} + + +@router.get("/summary") +async def get_stats_summary( + db: Session = Depends(get_db), + user = Depends(get_current_user_optional) +): + """获取统计摘要(用于仪表盘)""" + today = datetime.now().date() + + # 今日调用次数和 token 消耗 + today_stats = db.query( + func.count(AICallEvent.id).label('calls'), + func.coalesce(func.sum(AICallEvent.total_tokens), 0).label('tokens') + ).filter( + func.date(AICallEvent.created_at) == today + ).first() + + # 本周数据 + week_start = today - timedelta(days=today.weekday()) + week_stats = db.query( + func.count(AICallEvent.id).label('calls'), + func.coalesce(func.sum(AICallEvent.total_tokens), 0).label('tokens') + ).filter( + func.date(AICallEvent.created_at) >= week_start + ).first() + + return { + "today_calls": today_stats.calls if today_stats else 0, + "today_tokens": int(today_stats.tokens) if today_stats else 0, + "week_calls": week_stats.calls if week_stats else 0, + "week_tokens": int(week_stats.tokens) if week_stats else 0 + } + + +@router.get("/trend") +async def get_stats_trend( + days: int = Query(7, ge=1, le=30), + tenant_id: Optional[str] = None, + db: Session = Depends(get_db), + user = Depends(get_current_user_optional) +): + """获取调用趋势数据""" + end_date = datetime.now().date() + start_date = end_date - timedelta(days=days-1) + + query = db.query( + func.date(AICallEvent.created_at).label('date'), + func.count(AICallEvent.id).label('calls'), + func.coalesce(func.sum(AICallEvent.total_tokens), 0).label('tokens') + ).filter( + func.date(AICallEvent.created_at) >= start_date, + func.date(AICallEvent.created_at) <= end_date + ) + + if tenant_id: + query = query.filter(AICallEvent.tenant_id == tenant_id) + + results = query.group_by(func.date(AICallEvent.created_at)).all() + + # 转换为字典便于查找 + data_map = {str(r.date): {"calls": r.calls, "tokens": int(r.tokens)} for r in results} + + # 填充所有日期 + trend = [] + current = start_date + while current <= end_date: + date_str = str(current) + trend.append({ + "date": date_str, + "calls": data_map.get(date_str, {}).get("calls", 0), + "tokens": data_map.get(date_str, {}).get("tokens", 0) + }) + current += timedelta(days=1) + + return {"trend": trend}