refactor: P2 core+common 建设(应用工厂/扩展/工具层迁入 src/)

【结构】
- src/core/:database/response/jwt/password/security/redis_client/mail/scheduler
- src/common/:base_model/base_db/forms(base_form+fields)/constants/utils
- src/models_registry.py:模型聚合器(Flask-Migrate 全量表识别)

【绞杀者 shim】extensions/、utils/、application/constants|forms|models 打兼容转发
【兼容修复】应用工厂抽取至 application/factory.py(P2 前置,P1 已完成)

【基线修复】application/models(25) + middleware(7) 原被嵌套 .gitignore(*)
误排除出版本库,已补入(base_model/base_db 现为 shim,原版在 src/common/)

【登记既有缺陷】
1. flask db CLI 因 .env FLASK_VERSION 与 --version 冲突报错 → Makefile 已加 FLASK_SKIP_DOTENV=1
2. 本机 Redis 未设密码但 REDIS_AUTH=True,/captcha 报 AUTH 错误(环境问题)

【验收】22 模型表全量识别;src/ flake8 零告警;pytest 5 passed;应用启动;
/login 表单校验、/user/page 鉴权拦截冒烟正常

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DjangoAdmin Dev
2026-08-07 13:20:46 +08:00
co-authored by Claude
parent 92130ef070
commit 658ee6d596
96 changed files with 9926 additions and 4153 deletions
+220
View File
@@ -0,0 +1,220 @@
"""
操作日志自动记录中间件
=============================================================
通过 after_request 钩子自动记录写操作(POST/PUT/DELETE/PATCH)日志。
无需在每个视图函数中手动调用 record_operation_log。
记录规则:
- 只记录写操作(POST/PUT/DELETE/PATCH),GET/OPTIONS/HEAD 不记录
- 只记录成功操作(HTTP 2xx 且业务 code=0)
- 内部调用(定时任务等)不记录
- 登录/退出/验证码等路径不记录(由 login_log 单独记录)
- 日志记录失败不影响主流程
"""
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