- git 初始化 + .gitignore 补全(根 .env / logs / instance / __pycache__ 等) - 修复 utils/.gitignore 误将整个 utils/ 源码层忽略的问题,补入全部工具源码(含 ip2region.xdb) - 含后端全部代码:application / extensions / utils / config / migrations - 含前端 ui/(后续整体迁走) - 不含根 .env(含密钥,本地保留) Co-Authored-By: Claude <noreply@anthropic.com>
323 lines
9.9 KiB
Python
323 lines
9.9 KiB
Python
"""
|
||
操作日志管理服务模块
|
||
=============================================================
|
||
|
||
该模块实现了操作日志管理相关的所有业务逻辑。
|
||
包括操作日志的分页查询、详情查询、添加、删除、清空、统计等功能。
|
||
|
||
命名规范:
|
||
- 函数参数使用驼峰命名(如 logId, pageNo)
|
||
- 表单字段使用下划线命名(如 title, log_type)- 与数据库字段保持一致
|
||
- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致
|
||
- 私有函数以下划线开头(如 _apply_filters, _build_page_data)
|
||
|
||
功能模块:
|
||
1. 分页查询:支持按标题模糊搜索、操作类型筛选、来源筛选、状态筛选、时间范围筛选
|
||
2. 详情查询:根据ID获取单个操作日志的详细信息
|
||
3. 日志添加:记录用户操作日志
|
||
4. 日志删除:删除指定日志(支持批量)
|
||
5. 日志清空:清空所有操作日志
|
||
6. 统计查询:获取操作日志统计信息
|
||
7. 类型查询:获取操作日志类型列表
|
||
"""
|
||
|
||
import logging
|
||
from datetime import datetime
|
||
|
||
from flask import request
|
||
from sqlalchemy import and_
|
||
|
||
from application.constants.common import PAGE_SIZE
|
||
from application.constants.dict import OPERATION_LOG_SOURCE_LIST, OPERATION_LOG_TYPE_LIST
|
||
from application.models.operation_log import OperationLog
|
||
from extensions import db
|
||
from utils import R
|
||
from utils.model_utils import models_to_list, model_to_dict
|
||
from utils.request import parse_id_list, like_contains
|
||
|
||
# 获取日志记录器
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ============================================================
|
||
# 操作日志分页查询
|
||
# ============================================================
|
||
|
||
def get_operation_log_page():
|
||
"""
|
||
查询操作日志分页数据
|
||
|
||
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": 总页数
|
||
}
|
||
"""
|
||
try:
|
||
# 获取分页参数
|
||
page_no = int(request.args.get('pageNo', 1))
|
||
page_size = int(request.args.get('pageSize', PAGE_SIZE))
|
||
|
||
# 应用筛选条件
|
||
query = _apply_filters()
|
||
|
||
# 记录总数
|
||
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_page_data(log_list, total, page_size, page_no, pages)
|
||
return R.ok(data=page_data)
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询操作日志分页数据异常: {str(e)}")
|
||
return R.failed(msg="查询失败,请稍后重试")
|
||
|
||
|
||
def _apply_filters():
|
||
"""
|
||
应用查询筛选条件
|
||
|
||
根据请求参数动态构建查询条件:
|
||
- 自动过滤已删除记录(is_delete == 0)
|
||
- 支持请求标题模糊查询
|
||
- 支持操作类型精确查询
|
||
- 支持请求来源精确查询
|
||
- 支持请求状态精确查询
|
||
- 支持时间范围查询
|
||
|
||
Returns:
|
||
Query: 应用筛选后的SQLAlchemy查询对象
|
||
"""
|
||
# 基础查询:过滤已删除记录
|
||
query = OperationLog.query.filter(OperationLog.is_delete == 0)
|
||
|
||
# 请求标题模糊筛选
|
||
title = request.args.get('title')
|
||
if title:
|
||
query = query.filter(OperationLog.title.like(like_contains(title), escape='\\'))
|
||
|
||
# 请求类型精确筛选
|
||
log_type = request.args.get('type')
|
||
if log_type:
|
||
query = query.filter(OperationLog.type == log_type)
|
||
|
||
# 请求来源精确筛选
|
||
source = request.args.get('source')
|
||
if source:
|
||
query = query.filter(OperationLog.source == source)
|
||
|
||
# 请求状态精确筛选
|
||
status = request.args.get('status')
|
||
if status:
|
||
query = query.filter(OperationLog.status == status)
|
||
|
||
# 时间范围筛选
|
||
start_time = request.args.get('startTime')
|
||
end_time = request.args.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_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_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_info(log_id: int):
|
||
"""
|
||
根据ID查询操作日志信息(返回模型对象)
|
||
|
||
Args:
|
||
log_id: 操作日志ID
|
||
|
||
Returns:
|
||
OperationLog/None: 操作日志模型对象,不存在或已删除时返回 None
|
||
"""
|
||
if not log_id or log_id <= 0:
|
||
return None
|
||
|
||
try:
|
||
log = OperationLog.query.filter(
|
||
and_(OperationLog.id == log_id, OperationLog.is_delete == 0)
|
||
).first()
|
||
return log
|
||
except Exception as e:
|
||
logger.error(f"查询操作日志信息异常,ID: {log_id}, 错误: {str(e)}")
|
||
return None
|
||
|
||
|
||
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 extensions.sqlalchemy 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):
|
||
"""
|
||
删除操作日志(批量删除)
|
||
|
||
删除
|
||
|
||
Args:
|
||
log_ids: 日志ID字符串,多个ID用逗号分隔,如 "1,2,3"
|
||
|
||
Returns:
|
||
R: 操作结果
|
||
"""
|
||
return OperationLog.batch_delete_r(log_ids)
|