From 1756f549c9c90c8bf0e65ff0decc9eb7fedac249 Mon Sep 17 00:00:00 2001 From: XIE7654 <765462425@qq.com> Date: Thu, 16 Oct 2025 20:21:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=B9=82=E7=AD=89?= =?UTF-8?q?=E6=80=A7=E4=B8=AD=E9=97=B4=E4=BB=B6=20=E9=98=B2=E6=AD=A2?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/backend/settings.py | 3 +- .../DemoModeMiddleware.py} | 18 +++++------ backend/middleware/IdempotencyMiddleware.py | 18 +++++++++++ backend/utils/custom_model_viewSet.py | 2 ++ backend/utils/decorators.py | 27 ++++++++++++++++ backend/utils/idempotency_helper.py | 32 +++++++++++++++++++ 6 files changed, 90 insertions(+), 10 deletions(-) rename backend/{utils/middleware.py => middleware/DemoModeMiddleware.py} (90%) create mode 100644 backend/middleware/IdempotencyMiddleware.py create mode 100644 backend/utils/decorators.py create mode 100644 backend/utils/idempotency_helper.py diff --git a/backend/backend/settings.py b/backend/backend/settings.py index cf3acb5..d2c8497 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -67,11 +67,12 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'middleware.IdempotencyMiddleware.IdempotencyMiddleware', # 幂等性中间件 防止重复提交 ] # 演示环境中间件 - 全局禁止修改和删除操作 if DEMO_MODE: - MIDDLEWARE.append('utils.middleware.DemoModeMiddleware') + MIDDLEWARE.append('middleware.DemoModeMiddleware.DemoModeMiddleware') AUTH_USER_MODEL = 'system.User' ROOT_URLCONF = 'backend.urls' diff --git a/backend/utils/middleware.py b/backend/middleware/DemoModeMiddleware.py similarity index 90% rename from backend/utils/middleware.py rename to backend/middleware/DemoModeMiddleware.py index f620b88..a7b48bc 100644 --- a/backend/utils/middleware.py +++ b/backend/middleware/DemoModeMiddleware.py @@ -9,35 +9,35 @@ 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, + response_data, status=status.HTTP_403_FORBIDDEN, content_type='application/json' ) - - return None \ No newline at end of file + + return None \ No newline at end of file diff --git a/backend/middleware/IdempotencyMiddleware.py b/backend/middleware/IdempotencyMiddleware.py new file mode 100644 index 0000000..5d7cb82 --- /dev/null +++ b/backend/middleware/IdempotencyMiddleware.py @@ -0,0 +1,18 @@ +from django.http import JsonResponse +from utils.idempotency_helper import generate_idempotency_key, check_idempotency, get_user_identifier + + +class IdempotencyMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.method == "POST": + user_id = get_user_identifier(request) + key = generate_idempotency_key(user_id, request.path, request.body) + + if check_idempotency(key, 10): + return JsonResponse({"error": "请勿重复提交"}, status=409) + + response = self.get_response(request) + return response diff --git a/backend/utils/custom_model_viewSet.py b/backend/utils/custom_model_viewSet.py index 024b0de..4229499 100644 --- a/backend/utils/custom_model_viewSet.py +++ b/backend/utils/custom_model_viewSet.py @@ -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) diff --git a/backend/utils/decorators.py b/backend/utils/decorators.py new file mode 100644 index 0000000..99ba757 --- /dev/null +++ b/backend/utils/decorators.py @@ -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 diff --git a/backend/utils/idempotency_helper.py b/backend/utils/idempotency_helper.py new file mode 100644 index 0000000..5e24f15 --- /dev/null +++ b/backend/utils/idempotency_helper.py @@ -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"