Files
H3ConuMS-v2/backend/app/api/v1/audit.py
T
v6ole 5aadbc78c6 feat: v0.9.0 新增记录日志、设备更换记录及IMC服务集成
- 新增 RecordsLog.vue 操作记录日志页面
- 新增 ReplacementRecords.vue 设备更换记录页面
- 新增 imc_service.py IMC网管系统集成服务
- 新增 datetime.js 前端日期时间工具函数
- 新增 OLT时间同步.md 文档
- 扩展 devices.py API:设备更换记录、批量操作等
- 扩展 ssh_service.py:OLT时间同步功能
- 扩展 olt.py:新增时间同步相关接口
- 更新 DeviceList.vue:增强设备列表功能
- 更新路由和导航菜单
- 将 .claude/ 和 .mcp.json 加入 .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 10:36:06 +08:00

158 lines
5.2 KiB
Python

"""审计日志 API"""
from datetime import datetime, timedelta
from typing import Optional
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.middleware.permission_middleware import require_permission
from app.models.audit_log import AuditLog
from app.services.audit_service import query_logs
import csv
import io
router = APIRouter(prefix="/api/audit", tags=["审计日志"])
@router.get("/logs")
def get_audit_logs(
start_time: Optional[datetime] = Query(None),
end_time: Optional[datetime] = Query(None),
user_id: Optional[str] = Query(None),
username: Optional[str] = Query(None),
action_type: Optional[str] = Query(None),
resource_type: Optional[str] = Query(None),
status: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
_: dict = Depends(require_permission('*')),
):
"""查询审计日志(仅管理员)"""
total, items = query_logs(
db,
start_time=start_time,
end_time=end_time,
user_id=user_id,
username=username,
action_type=action_type,
resource_type=resource_type,
status=status,
page=page,
page_size=page_size,
)
return {
"total": total,
"page": page,
"page_size": page_size,
"items": [_fmt(r) for r in items],
}
@router.get("/logs/{log_id}")
def get_audit_log_detail(
log_id: int,
db: Session = Depends(get_db),
_: dict = Depends(require_permission('*')),
):
"""获取单条审计日志详情"""
log = db.query(AuditLog).filter(AuditLog.id == log_id).first()
if not log:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="日志不存在")
return _fmt(log, detail=True)
@router.get("/stats")
def get_audit_stats(
days: int = Query(7, ge=1, le=90),
db: Session = Depends(get_db),
_: dict = Depends(require_permission('*')),
):
"""审计日志统计(最近N天)"""
from datetime import timedelta
from sqlalchemy import func
since = datetime.utcnow() - timedelta(days=days)
rows = (
db.query(AuditLog.action_type, AuditLog.status, func.count().label("cnt"))
.filter(AuditLog.action_time >= since)
.group_by(AuditLog.action_type, AuditLog.status)
.all()
)
total = db.query(func.count(AuditLog.id)).filter(AuditLog.action_time >= since).scalar()
by_type = {}
for row in rows:
if row.action_type not in by_type:
by_type[row.action_type] = {"success": 0, "failed": 0, "error": 0}
by_type[row.action_type][row.status] = row.cnt
return {"total": total, "days": days, "by_type": by_type}
@router.get("/logs/export/csv")
def export_audit_logs(
start_time: Optional[datetime] = Query(None),
end_time: Optional[datetime] = Query(None),
action_type: Optional[str] = Query(None),
status: Optional[str] = Query(None),
db: Session = Depends(get_db),
_: dict = Depends(require_permission('*')),
):
"""导出审计日志为 CSV"""
_, items = query_logs(
db,
start_time=start_time,
end_time=end_time,
action_type=action_type,
status=status,
page=1,
page_size=5000,
)
def generate():
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["时间", "用户", "角色", "操作类型", "子类型", "路径", "状态码", "状态", "IP", "描述"])
for r in items:
t_cst = (r.action_time + timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S") if r.action_time else ""
writer.writerow([
t_cst,
r.username, r.user_role, r.action_type, r.action_subtype or "",
f"{r.request_method} {r.request_path}", r.status_code, r.status,
r.ip_address or "", r.description,
])
yield buf.getvalue().encode("utf-8-sig")
filename = f"audit_{datetime.now().strftime('%Y%m%d%H%M%S')}.csv"
return StreamingResponse(
generate(),
media_type="text/csv",
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
def _fmt(r: AuditLog, detail: bool = False) -> dict:
base = {
"id": r.id,
"action_time": (r.action_time.isoformat() + "Z") if r.action_time else None,
"user_id": r.user_id,
"username": r.username,
"user_role": r.user_role,
"action_type": r.action_type,
"action_subtype": r.action_subtype,
"request_method": r.request_method,
"request_path": r.request_path,
"status": r.status,
"status_code": r.status_code,
"resource_type": r.resource_type,
"resource_id": r.resource_id,
"resource_name": r.resource_name,
"description": r.description,
"ip_address": r.ip_address,
}
if detail:
base["request_params"] = r.request_params
base["response_data"] = r.response_data
base["error_message"] = r.error_message
base["user_agent"] = r.user_agent
return base