feat: 新增企业级操作审计日志闭环模块(包含底层模型、记录装饰器与前端看板)
This commit is contained in:
@ -1,8 +1,9 @@
|
||||
# app/utils/decorators.py
|
||||
from functools import wraps
|
||||
from flask_jwt_extended import get_jwt, verify_jwt_in_request
|
||||
from flask import jsonify, g
|
||||
from flask_jwt_extended import get_jwt, verify_jwt_in_request, get_jwt_identity
|
||||
from flask import jsonify, g, request
|
||||
import logging
|
||||
import json
|
||||
|
||||
|
||||
def role_required(*roles):
|
||||
@ -87,3 +88,104 @@ def permission_required(permission_code):
|
||||
return fn(*args, **kwargs)
|
||||
return decorator
|
||||
return wrapper
|
||||
|
||||
|
||||
def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target_name_fn=None, get_details_fn=None):
|
||||
"""
|
||||
审计日志装饰器
|
||||
用法: @audit_log(module='inbound_buy', action='create')
|
||||
@audit_log(module='bom', action='update', get_target_id_fn=lambda: ..., get_details_fn=lambda req, resp: ...)
|
||||
"""
|
||||
def wrapper(fn):
|
||||
@wraps(fn)
|
||||
def decorator(*args, **kwargs):
|
||||
# 获取请求上下文
|
||||
claims = get_jwt()
|
||||
user_id = get_jwt_identity()
|
||||
username = claims.get('username', '')
|
||||
display_name = claims.get('display_name', '')
|
||||
|
||||
# 获取IP
|
||||
ip_address = request.headers.get('X-Forwarded-For') or request.remote_addr or ''
|
||||
if ip_address and ',' in ip_address:
|
||||
ip_address = ip_address.split(',')[0].strip()
|
||||
|
||||
# 获取请求信息
|
||||
http_method = request.method
|
||||
url = request.url
|
||||
user_agent = request.headers.get('User-Agent', '')[:500]
|
||||
|
||||
# 解析 action(支持动态)
|
||||
final_action = action
|
||||
if callable(action):
|
||||
final_action = action()
|
||||
|
||||
# 执行原函数
|
||||
response = fn(*args, **kwargs)
|
||||
|
||||
# 只记录成功的请求(响应状态码 200/201)
|
||||
status_code = 200
|
||||
if hasattr(response, 'status_code'):
|
||||
status_code = response.status_code
|
||||
|
||||
if status_code in [200, 201]:
|
||||
try:
|
||||
from app.models.audit import AuditLog
|
||||
from app.extensions import db
|
||||
from flask import current_app
|
||||
|
||||
# 获取 target_id
|
||||
target_id = None
|
||||
if get_target_id_fn:
|
||||
try:
|
||||
target_id = get_target_id_fn()
|
||||
except Exception:
|
||||
pass
|
||||
if not target_id and hasattr(response, 'json'):
|
||||
resp_data = response.get_json()
|
||||
if resp_data and isinstance(resp_data, dict):
|
||||
target_id = resp_data.get('id')
|
||||
|
||||
# 获取 target_name
|
||||
target_name = None
|
||||
if get_target_name_fn:
|
||||
try:
|
||||
target_name = get_target_name_fn()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 获取 details
|
||||
details = None
|
||||
if get_details_fn:
|
||||
try:
|
||||
details = get_details_fn(request, response)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 保存日志
|
||||
log_entry = AuditLog(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
display_name=display_name,
|
||||
action=final_action or http_method.lower(),
|
||||
module=module,
|
||||
target_id=str(target_id) if target_id else None,
|
||||
target_name=target_name,
|
||||
details=details,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
method=http_method,
|
||||
url=url,
|
||||
status_code=status_code
|
||||
)
|
||||
db.session.add(log_entry)
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"审计日志记录失败: {str(e)}")
|
||||
db.session.rollback()
|
||||
|
||||
return response
|
||||
|
||||
return decorator
|
||||
return wrapper
|
||||
|
||||
Reference in New Issue
Block a user