очистить корзину покупок в приложении Django, которая сохраняется в сессии с помощью задачи Celery

Вопрос или проблема

Привет, я пытаюсь создать приложение электронной коммерции на Django. У меня есть класс корзины, который работает с сессией, это мой класс корзины:

from django.conf import settings
from shop.models import Product, ProductColor
from django.shortcuts import get_object_or_404
from django.utils import timezone

class Cart:
    def __init__(self, request_or_session_data):
        if hasattr(request_or_session_data, 'session'):  # Проверка, является ли это запросом
            self.session = request_or_session_data.session
        else:
            # Если это данные сессии, используйте их напрямую
            self.session = request_or_session_data
        cart = self.session.get(settings.CART_SESSION_ID)
        if not cart:
            cart = self.session[settings.CART_SESSION_ID] = {}
        self.cart = cart
        self.total_discount = None
        self.discount_code = None
        self.have_discount_code = self.session.get("have_discount_code", False)
        self.discount_code_amount = self.session.get("discount_code_amount", 0)
        self.last_updated = self.session.get("last_updated")
        if hasattr(request_or_session_data, 'user') and request_or_session_data.user.is_authenticated:
            self.user = request_or_session_data.user

    def add(self, product, color_id, quantity=1, override_quantity=False):
        product_id = str(product.id)
        color = get_object_or_404(ProductColor, id=color_id)
        cart_key = f"{product_id}_{color_id}"

        if cart_key not in self.cart:
            self.cart[cart_key] = {
                'color_id': str(color.id),
                'quantity': 0,
                'price': str(product.price),
                'discount': str(product.discount),
                'discount_price': str(product.get_discounted_price()),
            }

        if override_quantity:
            self.cart[cart_key]['quantity'] = quantity
        else:
            self.cart[cart_key]['quantity'] += quantity

        self.total_discount = self.get_total_discount()
        self.save()

    def mark_session_modified(self):
        # Отметить сессию как измененную, если `self.session` является объектом сессии Django
        if hasattr(self.session, 'modified'):
            self.session.modified = True

    def save(self):
        # отметьте сессию как «измененную», чтобы убедиться, что она будет сохранена
        self.last_updated = timezone.now()
        self.session[settings.CART_SESSION_ID] = self.cart
        self.session['have_discount_code'] = self.have_discount_code
        self.session['discount_code_amount'] = self.discount_code_amount
        self.session['last_updated'] = self.last_updated.isoformat()
        if hasattr(self, 'user') and self.user:
            self.session["user_id"] = self.user.id
        self.mark_session_modified()

    def remove(self, product, color_id=None):
        product_id = str(product.id)
        if color_id:
            cart_key = f"{product_id}_{color_id}"
            if cart_key in self.cart:
                del self.cart[cart_key]
        else:
            # Если не указан color_id, удалите все элементы, относящиеся к product_id
            keys_to_remove = [key for key in self.cart if key.startswith(product_id)]
            for key in keys_to_remove:
                del self.cart[key]

        if len(self.cart) == 0:
            self.clear()  # Вызовите метод clear, чтобы удалить коды скидок и другие данные
        else:
            self.total_discount = self.get_total_discount()  # Обновите общую скидку
            self.save()

    def __iter__(self):
        cart = self.cart.copy()
        product_ids = {key.split('_')[0] for key in cart.keys()}  # Извлекаем ID продуктов
        products = Product.objects.filter(id__in=product_ids)

        for product in products:
            for key in cart.keys():
                if key.startswith(str(product.id)):
                    item = cart[key]
                    item["product"] = product

                    # Извлекаем ID цвета из ключа
                    color_id = key.split('_')[1] if '_' in key else None

                    # Получаем соответствующий экземпляр ProductColor
                    if color_id:
                        try:
                            product_color = ProductColor.objects.get(id=color_id, product=product)
                            item["color_name"] = product_color.color  # Используйте цвет из ColorChoices
                        except ProductColor.DoesNotExist:
                            item["color_name"] = "Неизвестный цвет"
                    else:
                        item["color_name"] = "Без цвета"

                    item["color_id"] = str(color_id)
                    item["product_id"] = str(product.id)
                    item["price"] = int(item["price"])
                    item["discount_price"] = int(item["discount_price"])
                    if item["discount"] == "0":
                        item["total_price"] = item["price"] * item["quantity"]
                    else:
                        item["total_price"] = item["discount_price"] * item["quantity"]

                    yield item

    def __len__(self):
        return sum(item['quantity'] for item in self.cart.values())

    def get_total_price(self):
        total_price = 0
        for item in self:
            total_price += item["total_price"]
        if self.have_discount_code:
            discount_factor = self.discount_code_amount / 100
            total_price = int(total_price * (1 - discount_factor))
        return total_price

    def add_coupon(self, amount, code):
        self.discount_code_amount = amount
        self.have_discount_code = True
        self.discount_code = code
        self.save()

    def get_total_discount(self):
        total_discount = 0
        for item in self.cart.values():
            if item["discount"] != "0":
                discount = int(item["price"]) - int(item["discount_price"])
                total_discount += discount * item["quantity"]
        return total_discount

    @property
    def total_get_price_of_discount(self):
        total_price = 0
        for item in self:
            total_price += item["total_price"]
        price = self.get_total_price()

        return total_price - price

    def clear(self):
        # удалить корзину из сессии
        del self.session[settings.CART_SESSION_ID]
        self.have_discount_code = False
        self.discount_code = None
        self.discount_code_amount = 0
        self.total_discount = None
        self.save()

Каждый раз, когда пользователи добавляют продукт в корзину, я уменьшаю количество продукта, и у меня есть задача проверить, если их корзина пустует больше 30 минут, сначала отправить SMS пользователю и напомнить им о корзине, а затем через 1 час очистить корзину и вернуть количество обратно к продуктам. Проблема в том, что когда я пытаюсь в цикле пройти по всем сессиям и изменить эту сессию с помощью session_data[“sms_sent”] = True, после этого SMS будет отправлено снова. Также, когда пройдет больше 1 часа, оно добавит количество обратно в базу данных, но не очищает корзину из сессии.

Вот задача, которую я использую:

@shared_task
def clear_abandoned_carts():
    expired_time = timezone.now() - CART_EXPIRATION_TIME
    warning_time = timezone.now() - SEND_WARNING_TIME

    for session in Session.objects.filter(expire_date__gte=timezone.now()):
        data = session.get_decoded()
        last_updated_str = data.get("last_updated")
        cart = data.get(settings.CART_SESSION_ID)
        print("флаг sms_sent перед:", data.get("sms_sent"))  # Безопасный доступ

        if cart and last_updated_str:
            last_updated = timezone.datetime.fromisoformat(last_updated_str)

            if last_updated < expired_time:
                # Очистить корзину, если она истекла
                for key, item in cart.items():
                    color_id = item["color_id"]
                    quantity = item["quantity"]
                    try:
                        remove_from_cart(color_id, int(quantity))
                    except ProductColor.DoesNotExist:
                        pass  # Обработать по необходимости

                # Очистить данные сессии
                session_data = session.get_decoded()
                session_data.pop(settings.CART_SESSION_ID, None)
                session_data.pop("last_updated", None)
                session_data.pop("have_discount_code", None)
                session_data.pop("discount_code_amount", None)
                session_data.pop("sms_sent", None)
                session_data["modified"] = True

                session.save()
                cart_instance = Cart(data)
                cart_instance.clear()

            elif last_updated < warning_time:
                # Проверить, было ли SMS предупреждение уже отправлено
                if not data.get("sms_sent"):  # Продолжить только если SMS не было отправлено
                    user_id = data.get("user_id")
                    if user_id:
                        try:
                            user = CustomUser.objects.get(id=user_id)
                            if user.phone_number:
                                phone_number = user.phone_number
                                send_cart_warning(int(phone_number))  # Отправить SMS

                                # Отметить SMS как отправленное в данных сессии
                                session_data = session.get_decoded()
                                session_data["sms_sent"] = True  # Безопасно обновить сессию
                                session_data["modified"] = True
                                session.modified = True
                                session.save()

                                print("флаг sms_sent после:", session_data.get("sms_sent"))  # Безопасный доступ

                        except CustomUser.DoesNotExist:
                            pass

В консоли, когда отправляется сообщение, это print(“флаг sms_sent после:”, session_data.get(“sms_sent”)) дает мне True, но когда это снова запускается, этот print(“флаг sms_sent перед:”, data.get(“sms_sent”)) дает мне None. Можете ли вы помочь мне решить эту проблему, пожалуйста?

Ответ или решение

Чтобы справиться с вашей задачей в Django-приложении для электронной коммерции, давайте разберем, как эффективно очистить корзину покупок, используя Celery для выполнения фоновых задач. Мы также узнаем, как правильно обновить данные сессии после отправки SMS-уведомлений о заброшенных корзинах.

Основные проблемы и решение

  1. Обновление состояния сессии: Основной проблемой является то, что состояние сессии (sms_sent) не обновляется правильно. Вы изменяете его, но, возможно, неправильным образом извлекаете из сессии при последующих проверках.

  2. Чистка корзины: По истечении времени, если корзина считается неактивной, вам необходимо не только отправить уведомление, но и очистить корзину, возвращая количество товаров на склад.

Пример кода для задачи

Класс Cart

Ваш класс Cart уже хорошо спроектирован для управления корзиной в сессии. Убедитесь, что все изменения состояния, включая sms_sent, сохраняются корректно.

Фоновая задача clear_abandoned_carts

Вот отредактированная версия вашей фоновой задачи:

@shared_task
def clear_abandoned_carts():
    expired_time = timezone.now() - CART_EXPIRATION_TIME
    warning_time = timezone.now() - SEND_WARNING_TIME

    # Получаем активные сессии
    for session in Session.objects.filter(expire_date__gte=timezone.now()):
        data = session.get_decoded()
        last_updated_str = data.get("last_updated")
        cart = data.get(settings.CART_SESSION_ID)

        # Логгируем состояние sms_sent перед проверкой
        sms_sent_before = data.get("sms_sent")
        print("sms_sent flag before:", sms_sent_before)

        if cart and last_updated_str:
            last_updated = timezone.datetime.fromisoformat(last_updated_str)

            # Если корзина устарела
            if last_updated < expired_time:
                # Очистить корзину
                for key, item in cart.items():
                    color_id = item["color_id"]
                    quantity = item["quantity"]
                    try:
                        remove_from_cart(color_id, int(quantity))
                    except ProductColor.DoesNotExist:
                        pass  # Обработка исключений

                # Очистить сессионные данные
                session_data = session.get_decoded()
                session_data.pop(settings.CART_SESSION_ID, None)
                session_data.pop("last_updated", None)
                session_data.pop("have_discount_code", None)
                session_data.pop("discount_code_amount", None)
                session_data.pop("sms_sent", None)
                session.modified = True  # Помечаем сессию как измененную
                session.save()

                # Используя Cart, очищаем его
                cart_instance = Cart(data)
                cart_instance.clear()

            # Если корзина устарела, но не превышает время предупреждения
            elif last_updated < warning_time:
                if not sms_sent_before:  # Отправляем только если SMS еще не было отправлено
                    user_id = data.get("user_id")
                    if user_id:
                        try:
                            user = CustomUser.objects.get(id=user_id)
                            if user.phone_number:
                                phone_number = user.phone_number
                                send_cart_warning(int(phone_number))  # Отправка SMS

                                # Обновляем состояние SMS в данных сессии
                                session_data["sms_sent"] = True
                                session.modified = True  # Помечаем сессию как измененную
                                session.save()

                                print("sms_sent flag after:", session_data["sms_sent"])

                        except CustomUser.DoesNotExist:
                            pass

Ключевые моменты

  • Модификация сессии: Убедитесь, что вы правильно помечаете сессию как измененную (session.modified = True) перед сохранением. Это гарантирует, что все изменения сохранятся.

  • Логирование и проверка данных: Используйте логирование для отслеживания состояния флага sms_sent и других параметров.

  • Отмена SMS: Если SMS было отправлено, поместите соответствующую логику для этой проверки, чтобы избежать повторных сообщений.

Заключение

Данная реализация должна решить ваши проблемы с управлением сессиями и очисткой корзины в фоновом режиме. Убедитесь, что тестируете каждую часть на предмет правильного поведения как в случае успешной работы, так и в случае возникновения исключений. Хорошая практика также заключается в том, чтобы обрабатывать случаи, когда сессии могли быть неверно обновлены из-за условий гонки или других факторов.

Оцените материал
Добавить комментарий

Капча загружается...