Как я могу запустить автоматические действия, когда поле модели (например, дата) достигает определенного времени, например, 5 дней, еженедельно или ежегодно?

Я работаю над приложением на базе Django, в котором разработчики могут подключать пользователей к своим приложениям, используя номер учетной записи. После подключения создается модель Subscription с такими полями, как user, plan, is_active, и next_billing_date. next_billing_date зависит от типа тарифного плана (еженедельный, ежемесячный, годовой и т.д.).

Я хочу автоматически выполнить некоторые действия (например, отключить is_active или снова зарядить) при достижении next_billing_date. Это включает в себя:

  1. Деактивация просроченных подписок
  2. Или инициирование периодического выставления счетов в зависимости от цикла подписки (например, 1 января 2025 г. → 1 января 2026 г. для годовых или еженедельных платежей для других)

Я уже сохраняю следующую дату выставления счета следующим образом:

next_billing_date = timezone.now().date() + timedelta(days=30)  # For monthly plans

Я пытался использовать сигналы для обработки таких вещей, как автоматическое списание средств с пользователей через 7 дней или прекращение подписки через 1 месяц. Но я понял, что сигналы Django работают только тогда, когда модель сохраняется, обновляется или удаляется, а не по прошествии времени.

Это означает, что если я хочу, чтобы что-то произошло ровно через 7 дней после подписки пользователя, сигнал не поможет, если только что-то другое не вызовет его (например, повторное сохранение модели). Таким образом, даже если срок действия подписки подходит к концу, Django ничего не будет делать автоматически, пока какой-либо код не обновит этот объект.

Как лучше всего реализовать такую логику регулярного выставления счетов в Django? Мы будем признательны за любые предложения, особенно по триггерам, основанным на времени, в приложениях Django.

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

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

Но это не очень хорошая идея: если за это время вы перезапустите сервер, процесс завершится, как и все остальные задействованные элементы.

Вы могли бы использовать Celery [Celery-docs], который может запускать запланированные задачи и работать с постоянная база данных для хранения задач, и это могло бы сработать, но для настройки требуются определенные усилия, и, что, вероятно, более важно, хотя они определенно проделали хорошую работу, сделав ее надежной, она все еще не является полностью надежной.

Обычно вы стараетесь сделать приложение как можно более "пассивным". Это означает, что вы не завершаете подписку, вы просто определяете, активна ли подписка, когда она вам нужна.

Например:

from django.utils import timezone


class Subscription(models.Model):
    end_date = models.DateTimeField()

    @property
    def is_active(self):
        return self.end_date < timezone.now()

Вы также можете отфильтровать Subscription, чтобы проверить, есть ли у пользователя активная подписка на:

from django.db.models.functions import Now

user_with_active_subscription = User.objects.filter(subscription__lte=Now())

то же самое, когда вы хотите сгенерировать счета: вы позволяете команде management [Django-doc] периодически выполняться, например, с помощью cron [wiki], который генерирует счета и, возможно, заранее устанавливает временные метки активных подписок для следующей задачи выставления счетов. Как правило, вы пытаетесь сделать такие задачи надежными, поэтому вы не только генерируете счета за товары, дата выставления которых совпадает с текущей датой, но и ищете активные подписки с датой выставления счета в прошлом, так что, если скрипт по какой-то причине не запускался день или два, он все равно будет запущен. вы можете справиться с этой проблемой.

Я написал небольшую статью [django-антипаттерны] об этом несколько недель назад.

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