# ============================================================================= # 企微IT智能服务台 — 审计日志服务 # ============================================================================= # 说明: 提供 audit_log 写入/查询的统一入口 # 用法: # from app.services.audit_log_service import record_audit_log # await record_audit_log( # db, employee_id="sxn", action="role_change", # resource="agent", resource_id="agent-001", # details={"role": "agent"}, request=request, # ) # ============================================================================= import logging from datetime import datetime from typing import Any, Dict, Optional from fastapi import Request from sqlalchemy import select, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.models.audit_log import AuditLog logger = logging.getLogger(__name__) async def record_audit_log( db: AsyncSession, employee_id: str, action: str, resource: str, resource_id: Optional[str] = None, details: Optional[Dict[str, Any]] = None, result: str = "success", request: Optional[Request] = None, ) -> AuditLog: """记录一条审计日志。 Args: db: 数据库会话 employee_id: 操作人企微 UserID,系统操作传 "system" action: 操作类型 resource: 目标资源类型 resource_id: 目标资源 ID(可选) details: 详细上下文 JSON(可选) result: success / failure / partial request: FastAPI Request(可选,自动取 IP + UA) Returns: AuditLog: 写入的日志对象 """ ip_address = None user_agent = None if request is not None: # 优先用 X-Forwarded-For / X-Real-IP(proxy 后面) ip_address = ( request.headers.get("x-forwarded-for", "").split(",")[0].strip() or request.headers.get("x-real-ip") or (request.client.host if request.client else None) ) user_agent = request.headers.get("user-agent") log = AuditLog( employee_id=employee_id, action=action, resource=resource, resource_id=resource_id, details=details or {}, result=result, ip_address=ip_address, user_agent=user_agent, created_at=datetime.now(), ) db.add(log) # 注:不 commit,让调用方跟主操作一起 commit(避免日志写一半就回滚) return log async def list_audit_logs( db: AsyncSession, employee_id: Optional[str] = None, action: Optional[str] = None, resource: Optional[str] = None, from_time: Optional[datetime] = None, to_time: Optional[datetime] = None, page: int = 1, page_size: int = 50, ) -> Dict[str, Any]: """查询审计日志(分页 + 多维过滤)。 Args: db: 数据库会话 employee_id: 按操作人过滤(可选) action: 按操作类型过滤(可选) resource: 按资源类型过滤(可选) from_time: 起始时间(可选) to_time: 结束时间(可选) page: 页码,从 1 开始 page_size: 每页条数,默认 50 Returns: Dict: {items: [...], total: int, page, page_size} """ stmt = select(AuditLog) conditions = [] if employee_id: conditions.append(AuditLog.employee_id == employee_id) if action: conditions.append(AuditLog.action == action) if resource: conditions.append(AuditLog.resource == resource) if from_time: conditions.append(AuditLog.created_at >= from_time) if to_time: conditions.append(AuditLog.created_at <= to_time) if conditions: stmt = stmt.where(and_(*conditions)) # 倒序 + 分页 stmt = stmt.order_by(AuditLog.created_at.desc()) stmt = stmt.offset((page - 1) * page_size).limit(page_size) result = await db.execute(stmt) items = result.scalars().all() # 总数 count_stmt = select(func.count()).select_from(AuditLog) if conditions: count_stmt = count_stmt.where(and_(*conditions)) total = (await db.execute(count_stmt)).scalar() or 0 return { "items": items, "total": total, "page": page, "page_size": page_size, }