1
0
forked from erp-dev/erp
Files
erpnew/docs/FIX_FILE_UPLOAD_401.md
2025-11-19 14:08:05 +08:00

330 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 请求
- ✅ 其他 ViewSetPrintingOrderViewSet、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