Repository: Ehco1996/django-sspanel

Issue:
引入redis和分布式锁



Discussion:

Resolving patch:
diff --git a/apps/api/views.py b/apps/api/views.py
--- a/apps/api/views.py
+++ b/apps/api/views.py
@@ -338,6 +338,38 @@ def post(self, request):
         return JsonResponse(data)
 
 
+class OrderView(View):
+    @method_decorator(login_required)
+    def get(self, request):
+        user = request.user
+        order = UserOrder.get_and_check_recent_created_order(user)
+        if order and order.status != UserOrder.STATUS_CREATED:
+            info = {"title": "充值成功!", "subtitle": "请去商品界面购买商品！", "status": "success"}
+        else:
+            info = {"title": "支付查询失败!", "subtitle": "亲，确认支付了么？", "status": "error"}
+        return JsonResponse({"info": info})
+
+    @method_decorator(login_required)
+    def post(self, request):
+        try:
+            amount = int(request.POST.get("num"))
+            if amount < 1 or amount > 99999:
+                raise ValueError
+        except ValueError:
+            return JsonResponse(
+                {"info": {"title": "失败", "subtitle": "请保证金额正确", "status": "error"}},
+            )
+        order = UserOrder.get_or_create_order(request.user, amount)
+        info = {
+            "title": "请求成功！",
+            "subtitle": "支付宝扫描下方二维码付款，付款完成记得按确认哟！",
+            "status": "success",
+        }
+        return JsonResponse(
+            {"info": info, "qrcode_url": order.qrcode_url, "order_id": order.id}
+        )
+
+
 @login_required
 def gen_invite_code(request):
     """
@@ -393,35 +425,3 @@ def ailpay_callback(request):
         return HttpResponse("success")
     else:
         return HttpResponse("failure")
-
-
-class OrderView(View):
-    @method_decorator(login_required)
-    def get(self, request):
-        user = request.user
-        order = UserOrder.get_and_check_recent_created_order(user)
-        if order and order.status != UserOrder.STATUS_CREATED:
-            info = {"title": "充值成功!", "subtitle": "请去商品界面购买商品！", "status": "success"}
-        else:
-            info = {"title": "支付查询失败!", "subtitle": "亲，确认支付了么？", "status": "error"}
-        return JsonResponse({"info": info})
-
-    @method_decorator(login_required)
-    def post(self, request):
-        try:
-            amount = int(request.POST.get("num"))
-            if amount < 1 or amount > 99999:
-                raise ValueError
-        except ValueError:
-            return JsonResponse(
-                {"info": {"title": "失败", "subtitle": "请保证金额正确", "status": "error"}},
-            )
-        order = UserOrder.get_or_create_order(request.user, amount)
-        info = {
-            "title": "请求成功！",
-            "subtitle": "支付宝扫描下方二维码付款，付款完成记得按确认哟！",
-            "status": "success",
-        }
-        return JsonResponse(
-            {"info": info, "qrcode_url": order.qrcode_url, "order_id": order.id}
-        )
diff --git a/apps/extensions/lock.py b/apps/extensions/lock.py
--- a/apps/extensions/lock.py
+++ b/apps/extensions/lock.py
@@ -41,6 +41,10 @@ class LockManager:
     def __init__(self, redis_client) -> None:
         self._redis_client = redis_client
 
-    def order_lock(self, order_number):
+    def order_lock(self, order_number: str):
         key = f"lock.order_lock.{order_number}"
         return GlobalLock(key, self._redis_client, blocking=False)
+
+    def user_create_order_lock(self, user_id: int):
+        key = f"lock.user_create_order_lock.{user_id}"
+        return GlobalLock(key, self._redis_client, blocking=False)
diff --git a/apps/sspanel/models.py b/apps/sspanel/models.py
--- a/apps/sspanel/models.py
+++ b/apps/sspanel/models.py
@@ -23,7 +23,7 @@
 from django.utils import functional, timezone
 
 from apps import constants as c
-from apps.ext import cache, encoder, pay
+from apps.ext import cache, encoder, pay, lock
 from apps.utils import (
     get_current_datetime,
     get_long_random_string,
@@ -368,24 +368,64 @@ def get_not_paid_order_by_amount(cls, user, amount):
             .first()
         )
 
+    @classmethod
+    def get_or_create_order(cls, user, amount):
+        # NOTE 目前这里只支持支付宝 所以暂时写死
+        with lock.user_create_order_lock(user.id):
+            now = get_current_datetime()
+            order = cls.get_not_paid_order_by_amount(user, amount)
+            if order and order.expired_at > now:
+                return order
+            with transaction.atomic():
+                out_trade_no = cls.gen_out_trade_no()
+                trade = pay.alipay.api_alipay_trade_precreate(
+                    out_trade_no=out_trade_no,
+                    total_amount=amount,
+                    subject=settings.ALIPAY_TRADE_INFO.format(amount),
+                    timeout_express=cls.DEFAULT_ORDER_TIME_OUT,
+                    notify_url=cls.ALIPAY_CALLBACK_URL,
+                )
+                qrcode_url = trade.get("qr_code")
+                order = cls.objects.create(
+                    user=user,
+                    status=cls.STATUS_CREATED,
+                    out_trade_no=out_trade_no,
+                    amount=amount,
+                    qrcode_url=qrcode_url,
+                    expired_at=now.add(minutes=10),
+                )
+                return order
+
     @classmethod
     def get_and_check_recent_created_order(cls, user):
-        with transaction.atomic():
-            order = (
-                cls.objects.select_for_update()
-                .filter(user=user)
-                .order_by("-created_at")
-                .first()
-            )
-            order and order.check_order_status()
-            return order
+        order = (
+            cls.objects.select_for_update()
+            .filter(user=user)
+            .order_by("-created_at")
+            .first()
+        )
+        if order is None:
+            return
+        with lock.order_lock(order.out_trade_no):
+            with transaction.atomic():
+                order.check_order_status()
+        return order
+
+    @classmethod
+    def make_up_lost_orders(cls):
+        now = get_current_datetime()
+        for order in cls.objects.filter(status=cls.STATUS_CREATED, expired_at__gte=now):
+            with lock.order_lock(order.out_trade_no):
+                changed = order.check_order_status()
+                if changed:
+                    print(f"补单：{order.user}={order.amount}")
 
     @classmethod
     def handle_callback_by_alipay(cls, data):
-        with transaction.atomic():
-            order = UserOrder.objects.select_for_update().get(
-                out_trade_no=data["out_trade_no"]
-            )
+        order = UserOrder.objects.select_for_update().get(
+            out_trade_no=data["out_trade_no"]
+        )
+        with lock.order_lock(order.out_trade_no):
             if order.status != order.STATUS_CREATED:
                 return True
             signature = data.pop("sign")
@@ -394,48 +434,13 @@ def handle_callback_by_alipay(cls, data):
                 "TRADE_SUCCESS",
                 "TRADE_FINISHED",
             )
-            if success:
-                order.status = order.STATUS_PAID
-                order.save()
-            order.handle_paid()
+            with transaction.atomic():
+                if success:
+                    order.status = order.STATUS_PAID
+                    order.save()
+                order.handle_paid()
             return success
 
-    @classmethod
-    def make_up_lost_orders(cls):
-        now = get_current_datetime()
-        for order in cls.objects.filter(status=cls.STATUS_CREATED, expired_at__gte=now):
-            changed = order.check_order_status()
-            if changed:
-                print(f"补单：{order.user}={order.amount}")
-
-    @classmethod
-    def get_or_create_order(cls, user, amount):
-        # NOTE 目前这里只支持支付宝 所以暂时写死
-        # TODO 缺少一把分布式锁 目前还不想引入其他外部组件所以暂时忽略
-        now = get_current_datetime()
-        order = cls.get_not_paid_order_by_amount(user, amount)
-        if order and order.expired_at > now:
-            return order
-        with transaction.atomic():
-            out_trade_no = cls.gen_out_trade_no()
-            trade = pay.alipay.api_alipay_trade_precreate(
-                out_trade_no=out_trade_no,
-                total_amount=amount,
-                subject=settings.ALIPAY_TRADE_INFO.format(amount),
-                timeout_express=cls.DEFAULT_ORDER_TIME_OUT,
-                notify_url=cls.ALIPAY_CALLBACK_URL,
-            )
-            qrcode_url = trade.get("qr_code")
-            order = cls.objects.create(
-                user=user,
-                status=cls.STATUS_CREATED,
-                out_trade_no=out_trade_no,
-                amount=amount,
-                qrcode_url=qrcode_url,
-                expired_at=now.add(minutes=10),
-            )
-            return order
-
     def handle_paid(self):
         # NOTE Must use in transaction
         if self.status != self.STATUS_PAID:


Test patch: