PAYATHON 2026

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)

代码按以下顺序执行:

  1. 通过 Webhook(...) 创建模型实例。
  2. 把 POST 参数传给对应的模型字段。
  3. 调用实例的 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 并设置唯一约束,以实现幂等处理。

备注:内容仅供参考。