diff --git a/.gitignore b/.gitignore index ede1f53..ee79ecd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,9 @@ .settings/ .vscode/ vender/ +# 业务模块 log 目录(src/modules/log)须入库,特例排除 log/ +!src/modules/log/ composer.lock gitpush.sh pkg/ diff --git a/src/modules/log/__init__.py b/src/modules/log/__init__.py new file mode 100644 index 0000000..56ff4f7 --- /dev/null +++ b/src/modules/log/__init__.py @@ -0,0 +1 @@ +"""模块包。""" diff --git a/src/modules/log/login_log_repository.py b/src/modules/log/login_log_repository.py new file mode 100644 index 0000000..9686efc --- /dev/null +++ b/src/modules/log/login_log_repository.py @@ -0,0 +1,15 @@ +""" +LoginLog 仓储模块 +============================================================= + +数据访问层,继承 BaseRepository 获得通用 CRUD 能力。 +""" + +from src.common.base_repository import BaseRepository +from src.modules.log.models import LoginLog + + +class LoginLogRepository(BaseRepository): + """LoginLog 仓储""" + + model = LoginLog diff --git a/src/modules/log/models.py b/src/modules/log/models.py new file mode 100644 index 0000000..42aa499 --- /dev/null +++ b/src/modules/log/models.py @@ -0,0 +1,277 @@ +""" +登录日志模型模块 +============================================================= + +提供登录日志管理的数据模型定义,用于记录用户登录、退出等操作日志。 +""" + +from src.common.base_db import BaseDB +from src.common.base_model import BaseModel +from config import DB_PREFIX +from src.core.database import db + + +# ============================================================ +# 登录日志模型 +# ============================================================ + + +class LoginLog(BaseModel, BaseDB): + """ + 登录日志模型 + + 用于记录用户登录和退出操作的详细信息,包括:登录账号、日志类型、 + 来源、请求信息、IP地址、归属地、设备信息、请求耗时、操作状态等。 + + Attributes: + id: 主键ID(继承自 BaseModel) + username: 登录账号(String(50),必填) + log_type: 日志类型(Integer,1-登录,2-退出,必填) + source: 登录来源(Integer,0-后台,1-前台,必填) + method: 请求方法(String(10),必填) + request_method: 请求方式(String(255),必填) + url: 请求URL(String(500),必填) + param: 请求参数(Text,可选) + result: 返回结果(Text,必填) + ip: IP地址(String(50),必填) + location: IP归属地(String(100),必填) + os: 操作系统(String(100),必填) + browser: 浏览器(String(100),必填) + consume_time: 请求耗时(Integer,必填) + status: 操作状态(Integer,0-正常,1-异常,必填) + error: 错误信息(Text,可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "login_log" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 登录账号 + username = db.Column(db.String(50), nullable=False, comment="登录账号") + + # 日志类型:1-登录,2-退出 + log_type = db.Column(db.Integer, nullable=False, comment="日志类型:1-登录,2-退出") + + # 登录来源:0-后台,1-前台 + source = db.Column(db.Integer, nullable=False, comment="登录来源:0-后台,1-前台") + + # 请求方法(HTTP方法,如POST/GET) + method = db.Column(db.String(10), nullable=False, comment="请求方法") + + # 请求方式(视图方法路径,如application.views.login.logout) + request_method = db.Column(db.String(255), nullable=False, comment="请求方式") + + # 请求URL + url = db.Column(db.String(500), nullable=False, comment="请求URL") + + # 请求参数 + param = db.Column(db.Text, nullable=True, comment="请求参数") + + # 返回结果 + result = db.Column(db.Text, nullable=False, comment="返回结果") + + # IP地址 + ip = db.Column(db.String(50), nullable=False, comment="IP地址") + + # IP归属地 + location = db.Column(db.String(100), nullable=False, comment="IP归属地") + + # 操作系统 + os = db.Column(db.String(100), nullable=False, comment="操作系统") + + # 浏览器 + browser = db.Column(db.String(100), nullable=False, comment="浏览器") + + # 请求耗时(毫秒) + consume_time = db.Column(db.Integer, nullable=False, comment="请求耗时") + + # 操作状态:0-正常,1-异常 + status = db.Column(db.Integer, nullable=False, comment="操作状态:0-正常,1-异常") + + # 错误信息 + error = db.Column(db.Text, nullable=True, comment="错误信息") + + # ============================================================ + # 常量定义 + # ============================================================ + + class LogType: + """日志类型常量""" + + LOGIN = 1 # 登录 + LOGOUT = 2 # 退出 + + class Source: + """登录来源常量""" + + ADMIN = 0 # 后台 + FRONT = 1 # 前台 + + class Status: + """操作状态常量""" + + NORMAL = 0 # 正常 + ERROR = 1 # 异常 + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "登录日志:{username} - {log_type}" 的字符串 + """ + log_type_name = "登录" if self.log_type == self.LogType.LOGIN else "退出" + return "登录日志:{} - {}".format(self.username, log_type_name) + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "登录日志:{username} - {log_type}" 的字符串 + """ + return self.__repr__() + + +""" +操作日志模型模块 + +与 LoginLog 共用顶部导入。 +""" + + +# ============================================================ +# 操作日志模型 +# ============================================================ + + +class OperationLog(BaseModel, BaseDB): + """ + 操作日志模型 + + 用于记录用户操作行为的详细信息,包括:操作标题、操作类型、 + 来源、请求信息、IP地址、归属地、设备信息、请求耗时、操作状态等。 + + Attributes: + id: 主键ID(继承自 BaseModel) + title: 请求标题(String(150),必填) + type: 请求类型(Integer,必填) + source: 请求来源(Integer,必填,0-后台,1-前台) + method: 请求方法(String(100),必填) + request_method: 请求方式(String(255),必填) + url: 请求URL(String(1000),必填) + param: 请求参数(Text,必填) + result: 请求结果(Text,必填) + ip: 请求IP(String(100),必填) + location: 请求所在区域(String(255),必填) + os: 操作系统(String(100),必填) + browser: 浏览器(String(100),必填) + consume_time: 请求耗时(Integer,必填,单位毫秒) + status: 请求状态(Integer,必填,0-正常,1-异常) + error: 错误描述(Text,必填) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "operation_log" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 请求标题 + title = db.Column(db.String(150), nullable=False, comment="请求标题") + + # 请求类型 + type = db.Column(db.Integer, nullable=False, comment="请求类型") + + # 请求来源:0-后台,1-前台 + source = db.Column(db.Integer, nullable=False, comment="请求来源:0-后台,1-前台") + + # 请求方法(如:UserController.add) + method = db.Column(db.String(100), nullable=False, comment="请求方法") + + # 请求方式(如:GET、POST、PUT、DELETE) + request_method = db.Column(db.String(255), nullable=False, comment="请求方式") + + # 请求URL + url = db.Column(db.String(1000), nullable=False, comment="请求URL") + + # 请求参数 + param = db.Column(db.Text, nullable=False, comment="请求参数") + + # 请求结果 + result = db.Column(db.Text, nullable=False, comment="请求结果") + + # 请求IP + ip = db.Column(db.String(100), nullable=False, comment="请求IP") + + # 请求所在区域 + location = db.Column(db.String(255), nullable=False, comment="请求所在区域") + + # 操作系统 + os = db.Column(db.String(100), nullable=False, comment="操作系统") + + # 浏览器 + browser = db.Column(db.String(100), nullable=False, comment="浏览器") + + # 请求耗时(毫秒) + consume_time = db.Column(db.BigInteger, nullable=False, comment="请求耗时") + + # 请求状态:0-正常,1-异常 + status = db.Column(db.Integer, nullable=False, comment="请求状态:0-正常,1-异常") + + # 错误描述 + error = db.Column(db.Text, nullable=False, comment="错误描述") + + # ============================================================ + # 常量定义 + # ============================================================ + + class Source: + """请求来源常量""" + + ADMIN = 0 # 后台 + FRONT = 1 # 前台 + + class Status: + """请求状态常量""" + + NORMAL = 0 # 正常 + ERROR = 1 # 异常 + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "操作日志:{title} - {method}" 的字符串 + """ + return "操作日志:{} - {}".format(self.title, self.method) + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "操作日志:{title} - {method}" 的字符串 + """ + return self.__repr__() diff --git a/src/modules/log/operation_log_repository.py b/src/modules/log/operation_log_repository.py new file mode 100644 index 0000000..1e50f41 --- /dev/null +++ b/src/modules/log/operation_log_repository.py @@ -0,0 +1,15 @@ +""" +OperationLog 仓储模块 +============================================================= + +数据访问层,继承 BaseRepository 获得通用 CRUD 能力。 +""" + +from src.common.base_repository import BaseRepository +from src.modules.log.models import OperationLog + + +class OperationLogRepository(BaseRepository): + """OperationLog 仓储""" + + model = OperationLog diff --git a/src/modules/log/router.py b/src/modules/log/router.py new file mode 100644 index 0000000..256a169 --- /dev/null +++ b/src/modules/log/router.py @@ -0,0 +1,255 @@ +""" +登录日志管理控制器模块 +============================================================= + +提供登录日志(LoginLog)相关的API接口,包括: +- 登录日志分页查询 +- 登录日志详情查询 +- 登录日志删除(支持批量) +- 登录日志清空 +- 登录日志统计 +- 登录趋势分析 + +所属包: application.views +依赖模块: application.services.login_log - 登录日志业务逻辑层 + application.middleware.check_permission - 权限校验中间件 +""" + +from flask import Blueprint, request + +from application.middleware.check_demo import check_demo +from application.middleware.check_permission import check_permission +from src.modules.log import service as login_log +from src.modules.log import service as operation_log +from src.common.utils.request import parse_batch_ids + +from src.core import response as R + +login_log_blue = Blueprint("login_log", __name__, url_prefix="/login/log") + + +# ============================================================ +# 登录日志分页查询 +# ============================================================ + + +@login_log_blue.route("/page", methods=["GET"]) +@check_permission("sys:login_log:page") +def page(): + """ + 查询登录日志分页数据 + + 功能说明: + 获取登录日志列表的分页数据,支持按账号、日志类型、来源、 + 状态、时间范围等条件筛选。 + + Query Parameters: + pageNo: 当前页码(可选,默认1) + pageSize: 每页记录数(可选,默认PAGE_SIZE) + username: 登录账号(可选,模糊查询) + logType: 日志类型(可选,1-登录,2-退出) + source: 登录来源(可选,0-后台,1-前台) + status: 操作状态(可选,0-正常,1-异常) + startTime: 开始时间(可选) + endTime: 结束时间(可选) + + Returns: + JSON: 登录日志分页数据,包含 records(数据列表)、total(总记录数)等 + """ + return R.ok(data=login_log.get_login_log_page(request.args)) + + +# ============================================================ +# 登录日志详情查询 +# ============================================================ + + +@login_log_blue.route("/detail/", methods=["GET"]) +@check_permission("sys:login_log:detail") +def detail(id): + """ + 查询登录日志详情 + + Args: + id: 登录日志ID + + Returns: + JSON: 登录日志详细信息 + """ + data = login_log.get_login_log_detail(id) + if data: + return R.ok(data=data) + return R.failed("日志不存在") + + +# ============================================================ +# 登录日志删除 +# ============================================================ + + +@login_log_blue.route("/delete/", methods=["DELETE"]) +@check_permission("sys:login_log:delete") +@check_demo +def delete(id): + """ + 删除登录日志 + + Args: + id: 登录日志ID(支持逗号分隔的多个ID) + + Returns: + JSON: 操作结果 + """ + count = login_log.delete_login_logs(id) + return R.ok(msg=f"本次共删除{count}条数据") + + +# ============================================================ +# 登录日志批量删除 +# ============================================================ + + +@login_log_blue.route("/batchDelete", methods=["DELETE"]) +@check_permission("sys:login_log:batch_delete") +@check_demo +def batch_delete(): + """ + 批量删除登录日志 + + 功能说明: + 批量删除登录日志记录,支持传入ID数组。 + + Request Body: + [1, 2, 3] # 登录日志ID数组 + + Returns: + JSON: 操作结果 + """ + id_str, err = parse_batch_ids() + if err: + return err + count = login_log.delete_login_logs(id_str) + return R.ok(msg=f"本次共删除{count}条数据") + + +""" +操作日志管理控制器模块 +============================================================= + +提供操作日志(OperationLog)相关的API接口,包括: +- 操作日志分页查询 +- 操作日志详情查询 +- 操作日志删除(支持批量) +- 操作日志清空 +- 操作日志统计 +- 操作日志类型列表 + +所属包: application.views +依赖模块: application.services.operation_log - 操作日志业务逻辑层 + application.middleware.check_permission - 权限校验中间件 +""" +operation_log_blue = Blueprint("operation_log", __name__, url_prefix="/oper/log") + + +# ============================================================ +# 操作日志分页查询 +# ============================================================ + + +@operation_log_blue.route("/page", methods=["GET"]) +@check_permission("sys:operation_log:page") +def operation_log_page(): + """ + 查询操作日志分页数据 + + 功能说明: + 获取操作日志列表的分页数据,支持按标题、操作类型、来源、 + 状态、时间范围等条件筛选。 + + Query Parameters: + pageNo: 当前页码(可选,默认1) + pageSize: 每页记录数(可选,默认PAGE_SIZE) + title: 请求标题(可选,模糊查询) + type: 请求类型(可选,精确查询) + source: 请求来源(可选,0-后台,1-前台) + status: 请求状态(可选,0-正常,1-异常) + startTime: 开始时间(可选) + endTime: 结束时间(可选) + + Returns: + JSON: 操作日志分页数据,包含 records(数据列表)、total(总记录数)等 + """ + return R.ok(data=operation_log.get_operation_log_page(request.args)) + + +# ============================================================ +# 操作日志详情查询 +# ============================================================ + + +@operation_log_blue.route("/detail/", methods=["GET"]) +@check_permission("sys:operation_log:detail") +def operation_log_detail(id): + """ + 查询操作日志详情 + + Args: + id: 操作日志ID + + Returns: + JSON: 操作日志详细信息 + """ + data = operation_log.get_operation_log_detail(id) + if data: + return R.ok(data=data) + return R.failed("日志不存在") + + +# ============================================================ +# 操作日志删除 +# ============================================================ + + +@operation_log_blue.route("/delete/", methods=["DELETE"]) +@check_permission("sys:operation_log:delete") +@check_demo +def operation_log_delete(id): + """ + 删除操作日志 + + Args: + id: 操作日志ID(支持逗号分隔的多个ID) + + Returns: + JSON: 操作结果 + """ + count = operation_log.delete_operation_logs(id) + return R.ok(msg=f"本次共删除{count}条数据") + + +# ============================================================ +# 操作日志批量删除 +# ============================================================ + + +@operation_log_blue.route("/batchDelete", methods=["DELETE"]) +@check_permission("sys:operation_log:batch_delete") +@check_demo +def operation_log_batch_delete(): + """ + 批量删除操作日志 + + 功能说明: + 批量删除操作日志记录,支持传入ID数组。 + + Request Body: + [1, 2, 3] # 操作日志ID数组 + + Returns: + JSON: 操作结果 + """ + id_str, err = parse_batch_ids() + if err: + return err + count = operation_log.delete_operation_logs(id_str) + return R.ok(msg=f"本次共删除{count}条数据") diff --git a/src/modules/log/service.py b/src/modules/log/service.py new file mode 100644 index 0000000..6082873 --- /dev/null +++ b/src/modules/log/service.py @@ -0,0 +1,593 @@ +""" +登录日志管理服务模块 +============================================================= + +该模块实现了登录日志管理相关的所有业务逻辑。 +包括登录日志的分页查询、详情查询、添加、删除、清空等功能。 + +命名规范: +- 函数参数使用驼峰命名(如 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)