100 lines
3.8 KiB
Python
100 lines
3.8 KiB
Python
|
|
"""审计日志 API —— 查看系统操作审计记录
|
|||
|
|
|
|||
|
|
与 MOM(KCGL) /audit/logs 的接口保持同构的筛选维度(操作人/模块/动作/目标/
|
|||
|
|
时间区间),便于两端运维习惯统一;额外提供 request_id 筛选,可凭它直接跳到
|
|||
|
|
结构化日志里的那一次请求。
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from datetime import datetime, time, timedelta
|
|||
|
|
|
|||
|
|
from fastapi import APIRouter, Depends, Query
|
|||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
|
|
|||
|
|
from app.core.database import get_db
|
|||
|
|
from app.core.deps import require_admin
|
|||
|
|
from app.core.time_utils import BEIJING_TZ
|
|||
|
|
from app.schemas.audit import (
|
|||
|
|
AuditLogListResponse,
|
|||
|
|
AuditLogResponse,
|
|||
|
|
AuditOption,
|
|||
|
|
AuditOptionsResponse,
|
|||
|
|
)
|
|||
|
|
from app.services import audit_service
|
|||
|
|
from app.services.audit_service import ACTION_LABELS, MODULE_LABELS
|
|||
|
|
|
|||
|
|
router = APIRouter(prefix="/audit", tags=["审计日志"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _parse_day(value: str | None, *, end_of_day: bool = False) -> datetime | None:
|
|||
|
|
"""解析 YYYY-MM-DD 为北京时间。
|
|||
|
|
|
|||
|
|
结束日期取次日 00:00 作为上界(配合 < 判断)—— 直接取当天 23:59:59 会
|
|||
|
|
漏掉该秒内的记录,是日期区间筛选最常见的差一错误。
|
|||
|
|
"""
|
|||
|
|
if not value:
|
|||
|
|
return None
|
|||
|
|
try:
|
|||
|
|
day = datetime.strptime(value, "%Y-%m-%d").date()
|
|||
|
|
except ValueError:
|
|||
|
|
return None
|
|||
|
|
if end_of_day:
|
|||
|
|
return datetime.combine(day + timedelta(days=1), time.min, tzinfo=BEIJING_TZ)
|
|||
|
|
return datetime.combine(day, time.min, tzinfo=BEIJING_TZ)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("/logs", response_model=AuditLogListResponse)
|
|||
|
|
async def get_audit_logs(
|
|||
|
|
user_id: str | None = Query(None, description="操作人账号(模糊匹配)"),
|
|||
|
|
module: str | None = Query(None, description="业务模块"),
|
|||
|
|
action: str | None = Query(None, description="动作类型"),
|
|||
|
|
target_id: str | None = Query(None, description="目标ID"),
|
|||
|
|
request_id: str | None = Query(None, description="请求ID(与接口日志对账)"),
|
|||
|
|
status_code: int | None = Query(None, description="响应状态码"),
|
|||
|
|
start_date: str | None = Query(None, description="起始日期 YYYY-MM-DD"),
|
|||
|
|
end_date: str | None = Query(None, description="结束日期 YYYY-MM-DD(含当天)"),
|
|||
|
|
page: int = Query(1, ge=1),
|
|||
|
|
page_size: int = Query(50, ge=1, le=200),
|
|||
|
|
db: AsyncSession = Depends(get_db),
|
|||
|
|
current_user: dict = Depends(require_admin),
|
|||
|
|
) -> AuditLogListResponse:
|
|||
|
|
"""审计日志分页查询(按时间倒序)"""
|
|||
|
|
start = _parse_day(start_date)
|
|||
|
|
# 结束日期用「次日 00:00」作为开区间上界,避免漏掉当天最后几条
|
|||
|
|
end_exclusive = _parse_day(end_date, end_of_day=True)
|
|||
|
|
|
|||
|
|
rows, total = await audit_service.list_audit_logs(
|
|||
|
|
db,
|
|||
|
|
user_id=user_id,
|
|||
|
|
module=module,
|
|||
|
|
action=action,
|
|||
|
|
target_id=target_id,
|
|||
|
|
request_id=request_id,
|
|||
|
|
status_code=status_code,
|
|||
|
|
start=start,
|
|||
|
|
end=end_exclusive - timedelta(microseconds=1) if end_exclusive else None,
|
|||
|
|
skip=(page - 1) * page_size,
|
|||
|
|
limit=page_size,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
items = []
|
|||
|
|
for row in rows:
|
|||
|
|
item = AuditLogResponse.model_validate(row)
|
|||
|
|
# 中文标签由服务端补,避免前端为每个枚举再维护一份映射
|
|||
|
|
item.module_label = MODULE_LABELS.get(row.module, row.module)
|
|||
|
|
item.action_label = ACTION_LABELS.get(row.action, row.action)
|
|||
|
|
items.append(item)
|
|||
|
|
|
|||
|
|
return AuditLogListResponse(items=items, total=total)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("/options", response_model=AuditOptionsResponse)
|
|||
|
|
async def get_audit_options(
|
|||
|
|
current_user: dict = Depends(require_admin),
|
|||
|
|
) -> AuditOptionsResponse:
|
|||
|
|
"""筛选项:模块与动作的中文下拉"""
|
|||
|
|
return AuditOptionsResponse(
|
|||
|
|
modules=[AuditOption(value=k, label=v) for k, v in MODULE_LABELS.items()],
|
|||
|
|
actions=[AuditOption(value=k, label=v) for k, v in ACTION_LABELS.items()],
|
|||
|
|
)
|