"""操作日志与统计 API(仅管理员)。""" import csv import io import json from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse from sqlalchemy import func from sqlalchemy.orm import Session from ..database import get_db from ..deps import get_current_user from ..models import OperationLog router = APIRouter(prefix="/api/operations", tags=["operations"]) def _require_admin(user: dict): if not user.get("is_admin"): raise HTTPException(status_code=403, detail="需要管理员权限") def _query_logs(db: Session, days: int, username: str = "", action: str = ""): since = datetime.utcnow() - timedelta(days=max(1, min(days, 365))) query = db.query(OperationLog).filter(OperationLog.created_at >= since) if username: query = query.filter(OperationLog.username == username) if action: query = query.filter(OperationLog.action == action) return query @router.get("/summary") async def operation_summary(days: int = Query(30, ge=1, le=365), user: dict = Depends(get_current_user), db: Session = Depends(get_db)): _require_admin(user) query = _query_logs(db, days) total = query.count() action_counts = dict(query.with_entities(OperationLog.action, func.count()).group_by(OperationLog.action).all()) users = query.with_entities(OperationLog.username, func.count()).group_by(OperationLog.username).order_by(func.count().desc()).limit(10).all() apps = query.filter(OperationLog.app_name.isnot(None)).with_entities(OperationLog.app_name, func.count()).group_by(OperationLog.app_name).order_by(func.count().desc()).limit(10).all() return {"days": days, "total": total, "action_counts": action_counts, "top_users": [{"name": name, "count": count} for name, count in users], "top_apps": [{"name": name, "count": count} for name, count in apps]} @router.get("") async def list_operations(days: int = Query(30, ge=1, le=365), username: str = "", action: str = "", limit: int = Query(100, ge=1, le=500), user: dict = Depends(get_current_user), db: Session = Depends(get_db)): _require_admin(user) logs = _query_logs(db, days, username, action).order_by(OperationLog.created_at.desc()).limit(limit).all() return [{"id": log.id, "created_at": log.created_at, "username": log.username, "action": log.action, "app_name": log.app_name, "task_id": log.task_id, "resource_type": log.resource_type, "resource_name": log.resource_name, "detail": json.loads(log.detail_json or "{}"), "client_ip": log.client_ip} for log in logs] @router.get("/export") async def export_operations(days: int = Query(30, ge=1, le=365), user: dict = Depends(get_current_user), db: Session = Depends(get_db)): _require_admin(user) output = io.StringIO() writer = csv.writer(output) writer.writerow(["时间", "用户", "操作", "App", "资源类型", "资源名称", "IP", "详情"]) for log in _query_logs(db, days).order_by(OperationLog.created_at.desc()).all(): writer.writerow([log.created_at, log.username, log.action, log.app_name or "", log.resource_type or "", log.resource_name or "", log.client_ip or "", log.detail_json or ""]) return StreamingResponse(iter(["\ufeff" + output.getvalue()]), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=operation-logs.csv"})