2026-06-20 22:16:07 +08:00
|
|
|
|
"""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
|
|
|
|
|
|
|
2026-07-04 00:16:45 +08:00
|
|
|
|
from fastapi import APIRouter, Depends, Path, Query
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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} 之前声明) ===
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 03:29:06 +08:00
|
|
|
|
@audit_log_router.get("/export")
|
2026-06-20 22:16:07 +08:00
|
|
|
|
async def export_audit_logs(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
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),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
start_time: datetime | None = Query(None),
|
|
|
|
|
|
end_time: datetime | None = Query(None),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""导出审计日志为 JSON 字节流。
|
|
|
|
|
|
|
|
|
|
|
|
审计日志含跨系统敏感操作记录,导出需管理员权限。
|
|
|
|
|
|
"""
|
|
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.audit_log_service.export_audit_logs(input_dto)
|
|
|
|
|
|
|
|
|
|
|
|
payload = json.dumps(
|
|
|
|
|
|
[item.model_dump() for item in output.items],
|
|
|
|
|
|
ensure_ascii=False,
|
|
|
|
|
|
default=str,
|
|
|
|
|
|
).encode("utf-8")
|
|
|
|
|
|
timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
|
|
|
|
|
|
|
|
|
|
|
|
async def _stream():
|
|
|
|
|
|
yield payload
|
|
|
|
|
|
|
|
|
|
|
|
return StreamingResponse(
|
|
|
|
|
|
_stream(),
|
|
|
|
|
|
media_type="application/octet-stream",
|
|
|
|
|
|
headers={
|
|
|
|
|
|
"Content-Disposition": f"attachment; filename=audit_logs_{timestamp}.json",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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),
|
2026-07-04 00:16:45 +08:00
|
|
|
|
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),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
system_id: int | None = Query(None, ge=1),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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),
|
2026-07-04 00:16:45 +08:00
|
|
|
|
system_id: int | None = Query(None, ge=1),
|
|
|
|
|
|
env_key: str | None = Query(None, max_length=32),
|
|
|
|
|
|
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),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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,
|
|
|
|
|
|
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(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
log_id: int = Path(ge=1),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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()}
|