""" 登录日志管理服务模块 ============================================================= 该模块实现了登录日志管理相关的所有业务逻辑。 包括登录日志的分页查询、详情查询、添加、删除、清空等功能。 命名规范: - 函数参数使用驼峰命名(如 logId, pageNo) - 表单字段使用下划线命名(如 username, log_type)- 与数据库字段保持一致 - 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 - 私有函数以下划线开头(如 _apply_filters, _build_page_data) 功能模块: 1. 分页查询:支持按账号模糊搜索、日志类型筛选、来源筛选、状态筛选、时间范围筛选 2. 详情查询:根据ID获取单个登录日志的详细信息 3. 日志添加:记录用户登录/退出操作日志 4. 日志删除:删除指定日志(支持批量) 5. 日志清空:清空所有登录日志 6. 统计查询:获取登录日志统计信息 """ import logging from datetime import datetime from src.common.constants.common import PAGE_SIZE from src.common.constants.dictionary_constants import ( OPERATION_LOG_SOURCE_LIST, OPERATION_LOG_TYPE_LIST, LOGIN_LOG_TYPE_LIST, ) from src.modules.log.models import LoginLog, OperationLog from src.modules.log.login_log_repository import LoginLogRepository from src.modules.log.operation_log_repository import OperationLogRepository from src.core.exceptions import BizError from src.common.utils.model_utils import models_to_list, model_to_dict from src.common.utils.request import parse_id_list, like_contains # 获取日志记录器 logger = logging.getLogger(__name__) # ============================================================ # 登录日志分页查询 # ============================================================ def get_login_log_page(params: dict) -> dict: """ 查询登录日志分页数据 Args: request: Flask request对象,包含查询参数 - pageNo: 当前页码(可选,默认为1) - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - username: 登录账号(可选,模糊查询) - logType: 日志类型(可选,1-登录,2-退出) - source: 登录来源(可选,0-后台,1-前台) - status: 操作状态(可选,0-正常,1-异常) - startTime: 开始时间(可选) - endTime: 结束时间(可选) Returns: R: 包含分页数据的JSON响应,格式为: { "records": 日志列表, "total": 总记录数, "size": 每页大小, "current": 当前页码, "pages": 总页数 } """ # 获取分页参数 page_no = int(params.get("pageNo", 1)) page_size = int(params.get("pageSize", PAGE_SIZE)) # 应用筛选条件 query = _apply_filters(params) # 记录总数 total = query.count() # 计算总页数 pages = (total + page_size - 1) // page_size if total > 0 else 0 # 分页查询(按创建时间倒序) log_list = query.order_by(LoginLog.create_time.desc()).limit(page_size).offset((page_no - 1) * page_size).all() # 构建返回数据 page_data = _build_page_data(log_list, total, page_size, page_no, pages) return page_data def _apply_filters(params: dict): """ 应用查询筛选条件 根据请求参数动态构建查询条件: - 自动过滤已删除记录(is_delete == 0) - 支持登录账号模糊查询 - 支持日志类型精确查询 - 支持登录来源精确查询 - 支持操作状态精确查询 - 支持时间范围查询 Returns: Query: 应用筛选后的SQLAlchemy查询对象 """ # 基础查询:过滤已删除记录 query = LoginLog.query.filter(LoginLog.is_delete == 0) # 登录账号模糊筛选 username = params.get("username") if username: query = query.filter(LoginLog.username.like(like_contains(username), escape="\\")) # 日志类型精确筛选 log_type = params.get("logType") if log_type: query = query.filter(LoginLog.log_type == log_type) # 登录来源精确筛选 source = params.get("source") if source: query = query.filter(LoginLog.source == source) # 操作状态精确筛选 status = params.get("status") if status: query = query.filter(LoginLog.status == status) # 时间范围筛选 start_time = params.get("startTime") end_time = params.get("endTime") if start_time: try: start_date = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S") query = query.filter(LoginLog.create_time >= start_date) except ValueError: pass if end_time: try: end_date = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S") query = query.filter(LoginLog.create_time <= end_date) except ValueError: pass return query def _build_page_data(log_list, total, page_size, page_no, pages): """ 构建分页返回数据 Args: log_list: 登录日志对象列表 total: 总记录数 page_size: 每页大小 page_no: 当前页码 pages: 总页数 Returns: dict: 分页数据,包含 records、total、size、current、pages """ # 将模型对象列表转换为字典列表 records = models_to_list(log_list) # 遍历数据源 for record in records: # 日志来源 record["sourceText"] = OPERATION_LOG_SOURCE_LIST.get(record.get("source"), "") # 日志类型 record["logTypeText"] = LOGIN_LOG_TYPE_LIST.get(record.get("logType"), "") return {"records": records, "total": total, "size": page_size, "current": page_no, "pages": pages} # ============================================================ # 登录日志详情查询 # ============================================================ def get_login_log_detail(log_id): """ 根据ID查询登录日志详情(返回字典格式) Args: log_id: 登录日志ID Returns: dict/None: 登录日志详情字典,不存在时返回 None """ try: log = get_info(log_id) if not log: return None return model_to_dict(log) except Exception as e: logger.error(f"查询登录日志详情异常,ID: {log_id}, 错误: {str(e)}") return None def get_info(log_id: int): """ 根据ID查询登录日志信息(返回模型对象) Args: log_id: 登录日志ID Returns: LoginLog/None: 登录日志模型对象,不存在或已删除时返回 None """ return LoginLogRepository.get_by_id(log_id) def record_login_log(username, source, status, **kwargs): """ 记录登录日志(便捷方法) 供登录/退出接口直接调用,无需通过HTTP请求。 使用独立session写入,不受请求事务commit/rollback影响。 Args: username: 登录账号 source: 登录来源(0-后台,1-前台) status: 操作状态(0-正常,1-异常) **kwargs: 其他可选参数 - log_type: 日志类型(默认1-登录) - method: 请求方法 - request_method: 请求方式 - url: 请求URL - param: 请求参数 - result: 返回结果 - ip: IP地址 - location: IP归属地 - os: 操作系统 - browser: 浏览器 - consume_time: 请求耗时 - error: 错误信息 Returns: LoginLog/None: 创建的日志对象,失败返回None """ try: log_data = { "username": username, "log_type": kwargs.get("log_type", LoginLog.LogType.LOGIN), "source": source, "method": kwargs.get("method", ""), "request_method": kwargs.get("request_method", ""), "url": kwargs.get("url", ""), "param": kwargs.get("param", ""), "result": kwargs.get("result", ""), "ip": kwargs.get("ip", ""), "location": kwargs.get("location", ""), "os": kwargs.get("os", ""), "browser": kwargs.get("browser", ""), "consume_time": kwargs.get("consume_time", 0), "status": status, "error": kwargs.get("error", ""), } log = LoginLog(**log_data) log.create_user = username log.update_user = username # 使用独立session写入,不受请求事务影响 # 日志是审计数据,无论业务成功或失败都应持久化 from src.core.database import commit_independently commit_independently(log) return log except Exception as e: logger.error(f"记录登录日志异常: {str(e)}") return None # ============================================================ # 登录日志删除 # ============================================================ def delete_login_logs(log_ids: str) -> int: """ 删除登录日志(批量删除) 删除 Args: log_ids: 日志ID字符串,多个ID用逗号分隔,如 "1,2,3" Returns: R: 操作结果 """ log_ids_list = parse_id_list(log_ids) if not log_ids_list: raise BizError("无效的记录ID") return LoginLog.batch_delete(log_ids_list) """ 操作日志管理服务模块 ============================================================= 该模块实现了操作日志管理相关的所有业务逻辑。 包括操作日志的分页查询、详情查询、添加、删除、清空、统计等功能。 命名规范: - 函数参数使用驼峰命名(如 logId, pageNo) - 表单字段使用下划线命名(如 title, log_type)- 与数据库字段保持一致 - 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 - 私有函数以下划线开头(如 _apply_filters, _build_page_data) 功能模块: 1. 分页查询:支持按标题模糊搜索、操作类型筛选、来源筛选、状态筛选、时间范围筛选 2. 详情查询:根据ID获取单个操作日志的详细信息 3. 日志添加:记录用户操作日志 4. 日志删除:删除指定日志(支持批量) 5. 日志清空:清空所有操作日志 6. 统计查询:获取操作日志统计信息 7. 类型查询:获取操作日志类型列表 """ # ============================================================ # 操作日志分页查询 # ============================================================ def get_operation_log_page(params: dict) -> dict: """ 查询操作日志分页数据 Args: request: Flask request对象,包含查询参数 - pageNo: 当前页码(可选,默认为1) - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - title: 请求标题(可选,模糊查询) - type: 请求类型(可选,精确查询) - source: 请求来源(可选,0-后台,1-前台) - status: 请求状态(可选,0-正常,1-异常) - startTime: 开始时间(可选) - endTime: 结束时间(可选) Returns: R: 包含分页数据的JSON响应,格式为: { "records": 日志列表, "total": 总记录数, "size": 每页大小, "current": 当前页码, "pages": 总页数 } """ # 获取分页参数 page_no = int(params.get("pageNo", 1)) page_size = int(params.get("pageSize", PAGE_SIZE)) # 应用筛选条件 query = _apply_operation_log_filters(params) # 记录总数 total = query.count() # 计算总页数 pages = (total + page_size - 1) // page_size if total > 0 else 0 # 分页查询(按创建时间倒序) log_list = query.order_by(OperationLog.create_time.desc()).limit(page_size).offset((page_no - 1) * page_size).all() # 构建返回数据 page_data = _build_operation_log_page_data(log_list, total, page_size, page_no, pages) return page_data def _apply_operation_log_filters(params: dict): """ 应用查询筛选条件 根据请求参数动态构建查询条件: - 自动过滤已删除记录(is_delete == 0) - 支持请求标题模糊查询 - 支持操作类型精确查询 - 支持请求来源精确查询 - 支持请求状态精确查询 - 支持时间范围查询 Returns: Query: 应用筛选后的SQLAlchemy查询对象 """ # 基础查询:过滤已删除记录 query = OperationLog.query.filter(OperationLog.is_delete == 0) # 请求标题模糊筛选 title = params.get("title") if title: query = query.filter(OperationLog.title.like(like_contains(title), escape="\\")) # 请求类型精确筛选 log_type = params.get("type") if log_type: query = query.filter(OperationLog.type == log_type) # 请求来源精确筛选 source = params.get("source") if source: query = query.filter(OperationLog.source == source) # 请求状态精确筛选 status = params.get("status") if status: query = query.filter(OperationLog.status == status) # 时间范围筛选 start_time = params.get("startTime") end_time = params.get("endTime") if start_time: try: start_date = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S") query = query.filter(OperationLog.create_time >= start_date) except ValueError: pass if end_time: try: end_date = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S") query = query.filter(OperationLog.create_time <= end_date) except ValueError: pass return query def _build_operation_log_page_data(log_list, total, page_size, page_no, pages): """ 构建分页返回数据 Args: log_list: 操作日志对象列表 total: 总记录数 page_size: 每页大小 page_no: 当前页码 pages: 总页数 Returns: dict: 分页数据,包含 records、total、size、current、pages """ # 将模型对象列表转换为字典列表 records = models_to_list(log_list) # 遍历数据源 for record in records: # 日志来源 record["sourceText"] = OPERATION_LOG_SOURCE_LIST.get(record.get("source"), "") # 日志类型 record["typeText"] = OPERATION_LOG_TYPE_LIST.get(record.get("type"), "") return {"records": records, "total": total, "size": page_size, "current": page_no, "pages": pages} # ============================================================ # 操作日志详情查询 # ============================================================ def get_operation_log_detail(log_id): """ 根据ID查询操作日志详情(返回字典格式) Args: log_id: 操作日志ID Returns: dict/None: 操作日志详情字典,不存在时返回 None """ try: log = get_operation_log_info(log_id) if not log: return None data = model_to_dict(log) # 日志来源 data["sourceText"] = OPERATION_LOG_SOURCE_LIST.get(data.get("source"), "") return data except Exception as e: logger.error(f"查询操作日志详情异常,ID: {log_id}, 错误: {str(e)}") return None def get_operation_log_info(log_id: int): """ 根据ID查询操作日志信息(返回模型对象) Args: log_id: 操作日志ID Returns: OperationLog/None: 操作日志模型对象,不存在或已删除时返回 None """ return OperationLogRepository.get_by_id(log_id) def record_operation_log(title, log_type, source, status, **kwargs): """ 记录操作日志(便捷方法) 供业务接口直接调用,无需通过HTTP请求。 使用独立session写入,不受请求事务commit/rollback影响。 Args: title: 请求标题 log_type: 请求类型 source: 请求来源(0-后台,1-前台) status: 操作状态(0-正常,1-异常) **kwargs: 其他可选参数 - method: 请求方法 - request_method: 请求方式 - url: 请求URL - param: 请求参数 - result: 返回结果 - ip: IP地址 - location: IP归属地 - os: 操作系统 - browser: 浏览器 - consume_time: 请求耗时 - error: 错误信息 - username: 操作用户 Returns: OperationLog/None: 创建的日志对象,失败返回None """ try: log_data = { "title": title, "type": log_type, "source": source, "method": kwargs.get("method", ""), "request_method": kwargs.get("request_method", ""), "url": kwargs.get("url", ""), "param": kwargs.get("param", ""), "result": kwargs.get("result", ""), "ip": kwargs.get("ip", ""), "location": kwargs.get("location", ""), "os": kwargs.get("os", ""), "browser": kwargs.get("browser", ""), "consume_time": kwargs.get("consume_time", 0), "status": status, "error": kwargs.get("error", ""), } log = OperationLog(**log_data) log.create_user = kwargs.get("username", "system") log.update_user = kwargs.get("username", "system") # 使用独立session写入,不受请求事务影响 # 日志是审计数据,无论业务成功或失败都应持久化 from src.core.database import commit_independently commit_independently(log) return log except Exception as e: logger.error(f"记录操作日志异常: {str(e)}") return None # ============================================================ # 操作日志删除 # ============================================================ def delete_operation_logs(log_ids: str) -> int: """ 删除操作日志(批量删除) 删除 Args: log_ids: 日志ID字符串,多个ID用逗号分隔,如 "1,2,3" Returns: R: 操作结果 """ log_ids_list = parse_id_list(log_ids) if not log_ids_list: raise BizError("无效的记录ID") return OperationLog.batch_delete(log_ids_list)