PAYATHON 2026

如何在 django 中接收支付网关的 POST 数据?

支付阿杰

结论

PHP 示例读取的是 $_POST。在 Django 中,对应的写法是 request.POST,请求方法也应判断为 POST:

if request.method == "POST":
    payment_id = request.POST.get("paymentid")

request.GET 只包含 URL 中的查询参数,例如 ?paymentid=123,不会读取 POST 请求体。因此,当前的 request.method == "GET" 和 request.GET['paymentid'] 与网关示例并不匹配。

基本处理方式

可以为支付网关的返回地址单独编写一个视图:

from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.http import require_POST


@require_POST
def payment_callback(request):
    error_code = request.POST.get("Error", "")
    error_text = request.POST.get("ErrorText", "")
    payment_id = request.POST.get("paymentid", "")

    if not payment_id:
        return HttpResponseBadRequest("Missing paymentid")

    # 根据网关规则校验签名、金额、订单号和支付状态
    # 校验成功后再更新本地订单

    return HttpResponse("OK")

URL 配置如下:

from django.urls import path

from . import views


urlpatterns = [
    path("payments/callback/", views.payment_callback, name="payment_callback"),
]

这里使用 request.POST.get(),而不是:

request.POST["paymentid"]

两种写法都能读取字段,但如果字段不存在,方括号写法会抛出 MultiValueDictKeyError。.get() 会返回 None,也可以返回指定的默认值,因此更适合接收外部系统发送的数据:

payment_id = request.POST.get("paymentid", "")

同时检查 GET 和 POST

调试时,可以先确认网关实际发送的是哪种请求:

from django.http import JsonResponse


def payment_callback(request):
    return JsonResponse({
        "method": request.method,
        "content_type": request.content_type,
        "get": request.GET.dict(),
        "post": request.POST.dict(),
    })

这段代码只适合临时测试。生产环境中,不要把支付参数、签名、银行卡信息或其他敏感数据原样返回给客户端。

如果网关有时发送 POST,有时通过查询字符串跳转,可以明确区分两种情况:

def payment_callback(request):
    if request.method == "POST":
        payment_id = request.POST.get("paymentid")
    elif request.method == "GET":
        payment_id = request.GET.get("paymentid")
    else:
        payment_id = None

但不要为了所谓的兼容而直接接受两种方式。具体应以支付网关文档和实际请求为准。

为什么浏览器发生了跳转,却不一定是 GET

“重定向到指定 URL”并不意味着请求一定是 GET。支付网关可能采用以下任一种方式:

  • 返回 302 响应,让浏览器通过 GET 访问返回地址;
  • 输出隐藏表单,再通过 JavaScript 自动提交 POST;
  • 由网关服务器直接向回调地址发送 POST;
  • 让浏览器跳转到结果页,同时另行发送服务器端通知。

PHP 示例使用了 $_POST,说明该示例预期接收表单格式的 POST。不过,仍需查看网关如何定义“返回地址”和“通知地址”,因为两者可能不是同一个接口。

POST 数据为空时需要检查什么

即使请求方法是 POST,request.POST 也可能为空。常见原因是网关发送的数据不是表单格式。

request.POST 主要解析以下内容类型:

application/x-www-form-urlencoded
multipart/form-data

如果网关发送的是 JSON,例如:

Content-Type: application/json

就需要读取并解析 request.body:

import json

from django.http import HttpResponseBadRequest, JsonResponse


def payment_callback(request):
    if request.method != "POST":
        return HttpResponseBadRequest("POST required")

    try:
        payload = json.loads(request.body)
    except json.JSONDecodeError:
        return HttpResponseBadRequest("Invalid JSON")

    payment_id = payload.get("paymentid")
    error_code = payload.get("Error")
    error_text = payload.get("ErrorText")

    return JsonResponse({"received": bool(payment_id)})

排查时至少要确认以下内容:

request.method
request.content_type
request.GET
request.POST
request.body

记录 request.body 时要格外谨慎,避免把支付凭证或个人信息写入日志。

第三方 POST 与 CSRF

支付网关是第三方系统,通常无法携带 Django 页面生成的 CSRF token。如果请求确实被 Django 的 CSRF 中间件拒绝,可以只对专用回调接口使用 csrf_exempt:

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST


@csrf_exempt
@require_POST
def payment_callback(request):
    payment_id = request.POST.get("paymentid")

    # 必须按照网关文档验证签名或消息认证信息
    # 验证成功后才能更新订单状态

    return HttpResponse("OK")

不要关闭整个站点的 CSRF 防护。还要注意,csrf_exempt 只是允许请求进入视图,不能证明请求来自支付网关。

支付回调不能只相信 paymentid

任何人都可以构造这样的 POST 请求:

paymentid=123

收到数据后,还要按照网关协议完成安全校验,通常包括:

  • 校验签名、摘要或消息认证码;
  • 核对订单号、支付金额和币种;
  • 必要时调用网关的服务端查询接口确认交易状态;
  • 防止重复处理同一个通知;
  • 只允许合法的订单状态转换;
  • 在数据库事务中更新支付记录;
  • 按照网关要求返回确认响应。

应将“用户浏览器跳转到支付结果页”和“确认订单已付款”分开处理。页面跳转可以用来展示结果,但订单是否支付成功,应以通过签名验证或服务端查询确认的通知为准。

备注:内容仅供参考。