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