import uuid import csv import io from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Request, Query from fastapi.responses import StreamingResponse from sqlalchemy import select, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from database import get_db from models import AuditLog from schemas import AuditLogOut, AuditLogPage from dependencies import get_current_user router = APIRouter(prefix="/api/audit", tags=["audit"]) @router.get("/logs", response_model=AuditLogPage) async def list_logs( request: Request, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), action: str | None = Query(None), resource: str | None = Query(None), operator_id: uuid.UUID | None = Query(None), date_from: datetime | None = Query(None), date_to: datetime | None = Query(None), db: AsyncSession = Depends(get_db), ): conditions = [] if action: conditions.append(AuditLog.action == action) if resource: conditions.append(AuditLog.resource == resource) if operator_id: conditions.append(AuditLog.operator_id == operator_id) if date_from: conditions.append(AuditLog.created_at >= date_from) if date_to: conditions.append(AuditLog.created_at <= date_to) where = and_(*conditions) if conditions else None count_q = select(func.count(AuditLog.id)) if where is not None: count_q = count_q.where(where) total_result = await db.execute(count_q) total = total_result.scalar() or 0 q = select(AuditLog).order_by(AuditLog.created_at.desc()) if where is not None: q = q.where(where) q = q.offset((page - 1) * page_size).limit(page_size) result = await db.execute(q) logs = result.scalars().all() return AuditLogPage( items=[AuditLogOut.model_validate(log, from_attributes=True) for log in logs], total=total, page=page, page_size=page_size, ) @router.get("/actions") async def list_action_types(request: Request, db: AsyncSession = Depends(get_db)): result = await db.execute( select(AuditLog.action, func.count(AuditLog.id)).group_by(AuditLog.action) ) return { "code": 200, "data": [{"action": r[0], "count": r[1]} for r in result.all()], } @router.get("/stats") async def audit_stats(request: Request, db: AsyncSession = Depends(get_db)): total_result = await db.execute(select(func.count(AuditLog.id))) total = total_result.scalar() or 0 today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) today_result = await db.execute( select(func.count(AuditLog.id)).where(AuditLog.created_at >= today_start) ) today = today_result.scalar() or 0 top_result = await db.execute( select(AuditLog.action, func.count(AuditLog.id)) .group_by(AuditLog.action) .order_by(func.count(AuditLog.id).desc()) .limit(10) ) top_actions = [{"action": r[0], "count": r[1]} for r in top_result.all()] top_resources = await db.execute( select(AuditLog.resource, func.count(AuditLog.id)) .group_by(AuditLog.resource) .order_by(func.count(AuditLog.id).desc()) .limit(10) ) top_resources_list = [{"resource": r[0], "count": r[1]} for r in top_resources.all()] return { "code": 200, "data": { "total": total, "today": today, "top_actions": top_actions, "top_resources": top_resources_list, }, } @router.get("/export") async def export_logs( request: Request, date_from: datetime | None = Query(None), date_to: datetime | None = Query(None), db: AsyncSession = Depends(get_db), ): conditions = [] if date_from: conditions.append(AuditLog.created_at >= date_from) if date_to: conditions.append(AuditLog.created_at <= date_to) q = select(AuditLog).order_by(AuditLog.created_at.desc()) if conditions: q = q.where(and_(*conditions)) q = q.limit(10000) result = await db.execute(q) logs = result.scalars().all() output = io.StringIO() writer = csv.writer(output) writer.writerow(["ID", "操作时间", "操作人ID", "操作", "资源", "资源ID", "详情", "IP地址"]) for log in logs: writer.writerow([ str(log.id), log.created_at.isoformat() if log.created_at else "", str(log.operator_id) if log.operator_id else "", log.action, log.resource or "", log.resource_id or "", str(log.detail)[:500] if log.detail else "", log.ip_address or "", ]) output.seek(0) return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={"Content-Disposition": f"attachment; filename=audit_logs_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv"}, )