1
0
forked from erp-dev/erp

feat: added pgadmin container

This commit is contained in:
2025-11-19 14:08:05 +08:00
parent 400c0db6db
commit ad0b6a84d2
7 changed files with 1263 additions and 0 deletions

329
docs/FIX_FILE_UPLOAD_401.md Normal file
View File

@@ -0,0 +1,329 @@
# 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