8.8 KiB
PlateOrder 文件上传时返回 401 错误修复报告
更新(2025-11):
PlateOrder.plate_image现已改为 JSON 字段,仅接受通过/api/v1/upload/上传后的file_id引用,不再直接处理 multipart/form-data。以下内容保留旧问题的排查过程以供参考。
📋 问题描述
症状:
- PlateOrder 的 PATCH 请求在上传图片时返回 401 错误
- 错误信息:
token invalid - 仅修改普通字段(不上传文件)时工作正常
- 用户权限验证正常,Token 有效
🔍 根本原因
缺少文件解析器配置!
当前端使用 multipart/form-data 格式上传文件时,Django REST Framework 需要特定的解析器来处理请求体。
问题对比
❌ 修复前(PlateOrderViewSet)
class PlateOrderViewSet(viewsets.ModelViewSet):
queryset = models.PlateOrder.objects.all()
permission_classes = [DjangoModelPermissions]
# ❌ 缺少 parser_classes!
# 默认只有 JSONParser,无法处理 multipart/form-data
结果:
- 当 Content-Type 为
multipart/form-data时 - DRF 无法正确解析请求
- JWT 认证从 multipart 数据中无法提取 Token
- 返回 401 "token invalid"
✅ 其他工作正常的上传接口(UploadFileViewSet)
class UploadFileViewSet(viewsets.GenericViewSet):
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser] # ✅ 有配置!
🔧 修复方案
修改 1: 导入必要的解析器
# api_v1/views/printing/views.py
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
修改 2: 添加 parser_classes 配置
class PlateOrderViewSet(viewsets.ModelViewSet):
queryset = models.PlateOrder.objects.all()
permission_classes = [DjangoModelPermissions]
parser_classes = [MultiPartParser, FormParser, JSONParser] # ✅ 新增
pagination_class = LimitOffsetPagination
# ... 其他配置
解析器说明
| 解析器 | 作用 | 支持的 Content-Type |
|---|---|---|
MultiPartParser |
处理文件上传 | multipart/form-data |
FormParser |
处理表单数据 | application/x-www-form-urlencoded |
JSONParser |
处理 JSON 数据 | application/json |
为什么需要三个?
MultiPartParser- 支持文件上传(PATCH 带图片)FormParser- 兼容表单提交JSONParser- 保持原有的 JSON API 支持(PATCH 不带文件)
📝 技术细节
DRF 请求处理流程
- 请求到达 → 检查
Content-Type头 - 选择解析器 → 根据
parser_classes匹配合适的解析器 - 解析请求体 → 提取数据(包括 Token)
- 认证 → JWT 认证器从请求中提取并验证 Token
- 权限检查 → 验证用户权限
- 业务逻辑 → 处理请求
没有正确的解析器时:
- 步骤 3 失败 → 无法正确解析 multipart 数据
- 步骤 4 失败 → JWT 认证无法从请求中提取 Token
- 返回 401 "token invalid"
为什么 Authorization 头中的 Token 失效?
虽然 Token 在 HTTP Header 中(不在请求体),但 DRF 的认证流程依赖于正确的请求解析。当解析器不匹配时:
- DRF 尝试用默认解析器(JSONParser)解析 multipart 数据
- 解析失败,请求对象状态异常
- 认证流程检测到异常,拒绝请求
- 返回 401
✅ 验证
测试覆盖
创建了完整的测试文件 test_plate_order_file_upload.py,包含:
-
✅ PATCH 上传文件 + 更新字段
data = { 'urgency_level': '加急', 'plate_image': image_file, 'is_mark_frame': True, } response = client.patch(url, data, format='multipart') -
✅ PATCH 仅更新字段(不上传文件)
data = {'urgency_level': '特急'} response = client.patch(url, data, format='json') -
✅ PATCH 仅上传文件
data = {'plate_image': image_file} response = client.patch(url, data, format='multipart') -
✅ POST 创建时上传文件
data = { 'customer': customer.id, 'design_code': 'DESIGN001', 'plate_image': image_file, } response = client.post(url, data, format='multipart')
所有测试通过! ✅
🎯 前端使用指南
方法 1: 使用 FormData(推荐)
// 上传文件 + 更新字段
const formData = new FormData();
formData.append('urgency_level', '加急');
formData.append('is_mark_frame', true);
formData.append('plate_image', fileObject); // File 对象
// 重要:不要设置 Content-Type,浏览器会自动设置为 multipart/form-data
const response = await fetch('/api/v1/plate-orders/123/', {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`
// ❌ 不要设置 'Content-Type': 'multipart/form-data'
},
body: formData
});
方法 2: 使用 Axios
import axios from 'axios';
// 上传文件
const formData = new FormData();
formData.append('plate_image', file);
formData.append('urgency_level', '加急');
const response = await axios.patch(
'/api/v1/plate-orders/123/',
formData,
{
headers: {
'Authorization': `Bearer ${token}`,
// Axios 会自动设置正确的 Content-Type
}
}
);
方法 3: 纯字段更新(不上传文件)
// 不上传文件时,使用 JSON 格式
const response = await axios.patch(
'/api/v1/plate-orders/123/',
{
urgency_level: '正常',
is_mark_frame: false
},
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
⚠️ 常见错误
错误 1: 手动设置 Content-Type
// ❌ 错误:手动设置 multipart Content-Type
const formData = new FormData();
formData.append('file', file);
fetch(url, {
headers: {
'Content-Type': 'multipart/form-data' // ❌ 不要这样做!
},
body: formData
});
问题: 浏览器需要自动生成 boundary 参数,手动设置会导致边界标识符缺失。
正确做法: 让浏览器自动设置 Content-Type。
错误 2: 混用 JSON 和 FormData
// ❌ 错误:在 FormData 中添加对象
formData.append('data', { urgency_level: '加急' }); // ❌ 会变成 "[object Object]"
// ✅ 正确:逐个添加字段
formData.append('urgency_level', '加急');
formData.append('is_mark_frame', true);
错误 3: 文件字段名不匹配
// ❌ 错误:字段名与后端不一致
formData.append('image', file); // 后端期望 'plate_image'
// ✅ 正确:使用后端定义的字段名
formData.append('plate_image', file); // 匹配 PlateOrder.plate_image
🔄 影响范围
修改的文件
-
api_v1/views/printing/views.py- 添加解析器导入
- 添加
parser_classes配置到PlateOrderViewSet
-
api_v1/views/printing/test_plate_order_file_upload.py(新增)- 完整的文件上传测试套件
不影响的功能
- ✅ 纯 JSON 的 PATCH 请求(不上传文件)
- ✅ GET、POST、DELETE 请求
- ✅ 其他 ViewSet(PrintingOrderViewSet、PrintingJobViewSet)
- ✅ 现有的权限控制
- ✅ 现有的序列化器逻辑
📊 类似问题排查
如果其他接口也遇到文件上传时 401 错误,检查:
-
ViewSet 是否配置了 parser_classes?
parser_classes = [MultiPartParser, FormParser, JSONParser] -
序列化器是否包含文件字段?
class Meta: fields = [..., 'plate_image', ...] # 确保包含 -
模型是否有对应的文件字段?
plate_image = models.FileField(upload_to='plate_images/', ...) -
前端是否正确使用 FormData?
const formData = new FormData(); formData.append('plate_image', file);
🎓 经验总结
关键点
-
DRF 不会自动处理所有格式
- 默认只有 JSONParser
- 文件上传需要显式配置 MultiPartParser
-
解析器配置是认证的前提
- 正确的请求解析 → 正确的认证
- 解析失败 → 认证失败 → 401
-
前端不要设置 multipart 的 Content-Type
- 浏览器会自动设置
- 手动设置会缺少 boundary 参数
最佳实践
-
对于支持文件上传的 ViewSet
parser_classes = [MultiPartParser, FormParser, JSONParser] -
对于纯 API 的 ViewSet
parser_classes = [JSONParser] # 或不设置(使用默认) -
测试覆盖
- 测试文件上传
- 测试纯字段更新
- 测试混合场景(文件 + 字段)
修复完成日期: 2025-11-19
修复人员: AI Assistant
测试状态: ✅ 全部通过(4/4 tests)