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
相关产品推荐
相关产品推荐

