Вопрос или проблема
Привет, я пытаюсь создать приложение электронной коммерции на 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-уведомлений о заброшенных корзинах.
Основные проблемы и решение
-
Обновление состояния сессии: Основной проблемой является то, что состояние сессии (
sms_sent
) не обновляется правильно. Вы изменяете его, но, возможно, неправильным образом извлекаете из сессии при последующих проверках. -
Чистка корзины: По истечении времени, если корзина считается неактивной, вам необходимо не только отправить уведомление, но и очистить корзину, возвращая количество товаров на склад.
Пример кода для задачи
Класс 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 было отправлено, поместите соответствующую логику для этой проверки, чтобы избежать повторных сообщений.
Заключение
Данная реализация должна решить ваши проблемы с управлением сессиями и очисткой корзины в фоновом режиме. Убедитесь, что тестируете каждую часть на предмет правильного поведения как в случае успешной работы, так и в случае возникновения исключений. Хорошая практика также заключается в том, чтобы обрабатывать случаи, когда сессии могли быть неверно обновлены из-за условий гонки или других факторов.