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 )