1
0
forked from erp-dev/erp

feat: log formatted

This commit is contained in:
2026-06-26 17:03:54 +08:00
parent e8bf4b5e49
commit cb6589c884
26 changed files with 1410 additions and 50 deletions

View File

@@ -18,10 +18,10 @@ class ApiAuditLogMiddleware:
"""
API审计日志中间件
用于记录特定URL前缀的POST请求保存"创建"操作的历史现场。
用于记录特定URL前缀的POST/PUT/PATCH请求,保存"创建/更新"操作的历史现场。
特性:
- 只记录POST请求
- 只记录POST、PUT、PATCH请求
- 通过URL前缀白名单过滤
- 支持multipart/form-data请求文件字段只记录元信息
- 通过Celery异步写入不阻塞API响应
@@ -42,8 +42,8 @@ class ApiAuditLogMiddleware:
if not self.enabled:
return self.get_response(request)
# 只处理POST请求
if request.method != 'POST':
# 只处理POST、PUT、PATCH请求
if request.method not in ('POST', 'PUT', 'PATCH'):
return self.get_response(request)
# 检查URL是否在白名单中
@@ -51,7 +51,7 @@ class ApiAuditLogMiddleware:
logger.debug('ApiAuditLog: URL不匹配白名单, path=%s, prefixes=%s', request.path, self.url_prefixes)
return self.get_response(request)
logger.info('ApiAuditLog: 捕获到POST请求, path=%s', request.path)
logger.info('ApiAuditLog: 捕获到%s请求, path=%s', request.method, request.path)
# 在请求进入视图前,缓存请求体数据
# 注意request.body只能读取一次需要在这里缓存
@@ -86,30 +86,54 @@ class ApiAuditLogMiddleware:
return True
return False
def _get_post_and_files(self, request):
"""
获取表单字段和文件兼容PUT/PATCH请求
Django的request.POST/request.FILES只在method为POST时才会被解析
PUT/PATCH请求即使是multipart/form-data或urlencoded也不会自动填充
因此这里针对非POST方法手动解析。
"""
if request.method == 'POST':
return request.POST, request.FILES
content_type = request.content_type or ''
if 'multipart/form-data' in content_type:
try:
return request.parse_file_upload(request.META, io.BytesIO(request.body))
except MultiPartParserError:
return QueryDict(), MultiValueDict()
elif 'application/x-www-form-urlencoded' in content_type:
return QueryDict(request.body, encoding=request.encoding or settings.DEFAULT_CHARSET), MultiValueDict()
return QueryDict(), MultiValueDict()
def _extract_request_data(self, request) -> dict:
"""
提取请求数据
对于multipart/form-data请求分别处理表单字段和文件字段。
文件字段只记录元信息(文件名、大小、类型),不记录二进制内容。
"""
content_type = request.content_type or ''
if 'multipart/form-data' in content_type:
# multipart请求分别处理表单字段和文件
data = {}
post, files = self._get_post_and_files(request)
# 处理普通表单字段
for key, values in request.POST.lists():
for key, values in post.lists():
if len(values) == 1:
data[key] = values[0]
else:
data[key] = values
# 处理文件字段:只记录元信息
for key, files in request.FILES.lists():
for key, file_list in files.lists():
file_infos = []
for f in files:
for f in file_list:
file_infos.append({
'_type': 'file',
'name': f.name,
@@ -120,26 +144,27 @@ class ApiAuditLogMiddleware:
data[key] = file_infos[0]
else:
data[key] = file_infos
return data
elif 'application/json' in content_type:
# JSON请求解析body
try:
return json.loads(request.body.decode('utf-8'))
except (json.JSONDecodeError, UnicodeDecodeError):
return {'_raw': request.body.decode('utf-8', errors='replace')}
elif 'application/x-www-form-urlencoded' in content_type:
# 表单请求
data = {}
for key, values in request.POST.lists():
post, _ = self._get_post_and_files(request)
for key, values in post.lists():
if len(values) == 1:
data[key] = values[0]
else:
data[key] = values
return data
else:
# 其他类型:尝试作为文本记录
try: