Django 如何将多个 URL 参数保存到 model
结论
save() 是模型实例的方法,不能用 Webhook.save() 创建对象。正确的做法是先实例化 Webhook,给字段赋值,再调用 save()。也可以直接使用 Webhook.objects.create() 创建并保存对象。
还要注意两个问题:
- webhook 通过 POST 请求发送数据时,通常应从
request.POST或request.body中读取,而不是使用request.GET。 - 赋值时必须使用模型实际定义的字段名。例如模型字段是
clientAccnum,就不能写成不存在的client_acc_num。
使用模型实例保存
如果 webhook 发送的是表单数据,例如 application/x-www-form-urlencoded 或 multipart/form-data,可以这样处理:
from django.http import HttpResponse
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from django.views.decorators.http import require_POST
from .models import Webhook
@require_POST
def webhook(request):
timestamp = request.POST.get("timestamp")
hook = Webhook(
clientAccnum=request.POST.get("clientAccnum", ""),
clientSubacc=request.POST.get("clientSubacc", ""),
eventType=request.POST.get("eventType", ""),
eventGroupType=request.POST.get("eventGroupType", ""),
subscriptionId=request.POST.get("subscriptionId", ""),
time_stamp=parse_datetime(timestamp) if timestamp else None,
time_stamp_local=timezone.now(),
)
hook.save()
return HttpResponse(status=204)
代码按以下顺序执行:
- 通过
Webhook(...)创建模型实例。 - 把 POST 参数传给对应的模型字段。
- 调用实例的
hook.save(),将数据保存到数据库。
也可以先创建一个空实例,再逐项赋值:
hook = Webhook()
hook.clientAccnum = request.POST.get("clientAccnum", "")
hook.clientSubacc = request.POST.get("clientSubacc", "")
hook.eventType = request.POST.get("eventType", "")
hook.eventGroupType = request.POST.get("eventGroupType", "")
hook.subscriptionId = request.POST.get("subscriptionId", "")
hook.time_stamp_local = timezone.now()
hook.save()
使用 objects.create() 简化代码
objects.create() 会创建实例并立即保存,不需要再调用 save():
Webhook.objects.create(
clientAccnum=request.POST.get("clientAccnum", ""),
clientSubacc=request.POST.get("clientSubacc", ""),
eventType=request.POST.get("eventType", ""),
eventGroupType=request.POST.get("eventGroupType", ""),
subscriptionId=request.POST.get("subscriptionId", ""),
time_stamp_local=timezone.now(),
)
原代码中的写法:
hook = Webhook.save()
会报错,是因为它直接在类上调用了实例方法,却没有传入对应的实例,也就是错误信息中缺少的 self。
如果 webhook 发送的是 JSON
很多 webhook 使用 application/json 作为 Content-Type。此时 request.POST 通常为空,需要解析 request.body:
import json
from django.http import JsonResponse
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from django.views.decorators.http import require_POST
from .models import Webhook
@require_POST
def webhook(request):
try:
payload = json.loads(request.body)
except (json.JSONDecodeError, UnicodeDecodeError):
return JsonResponse(
{"error": "Invalid JSON payload"},
status=400,
)
timestamp = payload.get("timestamp")
parsed_timestamp = parse_datetime(timestamp) if timestamp else None
if timestamp and parsed_timestamp is None:
return JsonResponse(
{"error": "Invalid timestamp"},
status=400,
)
hook = Webhook.objects.create(
clientAccnum=payload.get("clientAccnum", ""),
clientSubacc=payload.get("clientSubacc", ""),
eventType=payload.get("eventType", ""),
eventGroupType=payload.get("eventGroupType", ""),
subscriptionId=payload.get("subscriptionId", ""),
time_stamp=parsed_timestamp,
time_stamp_local=timezone.now(),
)
return JsonResponse({"id": hook.pk}, status=201)
应该使用 request.POST 还是解析 request.body,取决于 webhook 提供方发送的 Content-Type。
修正可为空的时间字段
当前模型的时间字段只设置了 blank=True:
time_stamp = models.DateTimeField(blank=True)
blank=True 主要用于表单验证,并不表示数据库可以保存 NULL。如果 webhook 的 timestamp 可能缺失,建议改成:
time_stamp = models.DateTimeField(blank=True, null=True)
time_stamp_local = models.DateTimeField(default=timezone.now)
完整示例如下:
from django.db import models
from django.utils import timezone
class Webhook(models.Model):
clientAccnum = models.CharField(max_length=120, blank=True)
clientSubacc = models.CharField(max_length=120, blank=True)
eventType = models.CharField(max_length=120, blank=True)
eventGroupType = models.CharField(max_length=120, blank=True)
subscriptionId = models.CharField(max_length=120, blank=True)
time_stamp = models.DateTimeField(blank=True, null=True)
time_stamp_local = models.DateTimeField(default=timezone.now)
修改模型后,需要创建并执行迁移:
python manage.py makemigrations
python manage.py migrate
字段名必须保持一致
模型定义的字段名是:
clientAccnum
clientSubacc
eventType
eventGroupType
subscriptionId
保存数据时也必须使用这些名称。原 view 中的以下属性并不是模型字段:
hook.client_acc_num
hook.client_sub_acc
hook.event_type
hook.event_group_type
hook.sub_id
Python 允许给对象临时添加普通属性,因此这些赋值不一定会立即报错,但 Django 不会将它们写入对应的数据库列。
按照 Django 的常见命名习惯,可以把模型字段统一改成蛇形命名:
class Webhook(models.Model):
client_acc_num = models.CharField(max_length=120, blank=True)
client_sub_acc = models.CharField(max_length=120, blank=True)
event_type = models.CharField(max_length=120, blank=True)
event_group_type = models.CharField(max_length=120, blank=True)
subscription_id = models.CharField(max_length=120, blank=True)
time_stamp = models.DateTimeField(blank=True, null=True)
time_stamp_local = models.DateTimeField(default=timezone.now)
外部参数名不必与模型字段名一致,只要在保存时明确映射即可:
Webhook.objects.create(
client_acc_num=payload.get("clientAccnum", ""),
client_sub_acc=payload.get("clientSubacc", ""),
event_type=payload.get("eventType", ""),
event_group_type=payload.get("eventGroupType", ""),
subscription_id=payload.get("subscriptionId", ""),
)
如果修改了现有字段名,也需要生成并执行数据库迁移。
其他注意事项
request.GET对应 URL 查询字符串。即使请求方法是 POST,也只有 URL 中的?key=value部分会进入request.GET。- 不要把时间字符串直接交给
DateTimeField。应先使用parse_datetime(),或者按照 webhook 文档规定的格式进行解析。 - webhook 通常不需要渲染 HTML 模板,返回
200、201或204响应即可,具体状态码应以服务提供方的重试规则为准。 - 外部 webhook 通常无法提供 Django 的 CSRF token,但不能只关闭 CSRF 校验而不做其他验证。应按照 webhook 提供方的规则校验签名、时间戳或共享密钥,防止伪造请求。
- 有些 webhook 会在请求超时后重复投递。如果 payload 中包含唯一事件 ID,建议保存该 ID 并设置唯一约束,以实现幂等处理。
备注:内容仅供参考。
