You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

153 lines
4.9 KiB

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"},
)