本次提交对多个外部系统路由进行了多维度优化: 1. 统一分页参数:将所有路由的`page = offset//limit +1`、`page_size=limit`替换为标准的`limit`+`offset`分页格式 2. 完善接口文档:补充多个端点的功能说明、参数含义与返回字段解释 3. 增强参数校验:新增字段长度限制、正则校验、枚举类型约束与业务逻辑校验 4. 优化代码复用:提取重复逻辑为辅助函数,减少样板代码 5. 修复接口问题:修正工具健康检查端点路径参数类型,优化导出接口响应格式 6. 补充异常处理:为批量操作添加异常捕获与日志记录,避免流程中断
258 lines
9.3 KiB
Python
258 lines
9.3 KiB
Python
"""AuditLog 子域 Router。
|
||
|
||
外部系统限界上下文的审计日志查询 API,覆盖审计日志列表 / 详情 / 导出。
|
||
所有端点通过 ``create_use_cases_from_db`` 装配 use_cases,经 ``audit_log_service``
|
||
端口调用用例。
|
||
|
||
路径顺序约束:静态路径 ``/export`` 必须在 ``/{log_id}`` 前声明,避免被路径参数捕获。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import UTC, datetime
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Depends, Path, Query
|
||
from fastapi.responses import StreamingResponse
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
|
||
from yuxi.external_systems.use_cases.dto.audit_log import (
|
||
ExportAuditLogsInput,
|
||
GetAuditLogDetailInput,
|
||
GetAuditLogStatsInput,
|
||
ListActionTypesInput,
|
||
ListAuditLogsInput,
|
||
ListConfigChangesInput,
|
||
ListEventTypesInput,
|
||
ListOperatorsInput,
|
||
)
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
||
|
||
audit_log_router = APIRouter(
|
||
prefix="/audit-logs",
|
||
tags=["external-systems-audit-log"],
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# === 静态路径端点(必须在 /{log_id} 之前声明) ===
|
||
# =============================================================================
|
||
|
||
|
||
@audit_log_router.get("/export")
|
||
async def export_audit_logs(
|
||
system_id: int | None = Query(None, ge=1),
|
||
env_key: str | None = Query(None, max_length=32),
|
||
event_type: str | None = Query(None, max_length=64),
|
||
action_type: str | None = Query(None, max_length=64),
|
||
resource: str | None = Query(None, max_length=256),
|
||
user: str | None = Query(None, max_length=64),
|
||
start_time: datetime | None = Query(None),
|
||
end_time: datetime | None = Query(None),
|
||
limit: int = Query(10000, ge=1, le=10000, description="导出记录上限,最大 10000"),
|
||
offset: int = Query(0, ge=0),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
):
|
||
"""导出审计日志为 JSON 字节流。
|
||
|
||
审计日志含跨系统敏感操作记录,导出需管理员权限。
|
||
导出受 ``limit`` 限制(默认上限 10000 条),超出部分通过响应头
|
||
``X-Truncated`` 和 ``X-Total-Count`` 标识。
|
||
|
||
采用增量流式输出,避免一次性构建大 payload 导致内存峰值。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ExportAuditLogsInput(
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
event_type=event_type,
|
||
action_type=action_type,
|
||
resource=resource,
|
||
user=user,
|
||
start_time=start_time,
|
||
end_time=end_time,
|
||
limit=limit,
|
||
offset=offset,
|
||
)
|
||
output = await use_cases.audit_log_service.export_audit_logs(input_dto)
|
||
|
||
timestamp_str = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
|
||
exported_at = datetime.now(UTC).isoformat()
|
||
|
||
async def _stream():
|
||
# JSON 对象包裹 items 数组 + 元数据,增量 yield 每条记录
|
||
yield b'{"exported_at":'
|
||
yield json.dumps(exported_at).encode("utf-8")
|
||
yield b',"total":'
|
||
yield str(output.total).encode("utf-8")
|
||
yield b',"truncated":'
|
||
yield str(output.truncated).lower().encode("utf-8")
|
||
yield b',"count":'
|
||
yield str(len(output.items)).encode("utf-8")
|
||
yield b',"items":['
|
||
for i, item in enumerate(output.items):
|
||
if i > 0:
|
||
yield b","
|
||
yield json.dumps(item.model_dump(), ensure_ascii=False, default=str).encode(
|
||
"utf-8"
|
||
)
|
||
yield b"]}"
|
||
|
||
headers = {
|
||
"Content-Disposition": f"attachment; filename=audit_logs_{timestamp_str}.json",
|
||
"X-Total-Count": str(output.total),
|
||
"X-Truncated": str(output.truncated),
|
||
}
|
||
|
||
return StreamingResponse(
|
||
_stream(),
|
||
media_type="application/octet-stream",
|
||
headers=headers,
|
||
)
|
||
|
||
|
||
@audit_log_router.get("", response_model=dict)
|
||
async def list_audit_logs(
|
||
limit: int = Query(100, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
system_id: int | None = Query(None, ge=1),
|
||
env_key: str | None = Query(None, max_length=32),
|
||
event_type: str | None = Query(None, max_length=64),
|
||
action_type: str | None = Query(None, max_length=64),
|
||
resource: str | None = Query(None, max_length=256),
|
||
user: str | None = Query(None, max_length=64),
|
||
start_time: datetime | None = Query(None),
|
||
end_time: datetime | None = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""分页列出审计日志。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListAuditLogsInput(
|
||
limit=limit,
|
||
offset=offset,
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
event_type=event_type,
|
||
action_type=action_type,
|
||
resource=resource,
|
||
user=user,
|
||
start_time=start_time,
|
||
end_time=end_time,
|
||
)
|
||
output = await use_cases.audit_log_service.list_audit_logs(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@audit_log_router.get("/stats", response_model=dict)
|
||
async def get_audit_log_stats(
|
||
system_id: int | None = Query(None, ge=1),
|
||
start_time: datetime | None = Query(None),
|
||
end_time: datetime | None = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询审计日志统计聚合(按 action_type 分组)。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetAuditLogStatsInput(
|
||
system_id=system_id,
|
||
start_time=start_time,
|
||
end_time=end_time,
|
||
)
|
||
output = await use_cases.audit_log_service.get_audit_log_stats(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@audit_log_router.get("/config-changes", response_model=dict)
|
||
async def list_config_changes(
|
||
limit: int = Query(100, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
system_id: int | None = Query(None, ge=1),
|
||
env_key: str | None = Query(None, max_length=32),
|
||
event_type: str | None = Query(None, max_length=64),
|
||
action_type: str | None = Query(None, max_length=64),
|
||
resource: str | None = Query(None, max_length=256),
|
||
user: str | None = Query(None, max_length=64),
|
||
start_time: datetime | None = Query(None),
|
||
end_time: datetime | None = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询配置变更历史(含 snapshot_before / snapshot_after 的审计日志)。
|
||
|
||
``action_type`` 默认不限制,返回所有含快照的记录(CREATE/UPDATE/DELETE 等)。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListConfigChangesInput(
|
||
limit=limit,
|
||
offset=offset,
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
event_type=event_type,
|
||
action_type=action_type,
|
||
resource=resource,
|
||
user=user,
|
||
start_time=start_time,
|
||
end_time=end_time,
|
||
)
|
||
output = await use_cases.audit_log_service.list_config_changes(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@audit_log_router.get("/action-types", response_model=dict)
|
||
async def list_action_types(
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询审计日志中已出现的操作类型列表。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListActionTypesInput()
|
||
output = await use_cases.audit_log_service.list_action_types(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@audit_log_router.get("/event-types", response_model=dict)
|
||
async def list_event_types(
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询审计日志中已出现的事件类型列表。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListEventTypesInput()
|
||
output = await use_cases.audit_log_service.list_event_types(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@audit_log_router.get("/operators", response_model=dict)
|
||
async def list_operators(
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询审计日志中已出现的操作人列表。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListOperatorsInput()
|
||
output = await use_cases.audit_log_service.list_operators(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
# =============================================================================
|
||
# === 动态路径端点 /{log_id} ===
|
||
# =============================================================================
|
||
|
||
|
||
@audit_log_router.get("/{log_id}", response_model=dict)
|
||
async def get_audit_log_detail(
|
||
log_id: int = Path(ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取审计日志详情。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetAuditLogDetailInput(id=log_id)
|
||
output = await use_cases.audit_log_service.get_audit_log_detail(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|