Files
ipam/backend/app/services/audit_service.py
T
Your Name bca0da7c7d readme
2026-07-20 13:40:05 +08:00

246 lines
8.3 KiB
Python

from typing import Optional, List
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
import json
from app.models.audit import AuditLog, AuditAction, AuditResource
from app.models.auth import User
class AuditService:
"""审计日志服务"""
@staticmethod
def log(db: Session,
user: User,
action: AuditAction,
resource: AuditResource,
resource_id: Optional[str] = None,
description: Optional[str] = None,
old_value: Optional[dict] = None,
new_value: Optional[dict] = None,
changed_fields: Optional[List[str]] = None,
user_ip: Optional[str] = None,
user_agent: Optional[str] = None,
request_method: Optional[str] = None,
request_path: Optional[str] = None,
success: bool = True,
error_message: Optional[str] = None) -> AuditLog:
"""
记录审计日志
Args:
db: 数据库会话
user: 操作用户
action: 操作类型
resource: 资源类型
resource_id: 资源ID
description: 操作描述
old_value: 旧值
new_value: 新值
changed_fields: 变更的字段列表
user_ip: 用户IP
user_agent: 用户代理
request_method: 请求方法
request_path: 请求路径
success: 操作是否成功
error_message: 错误信息
Returns:
AuditLog 实例
"""
audit_log = AuditLog(
user_id=user.id if user else None,
username=user.username if user else None,
real_name=user.real_name if user else None,
action=action,
resource=resource,
resource_id=str(resource_id) if resource_id else None,
description=description,
old_value=json.dumps(old_value, ensure_ascii=False, default=str) if old_value else None,
new_value=json.dumps(new_value, ensure_ascii=False, default=str) if new_value else None,
changed_fields=','.join(changed_fields) if changed_fields else None,
user_ip=user_ip,
user_agent=user_agent,
request_method=request_method,
request_path=request_path,
success=1 if success else 0,
error_message=error_message
)
db.add(audit_log)
db.commit()
db.refresh(audit_log)
return audit_log
@staticmethod
def get_logs(db: Session,
user_id: Optional[int] = None,
action: Optional[AuditAction] = None,
resource: Optional[AuditResource] = None,
success: Optional[bool] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
skip: int = 0,
limit: int = 100) -> tuple[int, List[AuditLog]]:
"""
查询审计日志
Args:
db: 数据库会话
user_id: 按用户ID过滤
action: 按操作类型过滤
resource: 按资源类型过滤
success: 按操作结果过滤
start_time: 开始时间
end_time: 结束时间
skip: 跳过记录数
limit: 返回记录数
Returns:
(总数, 记录列表)
"""
query = db.query(AuditLog)
if user_id:
query = query.filter(AuditLog.user_id == user_id)
if action:
query = query.filter(AuditLog.action == action)
if resource:
query = query.filter(AuditLog.resource == resource)
if success is not None:
query = query.filter(AuditLog.success == (1 if success else 0))
if start_time:
query = query.filter(AuditLog.created_at >= start_time)
if end_time:
query = query.filter(AuditLog.created_at <= end_time)
total = query.count()
items = query.order_by(AuditLog.created_at.desc()).offset(skip).limit(limit).all()
return total, items
@staticmethod
def get_user_actions(db: Session, user_id: int, limit: int = 50) -> List[AuditLog]:
"""获取指定用户的操作日志"""
return db.query(AuditLog)\
.filter(AuditLog.user_id == user_id)\
.order_by(AuditLog.created_at.desc())\
.limit(limit)\
.all()
@staticmethod
def get_resource_logs(db: Session, resource: AuditResource, resource_id: str, limit: int = 50) -> List[AuditLog]:
"""获取指定资源的操作历史"""
return db.query(AuditLog)\
.filter(AuditLog.resource == resource, AuditLog.resource_id == str(resource_id))\
.order_by(AuditLog.created_at.desc())\
.limit(limit)\
.all()
@staticmethod
def get_statistics(db: Session, days: int = 30) -> dict:
"""
获取审计统计信息
Args:
db: 数据库会话
days: 统计天数
Returns:
统计信息字典
"""
start_date = datetime.utcnow() - timedelta(days=days)
# 总操作数
total_ops = db.query(AuditLog).filter(AuditLog.created_at >= start_date).count()
# 成功/失败统计
success_ops = db.query(AuditLog).filter(
AuditLog.created_at >= start_date,
AuditLog.success == 1
).count()
failed_ops = total_ops - success_ops
# 按操作类型统计
action_stats = {}
for action in AuditAction:
count = db.query(AuditLog).filter(
AuditLog.created_at >= start_date,
AuditLog.action == action
).count()
if count > 0:
action_stats[action.value] = count
# 按资源类型统计
resource_stats = {}
for resource in AuditResource:
count = db.query(AuditLog).filter(
AuditLog.created_at >= start_date,
AuditLog.resource == resource
).count()
if count > 0:
resource_stats[resource.value] = count
# 活跃用户数
active_users = db.query(AuditLog.user_id).filter(
AuditLog.created_at >= start_date
).distinct().count()
return {
"period_days": days,
"total_operations": total_ops,
"successful_operations": success_ops,
"failed_operations": failed_ops,
"success_rate": round(success_ops / total_ops * 100, 2) if total_ops > 0 else 0,
"by_action": action_stats,
"by_resource": resource_stats,
"active_users_count": active_users
}
@staticmethod
def clean_old_logs(db: Session, days: int = 90) -> int:
"""
清理指定天数之前的日志
Args:
db: 数据库会话
days: 保留天数
Returns:
删除的记录数
"""
cutoff_date = datetime.utcnow() - timedelta(days=days)
deleted = db.query(AuditLog).filter(AuditLog.created_at < cutoff_date).delete()
db.commit()
return deleted
class AuditLogger:
"""审计日志装饰器/上下文管理器"""
def __init__(self, db: Session, user: User, resource: AuditResource, resource_id: Optional[str] = None):
self.db = db
self.user = user
self.resource = resource
self.resource_id = resource_id
self.old_value = None
def set_old_value(self, value: dict):
"""设置旧值(用于更新操作)"""
self.old_value = value
def log(self, action: AuditAction, description: str, new_value: Optional[dict] = None, changed_fields: Optional[List[str]] = None):
"""记录日志"""
AuditService.log(
db=self.db,
user=self.user,
action=action,
resource=self.resource,
resource_id=self.resource_id,
description=description,
old_value=self.old_value,
new_value=new_value,
changed_fields=changed_fields
)