feat: 添加幂等性中间件 防止重复提交

This commit is contained in:
XIE7654
2025-10-16 20:21:55 +08:00
parent 42d5a3da21
commit 1756f549c9
6 changed files with 90 additions and 10 deletions

View File

@@ -1,6 +1,7 @@
from rest_framework import viewsets, status
from rest_framework.response import Response
# from utils.decorators import idempotent
from utils.export_mixin import ExportMixin
@@ -102,6 +103,7 @@ class CustomModelViewSet(viewsets.ModelViewSet, ExportMixin):
status=status.HTTP_200_OK
)
# @idempotent(timeout=10) # 幂等性装饰器,防止重复提交
def create(self, request, *args, **kwargs):
"""重写创建视图,支持批量创建"""
is_many = isinstance(request.data, list)

View File

@@ -0,0 +1,27 @@
from rest_framework.response import Response
from rest_framework import status
from utils.idempotency_helper import generate_idempotency_key, check_idempotency, get_user_identifier
def idempotent(timeout=10):
"""
幂等性装饰器用于单个DRF接口
:param timeout: 重复判断时间窗口(秒)
"""
def decorator(view_func):
def wrapper(self, request, *args, **kwargs):
user_id = get_user_identifier(request)
key = generate_idempotency_key(user_id, request.path, request.body)
if check_idempotency(key, timeout):
return Response(
{"error": "请勿重复提交"},
status=status.HTTP_409_CONFLICT
)
return view_func(self, request, *args, **kwargs)
return wrapper
return decorator

View File

@@ -0,0 +1,32 @@
import hashlib
from django.core.cache import cache
def generate_idempotency_key(user_id, path, body):
"""
生成幂等性检查的唯一标识key
:param user_id: 用户ID或"anonymous"
:param path: 请求路径
:param body: 请求体内容
:return: MD5哈希值作为唯一标识
"""
return hashlib.md5(f"{user_id}_{path}_{body}".encode()).hexdigest()
def check_idempotency(key, timeout=10):
"""
检查是否为重复请求
:param key: 幂等性标识key
:param timeout: 缓存超时时间(秒)
:return: True表示重复请求False表示首次请求
"""
if cache.get(key):
return True
cache.set(key, "processing", timeout)
return False
def get_user_identifier(request):
"""
获取用户标识符
:param request: HTTP请求对象
:return: 用户ID或"anonymous"
"""
return request.user.id if request.user.is_authenticated else "anonymous"

View File

@@ -1,43 +0,0 @@
import json
from django.http import JsonResponse
from django.utils.deprecation import MiddlewareMixin
from rest_framework import status
class DemoModeMiddleware(MiddlewareMixin):
"""
演示环境中间件
全局禁止修改和删除操作
"""
def process_request(self, request):
# 只处理 API 请求
if not request.path.startswith('/api/'):
return None
# 禁止的 HTTP 方法
forbidden_methods = ['POST', 'PUT', 'PATCH', 'DELETE']
if request.method in forbidden_methods:
# 检查是否是登录接口,登录接口允许 POST
if request.path.endswith('/login/') or request.path.endswith('/auth/login/'):
return None
# 检查是否是登出接口,登出接口允许 POST
if request.path.endswith('/logout/') or request.path.endswith('/auth/logout/'):
return None
# 其他修改/删除操作一律禁止
response_data = {
'code': 403,
'message': '演示环境禁止修改和删除操作',
'data': None
}
return JsonResponse(
response_data,
status=status.HTTP_403_FORBIDDEN,
content_type='application/json'
)
return None