from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from typing import Optional from datetime import datetime import csv import io from app.core.database import get_db from app.core.security import get_current_user from app.models.auth import User from app.services.audit_service import AuditService router = APIRouter( prefix="/audit", tags=["审计日志"], dependencies=[Depends(get_current_user)], ) @router.get("/logs", summary="查询审计日志") def get_audit_logs( user_id: Optional[int] = None, username: Optional[str] = None, action: Optional[str] = None, resource_type: Optional[str] = None, status: Optional[str] = None, start_time: Optional[datetime] = None, end_time: Optional[datetime] = None, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=500), # 当前用户必须是 admin 或 super_admin 才能看审计 current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): # 角色检查:super_admin / admin 可以看所有人的,operator/viewer 只能看自己的 if current_user.role not in ("super_admin", "admin"): user_id = current_user.id total, items = AuditService.get_logs( db, user_id=user_id, username=username, action=action, resource_type=resource_type, status=status, start_time=start_time, end_time=end_time, skip=skip, limit=limit, ) serialized = [] for log in items: serialized.append({ "id": log.id, "user_id": log.user_id, "username": log.username, "action": log.action, "resource_type": log.resource_type, "resource_id": log.resource_id, "resource_name": log.resource_name, "method": log.method, "path": log.path, "ip_address": log.ip_address, "user_agent": log.user_agent, "status": log.status, "detail": log.detail, "created_at": log.created_at.isoformat() if log.created_at else None, }) return {"total": total, "items": serialized} @router.get("/logs/export", summary="导出审计日志为 CSV") def export_audit_logs( user_id: Optional[int] = None, username: Optional[str] = None, action: Optional[str] = None, resource_type: Optional[str] = None, status: Optional[str] = None, start_time: Optional[datetime] = None, end_time: Optional[datetime] = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """导出 CSV 流。admin/super_admin 看全部;operator/viewer 仅能导出自己的。""" if current_user.role not in ("super_admin", "admin"): user_id = current_user.id # 导出上限 50000 行,避免一次拉太多 OOM total, items = AuditService.get_logs( db, user_id=user_id, username=username, action=action, resource_type=resource_type, status=status, start_time=start_time, end_time=end_time, skip=0, limit=50000, ) output = io.StringIO() # 写入 UTF-8 BOM 让 Excel 直接打开不乱码 output.write("\ufeff") writer = csv.writer(output) writer.writerow([ "ID", "时间", "用户", "操作", "资源类型", "资源ID", "资源名称", "HTTP方法", "路径", "客户端IP", "状态", "详情" ]) for log in items: writer.writerow([ log.id, log.created_at.isoformat() if log.created_at else "", log.username or "", log.action or "", log.resource_type or "", log.resource_id or "", log.resource_name or "", log.method or "", log.path or "", log.ip_address or "", log.status or "", (log.detail or "").replace("\n", " ")[:500], ]) output.seek(0) filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" return StreamingResponse( iter([output.getvalue()]), media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, ) @router.get("/stats", summary="审计日志统计") def get_audit_stats( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): if current_user.role not in ("super_admin", "admin"): # operator/viewer 只能看自己的简单计数 from app.models.audit import AuditLog from sqlalchemy import func my_count = db.query(func.count(AuditLog.id)).filter(AuditLog.user_id == current_user.id).scalar() return {"recent_24h": 0, "by_action_24h": {}, "my_total": int(my_count or 0)} return AuditService.get_stats(db) @router.get("/actions", summary="获取所有审计操作类型(用于前端过滤下拉框)") def get_action_types(): """硬编码返回常用 action 列表,避免每次都查 DB""" from app.services.audit_service import AuditAction actions = [] for name in dir(AuditAction): if name.startswith("_") or name.isupper() and not name.startswith("__"): continue val = getattr(AuditAction, name, None) if isinstance(val, str): actions.append({"code": val, "name": name}) return actions