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

У меня есть приложение, созданное на 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 и обновляются статусы. Сейчас это не является большой проблемой, поскольку мы только начинаем, но когда в таблице будут миллионы скидок, такой подход станет неоптимальным, поскольку будут обновляться все скидки, принадлежащие определенному бизнесу.

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

I think it is better not to make the field time-sensitive. Just determine the status when you need it, like:

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'

We thus don't change the status, or store it, we just determine it when needed. This also means that if the end_date is extended, or shortened, the filtering automatically will include/exclude the Discount again with the Discount.active.all() items.

Because we add database indexes, it will normally retrieve the Discounts efficiently, but it is also more robust, since we don't need to actively change statusses.

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