import logging import time from sqlalchemy import and_ from apps.constants.constants import LOG_TYPE_LIST, LOGIN_SOURCE_LIST, LOGIN_STATUS_LIST from apps.constants.message import PAGE_SIZE from apps.models.login_log import LoginLog from extensions.sqlalchemy import commit_independently from extensions import get_db_session from utils import R from utils.ip import get_client_ip from utils.ip2region import get_ip_location from utils.ua import parse_user_agent from utils.request import like_contains, parse_id_list logger = logging.getLogger(__name__) # ============================================================ # 核心便捷方法 # ============================================================ def record_login_log(request, username, log_type, source, status, **kwargs): """ 记录登录日志 使用独立session写入,不受请求事务commit/rollback影响。 日志是审计数据,无论业务成功或失败都应持久化。 Args: request: FastAPI Request 对象 username: 登录账号 log_type: 日志类型(1-登录,2-退出) source: 登录来源(0-后台,1-前台) status: 操作状态(0-正常,1-异常) **kwargs: 其他可选参数 - method: 请求方法 - request_method: 请求方式 - url: 请求URL - param: 请求参数 - result: 返回结果 - consume_time: 请求耗时(毫秒) - error: 错误信息 """ try: # 从 request 提取信息 ip = get_client_ip(request) location = get_ip_location(ip) ua = request.headers.get('User-Agent', '') os_name, browser_name = parse_user_agent(ua) log = LoginLog( username=username, log_type=log_type, source=source, method=kwargs.get('method', request.method), request_method=kwargs.get('request_method', ''), url=kwargs.get('url', str(request.url)), param=kwargs.get('param', ''), result=kwargs.get('result', ''), ip=ip, location=location, os=os_name, browser=browser_name, consume_time=kwargs.get('consume_time', 0), status=status, error=kwargs.get('error', ''), ) commit_independently(log) return log except Exception as e: logger.error(f"记录登录日志异常: {str(e)}") return None # ============================================================ # CRUD 服务 # ============================================================ def get_login_log_page(request): db = get_db_session() # 页码 page = int(request.query_params.get("pageNo", 1)) # 每页数 limit = int(request.query_params.get("pageSize", PAGE_SIZE)) # 实例化查询对象 query = db.query(LoginLog).filter(LoginLog.is_delete == 0) # 登录账号 username = request.query_params.get('username') if username: query = query.filter(LoginLog.username.like(like_contains(username), escape='\\')) # 日志类型 log_type = request.query_params.get('log_type') if log_type: query = query.filter(LoginLog.log_type == log_type) # 登录来源 source = request.query_params.get('source') if source: query = query.filter(LoginLog.source == source) # 操作状态 status = request.query_params.get('status') if status: query = query.filter(LoginLog.status == status) # 排序 query = query.order_by(LoginLog.id.desc()) # 记录总数 count = query.count() # 分页查询 log_list = query.limit(limit).offset((page - 1) * limit).all() # 若当前页超出末页(数据被并发删除),自动回退到第一页 if page > 1 and not log_list: page = 1 count = query.count() log_list = query.limit(limit).offset(0).all() # 实例化结果 result = [] if len(log_list) > 0: for item in log_list: data = item.to_dict() data['log_type_name'] = LOG_TYPE_LIST.get(item.log_type) data['source_name'] = LOGIN_SOURCE_LIST.get(item.source) data['status_name'] = LOGIN_STATUS_LIST.get(item.status) result.append(data) return R.ok(data=result, count=count) def delete_login_log(ids_str): return LoginLog.batch_delete_r(ids_str)