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