refactor: P5 DI 注入 + 中间件迁入 src/core
【依赖注入(解决 core→modules 倒置)】 - src/core/permission.py:PermissionProvider 协议 + check_permission (不再 import menu 服务,改从 current_app.extensions['permission_provider'] 获取) - src/core/middleware.py:整合 check_login/check_demo/transaction/op_log/error_handler (op_log 写入器 DI:current_app.extensions['operation_log_writer']) - application/factory.py:组合根注入 MenuPermissionProvider + record_operation_log - src/core/scheduler.py 懒加载指向 src.modules.job.scheduler_service 【验收】src/core 内零 application 引用;DI 注入验证通过; 未登录 401、白名单放行;12 passed;core+中间件 flake8 零告警 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
35f89395a2
commit
0b0fa0c047
@@ -1,220 +1,8 @@
|
||||
"""
|
||||
操作日志自动记录中间件
|
||||
兼容 shim(绞杀者迁移期间保留)
|
||||
=============================================================
|
||||
|
||||
通过 after_request 钩子自动记录写操作(POST/PUT/DELETE/PATCH)日志。
|
||||
无需在每个视图函数中手动调用 record_operation_log。
|
||||
|
||||
记录规则:
|
||||
- 只记录写操作(POST/PUT/DELETE/PATCH),GET/OPTIONS/HEAD 不记录
|
||||
- 只记录成功操作(HTTP 2xx 且业务 code=0)
|
||||
- 内部调用(定时任务等)不记录
|
||||
- 登录/退出/验证码等路径不记录(由 login_log 单独记录)
|
||||
- 日志记录失败不影响主流程
|
||||
操作日志中间件已迁移至 src/core/middleware.py(写入器由应用工厂注入)。
|
||||
本文件仅作兼容转发,最终在 P7(切尾)阶段删除。
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from flask import g, request
|
||||
|
||||
from application.models.operation_log import OperationLog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 不记录操作日志的路径前缀(登录/退出/验证码等由 login_log 单独记录)
|
||||
_EXCLUDED_PREFIXES = ('/login', '/captcha', '/logout')
|
||||
|
||||
# HTTP 方法 → 操作日志类型 映射
|
||||
# 类型值参考 application/constants/dict.py 的 OPERATION_LOG_TYPE_LIST
|
||||
_METHOD_LOG_TYPE = {
|
||||
'POST': 3, # 新增
|
||||
'PUT': 4, # 修改
|
||||
'DELETE': 5, # 删除
|
||||
'PATCH': 4, # 修改
|
||||
}
|
||||
|
||||
# 路径后缀 → 操作日志类型 覆盖映射(更精确)
|
||||
_PATH_ACTION_TYPE = {
|
||||
'/add': 3, # 新增
|
||||
'/update': 4, # 修改
|
||||
'/delete': 5, # 删除
|
||||
'/status': 4, # 状态变更→修改
|
||||
'/batchdelete': 5, # 批量删除
|
||||
'/reset': 4, # 重置→修改
|
||||
'/save': 4, # 保存→修改
|
||||
'/sort': 4, # 排序→修改
|
||||
}
|
||||
|
||||
|
||||
def register_operation_log(app):
|
||||
"""
|
||||
注册操作日志自动记录中间件
|
||||
|
||||
在 after_request 中自动记录写操作日志。
|
||||
|
||||
Args:
|
||||
app: Flask 应用实例
|
||||
"""
|
||||
|
||||
@app.after_request
|
||||
def record_op_log(response):
|
||||
# 只记录写操作
|
||||
if request.method not in ('POST', 'PUT', 'DELETE', 'PATCH'):
|
||||
return response
|
||||
|
||||
# 排除登录/退出/验证码等路径
|
||||
path = request.path
|
||||
if any(path.startswith(prefix) for prefix in _EXCLUDED_PREFIXES):
|
||||
return response
|
||||
|
||||
# 只记录成功操作(HTTP 2xx 且业务 code=0)
|
||||
if not (200 <= response.status_code < 300):
|
||||
return response
|
||||
|
||||
try:
|
||||
data = response.get_json(silent=True)
|
||||
if data and data.get('code', 0) != 0:
|
||||
return response
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 内部调用不记录
|
||||
if getattr(g, '_internal_call', False):
|
||||
return response
|
||||
|
||||
# 异步记录,不阻塞响应
|
||||
try:
|
||||
_write_log(response)
|
||||
except Exception as e:
|
||||
logger.warning(f"记录操作日志失败: {str(e)}")
|
||||
|
||||
return response
|
||||
|
||||
logger.info("操作日志中间件注册成功")
|
||||
|
||||
|
||||
def _write_log(response):
|
||||
"""写入操作日志"""
|
||||
from application.services import operation_log
|
||||
from utils.security import get_username
|
||||
from utils.ip import get_client_ip
|
||||
from utils.ip2region import get_ip_location
|
||||
from utils.user_agent import parse_user_agent
|
||||
|
||||
username = get_username() or 'system'
|
||||
client_ip = get_client_ip()
|
||||
ua_info = parse_user_agent()
|
||||
|
||||
# 确定操作类型
|
||||
log_type = _infer_log_type(request.method, request.path)
|
||||
|
||||
# 构建标题:从路径提取模块名,如 /user/add → "用户管理-新增"
|
||||
title = _build_title(request.path)
|
||||
|
||||
# 请求参数(限制长度,避免存入过大的数据)
|
||||
param = _get_request_param()
|
||||
|
||||
# 返回结果(限制长度)
|
||||
result = _get_response_result(response)
|
||||
|
||||
# 请求耗时
|
||||
start_time = getattr(g, 'request_start_time', None)
|
||||
consume_time = int((time.time() - start_time) * 1000) if start_time else 0
|
||||
|
||||
operation_log.record_operation_log(
|
||||
title=title,
|
||||
log_type=log_type,
|
||||
source=OperationLog.Source.ADMIN,
|
||||
status=OperationLog.Status.NORMAL,
|
||||
method=request.method,
|
||||
request_method=request.endpoint or '',
|
||||
url=request.path,
|
||||
param=param,
|
||||
result=result,
|
||||
ip=client_ip,
|
||||
location=get_ip_location(client_ip),
|
||||
os=ua_info.get('os', '未知'),
|
||||
browser=ua_info.get('browser', '未知'),
|
||||
consume_time=consume_time,
|
||||
error='',
|
||||
username=username,
|
||||
)
|
||||
|
||||
|
||||
def _infer_log_type(method, path):
|
||||
"""根据请求方法和路径推断操作日志类型"""
|
||||
# 优先根据路径后缀匹配
|
||||
path_lower = path.lower()
|
||||
for suffix, log_type in _PATH_ACTION_TYPE.items():
|
||||
if path_lower.endswith(suffix):
|
||||
return log_type
|
||||
|
||||
# 回退到 HTTP 方法映射
|
||||
return _METHOD_LOG_TYPE.get(method, 18) # 18=其他
|
||||
|
||||
|
||||
def _get_request_param():
|
||||
"""获取请求参数"""
|
||||
try:
|
||||
if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
|
||||
# 优先解析 JSON body(batchDelete 等 DELETE 请求也带 body)
|
||||
data = request.get_json(silent=True)
|
||||
if data:
|
||||
import json
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
# 回退到 URL 查询参数
|
||||
if request.args:
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
return json.dumps(OrderedDict(request.args), ensure_ascii=False)
|
||||
except Exception:
|
||||
pass
|
||||
return ''
|
||||
|
||||
|
||||
def _get_response_result(response):
|
||||
"""获取响应结果中的data字段"""
|
||||
try:
|
||||
data = response.get_json(silent=True)
|
||||
if data and isinstance(data, dict):
|
||||
result_data = data.get('data')
|
||||
if result_data is not None:
|
||||
import json
|
||||
return json.dumps(result_data, ensure_ascii=False)
|
||||
# data字段为空时取msg
|
||||
return data.get('msg', '')
|
||||
except Exception:
|
||||
pass
|
||||
return ''
|
||||
|
||||
|
||||
def _build_title(path):
|
||||
"""从请求路径构建操作标题"""
|
||||
# 路径格式: /user/add, /role/update, /menu/delete 等
|
||||
parts = path.strip('/').split('/')
|
||||
if len(parts) >= 2:
|
||||
module = parts[0]
|
||||
# 模块名中文化映射
|
||||
module_names = {
|
||||
'user': '用户管理', 'role': '角色管理', 'menu': '菜单管理',
|
||||
'dept': '部门管理', 'position': '岗位管理', 'level': '职级管理',
|
||||
'param': '参数配置', 'dict': '字典管理', 'dictitem': '字典项管理',
|
||||
'config': '系统配置', 'configitem': '配置项管理',
|
||||
'category': '分类管理', 'article': '文章管理',
|
||||
'notice': '通知管理', 'link': '友情链接',
|
||||
'job': '定时任务', 'login': '登录管理',
|
||||
'loginlog': '登录日志', 'operationlog': '操作日志',
|
||||
'upload': '文件上传',
|
||||
}
|
||||
module_name = module_names.get(module.lower(), module)
|
||||
action = parts[-1] if len(parts) > 1 else ''
|
||||
action_names = {
|
||||
'add': '新增', 'update': '修改', 'delete': '删除',
|
||||
'status': '状态变更', 'batchdelete': '批量删除',
|
||||
'reset': '重置', 'save': '保存', 'sort': '排序',
|
||||
'page': '查询', 'detail': '查询', 'list': '查询',
|
||||
}
|
||||
action_name = action_names.get(action.lower(), action)
|
||||
return f"{module_name}-{action_name}" if action_name else module_name
|
||||
return path
|
||||
from src.core.middleware import * # noqa: F401,F403
|
||||
|
||||
Reference in New Issue
Block a user