Skip to content
返回

拒绝手动改数!Django + Celery 打造“无人值守”财务自动续期功能

发布日期:

💡 背景:财务自由的第一步,是让系统自动化

作为一个“记账狂魔”,我开发了自己的个人财务管理应用。但随之而来的痛点是:定期交易(房租、订阅、房贷)每次都要手动更新日期。、

这不仅导致数据统计滞后,更违背了程序员“懒惰”的美德。于是,我利用 Django + Celery Beat 构建了一套自动化引擎,每晚准时巡检,自动完成账单续期并邮件提醒。

常见的 Django 定时任务方案对比

方案优点缺点适用场景
Django Celery Beat最强大。支持分布式,有精美的监控后台,功能极全。需要安装 Redis/RabbitMQ,配置较复杂。生产环境、高并发、任务量大且复杂的场景。
Django APScheduler轻量级。直接集成在 Django 中,无需额外中间件。任务多时会增加主进程负担,不支持分布式环境。中小型项目,只需简单的定时触发。
Django Management Command + Crontab最原始/稳定。利用 Linux 自带的 cron 执行。不易在代码层面管理,无法跨平台(Windows 较难)。传统的运维管理方式,任务相对单一。

🏗️环境配置

📅安装配置 Celery Beat

想要让 Django 自动动起来,我们需要安装“闹钟”插件:

# 安装 Celery 核心及数据库调度插件
pip install celery django-celery-beat

小贴士: django-celery-beat 相比原生的 Celery Beat 更好用,因为它允许你直接在 Django Admin 后台动态修改任务时间,而不需要重启服务器。

🧠核心逻辑:编写 Celery Task

我在应用目录下创建了tasks.py。为了保证金融数据的严谨性,加入了 dateutil 处理复杂的月份逻辑,并集成邮件通知,让系统执行情况尽在掌握。

from celery import shared_task
from django.core.mail import send_mail, EmailMessage
from django.utils import timezone
from bill.models import Transaction, RecurringTransaction
import datetime
import os
from dateutil.relativedelta import relativedelta

@shared_task
def update_transactions():
    """
    自动化巡检:更新所有未完成的定期交易日期
    """
    now = timezone.now().date()  # 获取当前日期
    # 筛选:非已完成(status=2) 且 已到达交易日期
    transactions = RecurringTransaction.objects.exclude(status=2).filter(
        next_occurrence__lte=now
    )
    if not transactions.exists():
        return "✨ 扫描完成:暂无待更新交易。"
    
    updated_transaction_names = []  # 用于存储更新的交易名称

    for transaction in transactions:
        # 情况A:未设结束日期 或 尚未到达生命周期终点
        if transaction.end_date is None or transaction.next_occurrence < transaction.end_date:
            # 灵活处理多种交易频率
            if transaction.frequency == 'daily':
                transaction.next_occurrence += timezone.timedelta(days=1)
            elif transaction.frequency == 'weekly':
                transaction.next_occurrence += timezone.timedelta(weeks=1)
            elif transaction.frequency == 'monthly':
                transaction.next_occurrence = transaction.next_occurrence + relativedelta(months=1)
            elif transaction.frequency == 'quarterly':
                transaction.next_occurrence = transaction.next_occurrence + relativedelta(months=3)
            elif transaction.frequency == 'yearly':
                transaction.next_occurrence = transaction.next_occurrence + relativedelta(years=1)
            
            transaction.save()  # 保存更新后的交易
            updated_transaction_names.append(transaction.name)  # 收集更新后的交易名称
        # 情况B:合约到期,优雅告别    
        elif transaction.next_occurrence >= now and transaction.end_date is not None and transaction.next_occurrence > transaction.end_date:
            transaction.status = 2  # 更新状态为已完成
            transaction.save()
            updated_transaction_names.append(transaction.name)  # 收集更新后的交易名称
    updated_transaction_names_str = ', '.join(updated_transaction_names)

    if updated_transaction_names:
        send_mail(
            '💰 财务系统:定期交易数据已自动更新',
            f'亲爱的开发者,系统已完成今日巡检,更新如下:{updated_transaction_names_str}',
            'your_mail@qq.com',  # 发件人
            ['your_mail@qq.com'],  # 收件人
            fail_silently=False,
        )

    return f"成功处理 {len(updated_transaction_names)} 条记录"

⚙️ 自动化配置:让“闹钟”响起来

1. 项目心脏:celery.py

在项目主目录下新建配置文件,让 Celery 能够自动发现各个 App 下的 tasks.py。

# celery.py
import os
from celery import Celery

# 设置默认的 Django 设置模块
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings')
# 创建 Celery 应用
app = Celery('myapp')
# 从 Django 的 settings.py 中加载配置
app.config_from_object('django.conf:settings', namespace='CELERY')
# 自动发现 Django 应用中的任务
app.autodiscover_tasks()

2. 调度清单:settings.py

集成 Redis 作为消息代理(Broker),并设定在每晚 0:00 准时执行。

# Celery配置
CELERY_BROKER_URL = 'redis://:password@ip:port/0'  # Broker配置
CELERY_RESULT_BACKEND = 'django-db'
CELERY_CACHE_BACKEND = 'django-cache'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_RESULT_EXPIRES = 259200  # 3天 秒
CELERY_RESULT_EXTENDED = True
CELERY_TASK_TRACK_STARTED = True
CELERY_ENABLE_UTC = False
# 设置定时任务
CELERY_BEAT_SCHEDULE = {
    'update-transactions-every-midnight': {
        'task': 'bill.tasks.update_transactions',
        'schedule': crontab(hour=0, minute=0),  # 每天午夜12点执行
        # 'schedule': crontab(hour=23, minute=59, day_of_week='0'),  # 每周日执行
    }
}

任务

⚠️ 避坑那些事儿

在实现过程中,我总结了 3 个非常重要的经验,分享给准备动手的小伙伴:

  1. 时区陷阱 (Timezone): 如果任务在清晨 8 点才跑,多半是 CELERY_TIMEZONE 没设对。建议将 CELERY_ENABLE_UTC 设为 False 以对齐本地时间。

  2. relativedelta 的必要性: 不要用 timedelta(days=30) 来处理月份!2 月份会让你痛不欲生。dateutil.relativedelta 能完美处理大小月和闰年。

  3. Worker 与 Beat 必须同在: 很多同学只启动了 celery worker,却忘了启动 celery beat。没有 Beat 这个“指挥官”,Worker 是永远等不到任务的。

🏁 结语

通过这套方案,我的财务应用彻底实现了 “感知日期并自动进化”。数据统计再也没有断档,每个月生成的财务报表也更加精准。

如果你也有重复性的数据维护工作,不妨试试 Celery,把时间留给更有意义的代码!


分享此文章:

下一篇
College Admissions for Adults