You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React多部分表单嵌套JSON及文件上传至Django后端问题

一对一关联模型的嵌套文件上传解决方案

问题场景

需要将包含嵌套thirdAgencyDetails对象的表单数据通过multipart表单提交至Django后端,Project与ThirdAgencyDetail模型为一对一关联。目前根层级文件(如project_mou)上传正常,但thirdAgencyDetails下的文件(agreement、sanction_letter等)无法正确关联,尝试多种FormData构造方式要么触发后端报错Project() got unexpected keyword arguments: 'agreement',要么上传空的关联对象。

表单数据结构

{
  project_status: 'OPEN',
  project_title: '',
  project_code: '',
  start_date: '',
  end_date: '',
  duration: '',
  engagement_year: '',
  financial_year: '',
  project_budget: '',
  domain: '',
  project_type: '',
  project_mou: null,
  other_docs: null,
  thirdAgencyDetails: {
    government_body: '',
    scheme_name: '',
    sanctioned_budget: '',
    industry_contribution: '',
    government_contribution: '',
    agreement: null,
    sanction_letter: null,
    proposal_doc: null
  },
  facultyDetails: []
}

Django模型定义

class Project(models.Model):
    user = models.CharField(max_length=15, null=True)
    project_title = models.CharField(max_length=250, null=True)
    project_code = models.CharField(max_length=250, null=True)
    start_date = models.DateField(max_length=10, null=True, default='')
    end_date = models.DateField(max_length=10, null=True, default='')
    duration = models.CharField(max_length=255)
    engagement_year = models.IntegerField()
    financial_year = models.CharField(max_length=255)
    project_budget = models.DecimalField(max_digits=10, decimal_places=2)
    domain = models.CharField(max_length=250, null=True)
    project_type = models.CharField(max_length=255)
    project_status=models.CharField(max_length=250, blank=True)
    thirdAgencyDetails = models.OneToOneField(ThirdAgencyDetail, on_delete=models.CASCADE, null=True, blank=True)
    project_mou = models.FileField(upload_to='R&D_docs/', blank=True)
    other_docs = models.FileField(upload_to='R&D_docs/', blank=True)
class ThirdAgencyDetail(models.Model):
    government_body = models.CharField(max_length=255)
    scheme_name = models.CharField(max_length=255)
    sanctioned_budget = models.DecimalField(max_digits=10, decimal_places=2)
    industry_contribution = models.DecimalField(max_digits=10, decimal_places=2)
    government_contribution = models.DecimalField(max_digits=10, decimal_places=2)
    agreement = models.FileField(upload_to='jointProject_thirdAgency_docs/',blank=True)
    sanction_letter = models.FileField(upload_to='jointProject_thirdAgency_docs/',blank=True)
    proposal_doc = models.FileField(upload_to='jointProject_thirdAgency_docs/',blank=True)

解决方案

一、前端FormData构造

Django支持通过**双下划线__**标识关联模型的字段,需将嵌套的thirdAgencyDetails字段转换为thirdAgencyDetails__字段名的格式添加到FormData中:

const formData = new FormData();

// 处理根层级字段
const rootFields = ['project_status', 'project_title', 'project_code', 'start_date', 'end_date', 'duration', 'engagement_year', 'financial_year', 'project_budget', 'domain', 'project_type'];
rootFields.forEach(field => {
  formData.append(field, yourFormData[field]);
});

// 处理根层级文件
if (yourFormData.project_mou) {
  formData.append('project_mou', yourFormData.project_mou);
}
if (yourFormData.other_docs) {
  formData.append('other_docs', yourFormData.other_docs);
}

// 处理嵌套的thirdAgencyDetails字段
const thirdAgency = yourFormData.thirdAgencyDetails;
const agencyFields = ['government_body', 'scheme_name', 'sanctioned_budget', 'industry_contribution', 'government_contribution', 'agreement', 'sanction_letter', 'proposal_doc'];
agencyFields.forEach(field => {
  const value = thirdAgency[field];
  if (value) {
    formData.append(`thirdAgencyDetails__${field}`, value);
  }
});

// 发送请求(需携带CSRF令牌)
fetch('/your-project-endpoint/', {
  method: 'POST',
  body: formData,
  headers: {
    'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
  }
});

二、后端Django处理

自定义ModelForm,手动处理一对一关联模型的字段保存逻辑:

1. 定义表单类

from django import forms
from .models import Project, ThirdAgencyDetail

class ProjectForm(forms.ModelForm):
    # 声明thirdAgencyDetails的关联字段
    thirdAgencyDetails__government_body = forms.CharField(max_length=255, required=False)
    thirdAgencyDetails__scheme_name = forms.CharField(max_length=255, required=False)
    thirdAgencyDetails__sanctioned_budget = forms.DecimalField(max_digits=10, decimal_places=2, required=False)
    thirdAgencyDetails__industry_contribution = forms.DecimalField(max_digits=10, decimal_places=2, required=False)
    thirdAgencyDetails__government_contribution = forms.DecimalField(max_digits=10, decimal_places=2, required=False)
    thirdAgencyDetails__agreement = forms.FileField(required=False)
    thirdAgencyDetails__sanction_letter = forms.FileField(required=False)
    thirdAgencyDetails__proposal_doc = forms.FileField(required=False)

    class Meta:
        model = Project
        fields = ['project_status', 'project_title', 'project_code', 'start_date', 'end_date', 'duration', 'engagement_year', 'financial_year', 'project_budget', 'domain', 'project_type', 'project_mou', 'other_docs']

    def save(self, commit=True):
        # 先保存Project主对象(暂不提交到数据库)
        project = super().save(commit=False)
        
        # 提取thirdAgencyDetails的清洗后数据
        agency_data = {}
        for field in ThirdAgencyDetail._meta.fields:
            key = f'thirdAgencyDetails__{field.name}'
            if key in self.cleaned_data and self.cleaned_data[key] is not None:
                agency_data[field.name] = self.cleaned_data[key]
        
        if agency_data:
            # 存在关联数据时,更新或创建ThirdAgencyDetail对象
            agency_obj, created = ThirdAgencyDetail.objects.get_or_create(
                id=project.thirdAgencyDetails_id if project.thirdAgencyDetails else None,
                defaults=agency_data
            )
            project.thirdAgencyDetails = agency_obj
        
        # 提交到数据库
        if commit:
            project.save()
        return project

2. 视图函数处理

from django.http import JsonResponse
from .forms import ProjectForm

def create_project(request):
    if request.method != 'POST':
        return JsonResponse({'status': 'error', 'msg': '仅支持POST请求'}, status=405)
    
    form = ProjectForm(request.POST, request.FILES)
    if form.is_valid():
        project = form.save()
        return JsonResponse({'status': 'success', 'project_id': project.id})
    else:
        return JsonResponse({'status': 'error', 'errors': form.errors}, status=400)

排查要点

  • 确保前端FormData的字段名与后端表单的字段名完全匹配(包括双下划线)
  • 确认文件字段是真实的File对象,而非null或字符串
  • 后端视图必须同时接收request.POST和request.FILES,否则文件无法被解析
  • 检查CSRF令牌是否正确携带,避免请求被Django拦截

内容的提问来源于stack exchange,提问作者Vishnu Ezhuthachan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 07:32:12