Files
wecom_it_smart_desk/backend/app/services/audit_log_service.py
T

138 lines
4.2 KiB
Python
Raw Normal View History

# =============================================================================
# 企微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,
}