Django 根据 webhook 参数更新扩展用户 Profile 字段
结论
先根据 clientAccnum 找到对应的 User,再通过一对一关系获取该用户的 Profile,然后更新模型中实际定义的 account_level 字段。
Profile.account_paid.update(True) 不能完成更新,原因有两个:
- 当前
Profile模型中没有account_paid字段,实际字段名是account_level。 update()属于QuerySet,不能直接对模型字段调用。
如果 clientAccnum 保存的是 Django 用户主键,可以直接更新:
Profile.objects.filter(user_id=client_accnum).update(
account_level=True
)
在 webhook 中,还要处理用户或 Profile 不存在、通知重复以及事件类型无关等情况。
基本实现
下面的示例基于这些前提:
clientAccnum的值是 DjangoUser.id;RenewalSuccess表示付费状态;- 只有明确的付款成功或失效事件才会修改账户状态。
from django.contrib.auth.models import User
from django.db import transaction
from django.http import HttpResponse, HttpResponseBadRequest
from django.utils import timezone
from django.views.decorators.http import require_POST
from .models import Profile, Webhook
@require_POST
def webhook(request):
client_accnum = request.POST.get("clientAccnum")
event_type = request.POST.get("eventType")
if not client_accnum or not event_type:
return HttpResponseBadRequest("Missing required parameters")
try:
user_id = int(client_accnum)
except (TypeError, ValueError):
return HttpResponseBadRequest("Invalid clientAccnum")
try:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
return HttpResponseBadRequest("Unknown clientAccnum")
with transaction.atomic():
Webhook.objects.create(
user=client_accnum,
clientSubacc=request.POST.get("clientSubacc"),
eventType=event_type,
eventGroupType=request.POST.get("eventGroupType"),
subscriptionId=request.POST.get("subscriptionId"),
timestamp=request.POST.get("timestamp"),
timestamplocal=timezone.now(),
)
profile, _ = Profile.objects.get_or_create(user=user)
if event_type == "RenewalSuccess":
profile.account_level = True
profile.save(update_fields=["account_level"])
elif event_type in {"SubscriptionCancelled", "SubscriptionExpired"}:
profile.account_level = False
profile.save(update_fields=["account_level"])
return HttpResponse(status=200)
使用 get_or_create() 后,即使用户还没有 Profile,也不会直接抛出 Profile.DoesNotExist。如果项目已经通过信号保证每个用户都有 Profile,也可以这样写:
profile = user.profile
profile.account_level = event_type == "RenewalSuccess"
profile.save(update_fields=["account_level"])
使用 QuerySet.update() 简化更新
如果能确定 Profile 已经存在,而且不需要触发模型的 save() 方法或相关信号,可以直接更新数据库:
updated_count = Profile.objects.filter(
user_id=user_id
).update(
account_level=True
)
updated_count 表示受影响的记录数。由于 Profile.user 是 OneToOneField,正常情况下只会返回 0 或 1:
if updated_count == 0:
return HttpResponseBadRequest("Profile not found")
也可以先根据事件计算账户状态:
is_paid = event_type == "RenewalSuccess"
Profile.objects.filter(user_id=user_id).update(
account_level=is_paid
)
这样写会把所有非 RenewalSuccess 事件都当作未付款。计费系统还可能发送测试、退款处理中或订阅变更等事件,所以更稳妥的方式是只处理明确支持的事件:
event_status = {
"RenewalSuccess": True,
"SubscriptionCancelled": False,
"SubscriptionExpired": False,
}
if event_type in event_status:
Profile.objects.filter(user_id=user_id).update(
account_level=event_status[event_type]
)
如果 clientAccnum 不是 Django 用户 ID
不能根据字段名称直接认定 clientAccnum 就是 User.id。如果它是计费平台生成的客户编号,应在本地保存一份明确的映射,并为该字段设置唯一约束:
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
billing_account_number = models.CharField(
max_length=100,
unique=True,
null=True,
blank=True,
)
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
account_level = models.BooleanField(default=False)
收到 webhook 后,通过外部编号查询 Profile:
try:
profile = Profile.objects.get(
billing_account_number=client_accnum
)
except Profile.DoesNotExist:
return HttpResponseBadRequest("Unknown clientAccnum")
profile.account_level = event_type == "RenewalSuccess"
profile.save(update_fields=["account_level"])
这种做法通常比直接把 Django 主键交给第三方更清楚,也能避免外部账户编号与本地用户编号的含义不一致。
统一字段名称
问题描述使用了 account_paid,模型中实际定义的是:
account_level = models.BooleanField(default=False)
代码必须使用模型中真实存在的字段名。如果字段的含义是“是否已经付款”,也可以把模型字段改成:
account_paid = models.BooleanField(default=False)
修改模型后,需要生成并执行数据库迁移:
python manage.py makemigrations
python manage.py migrate
完成迁移后,更新代码才可以写成:
Profile.objects.filter(user_id=user_id).update(
account_paid=True
)
Webhook 处理注意事项
正式接入时,可以从 request.POST 读取表单数据。如果计费平台发送的是 JSON 请求体,则需要解析 request.body。具体处理方式取决于请求的 Content-Type:
import json
payload = json.loads(request.body)
client_accnum = payload.get("clientAccnum")
还需要处理以下问题:
- 验证计费平台提供的签名或认证信息,不能只信任请求参数。
- 使用事件 ID 或其他唯一标识,防止重复处理同一个 webhook。
- 收到未知事件类型时,不要直接把付费状态改为
False。 - 如果付款记录和账户状态必须同时写入成功,应使用
transaction.atomic()保持一致性。 - 处理成功后及时返回
2xx,因为失败状态码可能触发计费平台重试。 - 如果事件可能乱序到达,应比较事件时间或订阅状态,避免旧通知覆盖新状态。
备注:内容仅供参考。
