refactor: P4c 最终批次-1 log 模块(login_log+operation_log 合并服务)

【关键修复】根 .gitignore 模板规则 log/ 误忽略 src/modules/log/,
log 模块自 P3c 起未入库 → 加 !src/modules/log/ 特例排除并补入

【修复合并遗留 bug】
- 两个 get_info/_apply_filters/_build_page_data 重名互相覆盖 → 重命名
- log router 误导入 src.modules.auth 为 login_log → 改 log service
- 双 blueprint 视图函数重名 → 重命名 operation 版本
- 双 import 块合并(OPERATION_LOG_TYPE_LIST 并入顶部)

【改造】page 函数 params 化返回 dict;get_info 委托 Repository
【验收】flake8 零告警;应用启动;log 分页运行时正常;12 passed

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DjangoAdmin Dev
2026-08-07 16:11:26 +08:00
co-authored by Claude
parent 3dbce04eb1
commit a054b14485
7 changed files with 1158 additions and 0 deletions
+593
View File
@@ -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)