138 lines
4.2 KiB
Python
138 lines
4.2 KiB
Python
|
|
# =============================================================================
|
||
|
|
# 企微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,
|
||
|
|
}
|