Django 如何创建每月定期付款并写入 Credit 表
结论
建议把“生成月度付款记录”写成可重复执行的幂等业务函数,再由定时任务每月调用。生产环境一般使用 Celery 和 django-celery-beat;项目规模较小时,也可以使用 Django management command 配合系统 cron。
不要把定时逻辑放进视图、save() 方法或 Django 启动代码,否则可能因重复请求、多进程部署或服务重启而多次生成付款记录。数据库中也要增加唯一约束,保证同一救助对象每个月最多只有一条付款记录。
以下示例基于两个假设:
- “2500k”和“2000k”指的是
2500.00和2000.00。如果k表示“千”,金额应改为2500000.00和2000000.00。 - 年龄恰好为 50 岁时发放
2000.00。如果实际规则不同,调整判断条件即可。
调整 Credit 模型
DecimalField 必须设置 decimal_places。建议同时保存付款月份,并为“收款人 + 付款月份”添加唯一约束。
from django.db import models
from django.utils.translation import gettext_lazy as _
class Destitute(models.Model):
GENDER = (("male", "male"), ("female", "female"))
name = models.CharField(max_length=100)
acc_no = models.CharField(_("Bank Account"), max_length=50)
age = models.PositiveIntegerField(default=1)
sex = models.CharField(_("Gender"), max_length=64, choices=GENDER)
created_on = models.DateTimeField(_("Date Joined"), auto_now_add=True)
def __str__(self):
return self.name
class Credit(models.Model):
credit_title = models.CharField(_("Payment Title"), max_length=50)
payee = models.ForeignKey(
Destitute,
related_name="destitute_paid",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
total_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
)
payment_date = models.DateField()
payment_month = models.DateField(
help_text="使用当月第一天表示付款月份",
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["payee", "payment_month"],
name="unique_credit_per_payee_month",
),
]
def __str__(self):
return self.credit_title
修改模型后执行迁移:
python manage.py makemigrations
python manage.py migrate
如果数据库中已经有 Credit 数据,新增非空字段时需要先设置默认值。也可以分两次迁移:第一次允许 null=True,补齐历史数据后,再将字段改为非空。
编写月度付款函数
把业务逻辑放在普通服务函数中,例如 payments/services.py:
from datetime import date
from decimal import Decimal
from django.db import transaction
from .models import Credit, Destitute
AMOUNT_OVER_50 = Decimal("2500.00")
AMOUNT_50_OR_UNDER = Decimal("2000.00")
@transaction.atomic
def generate_monthly_credits(run_date=None):
run_date = run_date or date.today()
payment_month = run_date.replace(day=1)
created_count = 0
for destitute in Destitute.objects.iterator():
amount = (
AMOUNT_OVER_50
if destitute.age > 50
else AMOUNT_50_OR_UNDER
)
_, created = Credit.objects.get_or_create(
payee=destitute,
payment_month=payment_month,
defaults={
"credit_title": payment_month.strftime(
"Monthly Payment %Y-%m"
),
"total_amount": amount,
"payment_date": run_date,
},
)
if created:
created_count += 1
return created_count
这段代码通过 get_or_create() 和数据库唯一约束保证幂等性。即使任务因重试、误操作或多个 worker 同时运行而执行多次,同一个人也不会收到重复的当月付款记录。
iterator() 可以防止程序一次性把整张 Destitute 表加载到内存中。如果数据量很大,还可以使用批量查询和 bulk_create(ignore_conflicts=True)。对于普通规模的数据,get_or_create() 更容易理解,也更容易保证结果正确。
使用 Celery 定期执行
安装 Celery,并按照部署环境选择消息代理,通常可以使用 Redis 或 RabbitMQ:
pip install celery django-celery-beat
在 settings.py 中启用应用:
INSTALLED_APPS = [
# ...
"django_celery_beat",
]
接着迁移调度表:
python manage.py migrate django_celery_beat
定义 Celery 任务:
# payments/tasks.py
from celery import shared_task
from .services import generate_monthly_credits
@shared_task
def create_monthly_credit_records():
return generate_monthly_credits()
可以在 settings.py 中配置每月执行的任务:
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
"create-monthly-credit-records": {
"task": "payments.tasks.create_monthly_credit_records",
"schedule": crontab(
minute=0,
hour=2,
day_of_month="1",
),
},
}
这项配置会在每月 1 日凌晨 2 点执行任务。部署时还要启动 Celery worker 和 beat:
celery -A config worker --loglevel=INFO
celery -A config beat --loglevel=INFO
将 config 替换为包含项目 settings.py 的 Django 项目包名。如果需要通过 Django Admin 动态调整执行时间,可以在后台使用 django-celery-beat 的 PeriodicTask 配置,无需把调度时间写死在代码中。
小型项目的简单方案
如果不想引入 Celery,可以创建 management command,再由服务器上的 cron 每月运行。
# payments/management/commands/generate_monthly_credits.py
from django.core.management.base import BaseCommand
from payments.services import generate_monthly_credits
class Command(BaseCommand):
help = "Generate monthly credit records"
def handle(self, *args, **options):
created_count = generate_monthly_credits()
self.stdout.write(
self.style.SUCCESS(
f"Created {created_count} monthly credit records."
)
)
先手动验证命令:
python manage.py generate_monthly_credits
Linux cron 示例:
0 2 1 * * /path/to/venv/bin/python /path/to/project/manage.py generate_monthly_credits
cron 比较轻量,但没有 Celery 提供的任务重试、集中监控和数据库调度功能。如果项目已经使用 Redis、RabbitMQ 或其他后台任务,建议选择 Celery。若系统中只有一个简单的月度任务,management command 配合 cron 通常就够了。
注意事项
- 先确认年龄恰好为 50 岁时采用哪档付款标准。
- 金额要使用
Decimal,不要使用float。 - 如果年龄会随时间变化,长期保存
age并不可靠。更合适的做法是保存出生日期,例如date_of_birth,在执行付款时计算当月年龄。 payment_month应统一保存为当月第一天,防止同一个月出现多个不同日期。- 定时任务使用的时区应与
TIME_ZONE、Celery 时区和服务器时区一致。 - 银行提交和数据库记账最好使用不同状态,例如
pending、submitted、paid、failed。创建Credit记录不代表银行付款已经成功。 - 如果删除救助对象后仍要保留付款审计记录,使用
SET_NULL是合理的。不过,不同数据库对唯一约束中NULL的处理可能不同,因此历史记录最好不要继续参与新付款的生成逻辑。 - 上线前需要测试重复执行、任务中断后的重试、空数据表、年龄边界和金额上限等情况。
备注:内容仅供参考。
