本次提交包含多类代码优化: 1. 修复多处单行代码换行格式,统一代码排版 2. 为外部系统模块新增快捷菜单配置 3. 优化前端API请求参数命名一致性 4. 补充媒体下载超限错误码与领域异常类 5. 完善仓储层更新逻辑,支持显式清空字段 6. 优化部分测试用例与工具函数代码结构 7. 为微信插件白名单缓存增加过期时间
260 lines
10 KiB
Python
260 lines
10 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 yuxi.external_systems.infrastructure.container import UseCases
|
||
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.routers.external_systems import get_use_cases, parse_and_naive, validate_time_range
|
||
from server.utils.auth_middleware import get_admin_user, 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: str | None = Query(None, description="开始时间(ISO 8601)"),
|
||
end_time: str | None = Query(None, description="结束时间(ISO 8601)"),
|
||
limit: int = Query(10000, ge=1, le=10000, description="导出记录上限,最大 10000"),
|
||
offset: int = Query(0, ge=0),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> StreamingResponse:
|
||
"""导出审计日志为 JSON 字节流。
|
||
|
||
审计日志含跨系统敏感操作记录,导出需管理员权限。
|
||
导出受 ``limit`` 限制(默认上限 10000 条),超出部分通过响应头
|
||
``X-Truncated`` 和 ``X-Total-Count`` 标识。
|
||
|
||
采用增量流式输出,避免一次性构建大 payload 导致内存峰值。
|
||
"""
|
||
start_dt = parse_and_naive("start_time", start_time)
|
||
end_dt = parse_and_naive("end_time", end_time)
|
||
validate_time_range(start_dt, end_dt)
|
||
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_dt,
|
||
end_time=end_dt,
|
||
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).lower(),
|
||
}
|
||
|
||
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: str | None = Query(None, description="开始时间(ISO 8601)"),
|
||
end_time: str | None = Query(None, description="结束时间(ISO 8601)"),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""分页列出审计日志。"""
|
||
start_dt = parse_and_naive("start_time", start_time)
|
||
end_dt = parse_and_naive("end_time", end_time)
|
||
validate_time_range(start_dt, end_dt)
|
||
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_dt,
|
||
end_time=end_dt,
|
||
)
|
||
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: str | None = Query(None, description="开始时间(ISO 8601)"),
|
||
end_time: str | None = Query(None, description="结束时间(ISO 8601)"),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询审计日志统计聚合(按 action_type 分组)。"""
|
||
start_dt = parse_and_naive("start_time", start_time)
|
||
end_dt = parse_and_naive("end_time", end_time)
|
||
validate_time_range(start_dt, end_dt)
|
||
input_dto = GetAuditLogStatsInput(
|
||
system_id=system_id,
|
||
start_time=start_dt,
|
||
end_time=end_dt,
|
||
)
|
||
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: str | None = Query(None, description="开始时间(ISO 8601)"),
|
||
end_time: str | None = Query(None, description="结束时间(ISO 8601)"),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询配置变更历史(含 snapshot_before / snapshot_after 的审计日志)。
|
||
|
||
``action_type`` 默认不限制,返回所有含快照的记录(CREATE/UPDATE/DELETE 等)。
|
||
"""
|
||
start_dt = parse_and_naive("start_time", start_time)
|
||
end_dt = parse_and_naive("end_time", end_time)
|
||
validate_time_range(start_dt, end_dt)
|
||
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_dt,
|
||
end_time=end_dt,
|
||
)
|
||
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(
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询审计日志中已出现的操作类型列表。"""
|
||
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(
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询审计日志中已出现的事件类型列表。"""
|
||
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(
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询审计日志中已出现的操作人列表。"""
|
||
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),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取审计日志详情。"""
|
||
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()}
|