Как обновить статус на истекший или активный в зависимости от даты и времени?

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

Для того чтобы предприятия могли прикреплять скидки к своим товарам, я создал Discount модель:

class Discount(CommonField):
    name = models.CharField(max_length=255, blank=True, null=True)
    discount = models.DecimalField(max_digits=15, decimal_places=2)
    discount_type = models.CharField(max_length=255, choices=DISCOUNT_TYPE_CHOICES, blank=True, null=True)

    discounted_products_count = models.PositiveSmallIntegerField(default=0)

    start_date = models.DateTimeField(blank=True, null=True)
    expiry_date = models.DateTimeField(blank=True, null=True)

    status = models.CharField(max_length=255, default="inactive", choices=DISCOUNT_STATUS)

    objects = DiscountModelManager()

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

Одна из проблем, с которой я столкнулся, заключалась в том, в какой момент мне следует обновлять статус скидок. Чтобы решить эту проблему, я создал Model Manager, чтобы иметь центральное место, где размещается логика обновления статуса;

class DiscountModelManager(TenantAwareManager):
    def get_queryset(self):
        queryset = super().get_queryset()
        self.change_promo_code_if_end_date_extended(queryset)
        self.activate_discount_if_start_date_reached(queryset)
        self.expire_discount_if_expiry_date_reached(queryset)
        return super().get_queryset()
        

    def change_promo_code_if_end_date_extended(self, queryset):
        """
        Activates promo codes if expiry_date has been extended and the status is expired.
        """
        queryset.filter(expiry_date__gte=timezone.now(), status="expired").update(status="active")

    def activate_discount_if_start_date_reached(self, queryset):
        """
        Activates promo codes if start_date has been reached and the status is inactive.
        """
        queryset.filter(start_date__lte=timezone.now(), status="inactive").update(status="active")

    def expire_discount_if_expiry_date_reached(self, queryset):
        queryset.filter(expiry_date__lte=timezone.now()).update(status="expired")

На данный момент у меня есть четыре вида, которые занимаются скидками:

  1. Dicount List View: Где перечислены все скидки, относящиеся к данному конкретному бизнесу ( с пагинацией).
  2. Detail Discount View: Где отображается детальный вид скидки.
  3. Edit Discount View: Где пользователь может редактировать скидку после просмотра ее в детальном представлении
  4. .
  5. Point of Sale View: Где происходит продажа и скидка может быть использована во время оформления заказа
  6. .

Код работает отлично, за исключением того, что у меня есть еще одна проблема...

Если мы посмотрим на представление списка скидок:

class DiscountListView(LoginRequiredMixin, View):
    def get(self, request):
        business_id = current_business_id()
        discounts = Discount.objects.filter(business__business_id=business_id)

        paginator = Paginator(discounts, 25)
        page_number = request.GET.get("page")
        page = paginator.get_page(page_number)

        context = {
            "table_headers": HTMLTemplateTags().table_headers["discounts"],
            "page": page,
            "search_query": search_query,
            "paginator": paginator,
        }

        return render(request, "pages/sales/discounts/discount_list.html", context)

Вы видите, что у нас есть queryset, где мы получаем все скидки, относящиеся к текущему бизнесу

discounts = Discount.objects.filter(business__business_id=business_id)

Перед тем как этот набор запросов будет завершен, вызывается DiscountModelManager и обновляются статусы. Сейчас это не является большой проблемой, поскольку мы только начинаем, но когда в таблице будут миллионы скидок, такой подход станет неоптимальным, поскольку будут обновляться все скидки, принадлежащие определенному бизнесу.

Есть ли лучший и более эффективный способ решения этой задачи?

Я думаю, что лучше не делать поле чувствительным ко времени. Просто определяйте статус, когда он вам нужен, например:

from django.db.models import Q
from django.db.models.functions import Now


class ActiveDiscountManager(models.Manager):
    def get_queryset(self, *args, **kwargs):
        return (
            super()
            .get_queryset(*args, **kwargs)
            .filter(~Q(expiry_date__gte=Now()), start_date__gte=Now())
        )


class Discount(CommonField):
    start_date = models.DateTimeField(blank=True, null=True, db_index=True)
    expiry_date = models.DateTimeField(blank=True, null=True, db_index=True)
    # no status field

    objects = models.Manager()
    active = ActiveDiscountManager()

    @property
    def status(self):
        if self.start_date is None or self.start_date < datetime.now():
            return 'inactive'
        elif self.end_date and self.end_date < datetime.now():
            return 'expired'
        else:
            return 'active'

Таким образом, мы не меняем статус и не храним его, а просто определяем его при необходимости. Это также означает, что если end_date будет расширен или сокращен, то фильтрация автоматически включит/исключит Discount снова вместе с элементами Discount.active.all().

Поскольку мы добавляем индексы базы данных, это обычно эффективно извлекает Discount, но это также более надежно, поскольку нам не нужно активно менять статусы.

Вернуться на верх