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

559 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 PATCH 请求返回 401 错误排查指南
## 📋 问题概述
前端反馈 PlateOrder 接口的 PATCH 请求返回 401 未授权错误。
## 🔍 可能的原因与解决方案
### 原因 1⃣: JWT Token 未发送或格式错误 ⭐⭐⭐⭐⭐
**最常见原因!** 前端请求时缺少 Authorization 头或格式不正确。
#### 检查方法:
在浏览器开发者工具中:
1. 打开 **Network** 标签页
2. 找到失败的 PATCH 请求
3. 查看 **Request Headers**
#### 正确的格式:
```http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
```
#### ❌ 常见错误:
```http
# 1: Authorization
Content-Type: application/json
# 2: 使
Authorization: Token eyJhbGciOiJIUzI1NiIs... Bearer
# 3: Token
Authorization: Bearer undefined token
```
#### ✅ 前端修复代码:
```javascript
// Axios 示例
import axios from 'axios';
// 方法1: 每次请求都带 token
const token = localStorage.getItem('access_token');
axios.patch('/api/v1/plate-orders/123/', {
urgency_level: '加急'
}, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
// 方法2: 配置全局拦截器(推荐)
axios.interceptors.request.use(config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, error => {
return Promise.reject(error);
});
// 之后所有请求自动带 token
axios.patch('/api/v1/plate-orders/123/', {
urgency_level: '加急'
});
```
```javascript
// Fetch 示例
const token = localStorage.getItem('access_token');
fetch('/api/v1/plate-orders/123/', {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
urgency_level: '加急'
})
});
```
---
### 原因 2⃣: JWT Token 已过期 ⭐⭐⭐⭐
Token 有效期为 7 天(根据 `settings.py` 配置),过期后需要刷新。
#### 检查方法:
```bash
# 在后端查看 Token 是否有效
curl -X GET http://localhost:8000/api/v1/plate-orders/ \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# 如果返回 401说明 Token 已过期
```
#### ✅ 前端修复代码:
```javascript
// 实现 Token 自动刷新
import axios from 'axios';
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// 如果正在刷新,将请求加入队列
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token;
return axios(originalRequest);
}).catch(err => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = localStorage.getItem('refresh_token');
try {
const response = await axios.post('/api/token/refresh/', {
refresh: refreshToken
});
const { access } = response.data;
localStorage.setItem('access_token', access);
originalRequest.headers['Authorization'] = 'Bearer ' + access;
processQueue(null, access);
return axios(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
// Token 刷新失败,跳转到登录页
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
```
---
### 原因 3⃣: 用户缺少 `change_plateorder` 权限 ⭐⭐⭐
用户可能有 `view_plateorder` 权限(能 GET但缺少 `change_plateorder` 权限(不能 PATCH/PUT
#### 检查方法:
```bash
# 使用诊断脚本检查
cd /home/f/coding/flower
uv run python diagnose_permissions.py <username>
# 或在 Django shell 中检查
python manage.py shell
>>> from django.contrib.auth import get_user_model
>>> User = get_user_model()
>>> user = User.objects.get(username='前端用户名')
>>> user.has_perm('printing.change_plateorder')
False # ❌ 如果返回 False说明缺少权限
```
#### ✅ 后端修复方法:
**方法1: 在 Django Admin 中添加**
1. 登录 Admin: http://localhost:8000/admin/
2. 进入 **用户****组** 管理
3. 找到目标用户/组
4.**权限** 区域勾选:
- `printing | plate order | Can change plate order`
5. 保存
**方法2: 使用 Django Shell**
```python
python manage.py shell
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
User = get_user_model()
user = User.objects.get(username='前端用户名')
# 添加单个权限
change_perm = Permission.objects.get(codename='change_plateorder')
user.user_permissions.add(change_perm)
# 或批量添加所有 PlateOrder 权限
perms = Permission.objects.filter(
content_type__app_label='printing',
content_type__model='plateorder'
)
user.user_permissions.add(*perms)
print("✅ 权限已添加!")
```
**方法3: 通过用户组管理(推荐)**
```python
from django.contrib.auth.models import Group, Permission
# 创建或获取用户组
group, created = Group.objects.get_or_create(name='业务员')
# 为组添加权限
perms = Permission.objects.filter(
content_type__app_label='printing',
content_type__model='plateorder'
)
group.permissions.add(*perms)
# 将用户加入组
user.groups.add(group)
```
---
### 原因 4⃣: CORS 问题导致预检请求失败 ⭐⭐
如果前端和后端在不同域名PATCH 请求会先发送 OPTIONS 预检请求。
#### 检查方法:
在 Network 标签页中查看是否有:
1. 一个 OPTIONS 请求(预检)
2. 紧随其后的 PATCH 请求
如果 OPTIONS 请求失败PATCH 不会发送。
#### ✅ 后端修复方法:
检查 `settings.py` 中的 CORS 配置:
```python
# flower/settings.py
# 确保已安装 django-cors-headers
INSTALLED_APPS = [
...
'corsheaders',
...
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # 放在最前面
'django.middleware.common.CommonMiddleware',
...
]
# 开发环境可以允许所有域名
CORS_ALLOW_ALL_ORIGINS = True
# 生产环境应该指定具体域名
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://localhost:8080",
"https://your-frontend-domain.com",
]
# 允许携带认证信息
CORS_ALLOW_CREDENTIALS = True
# 允许的 HTTP 方法
CORS_ALLOW_METHODS = [
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
# 允许的请求头
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]
```
---
### 原因 5⃣: 前端发送了错误的 Content-Type ⭐
某些情况下Content-Type 不匹配会导致认证失败。
#### 检查方法:
Request Headers 中应该有:
```
Content-Type: application/json
```
#### ❌ 常见错误:
```
Content-Type: text/plain
Content-Type: application/x-www-form-urlencoded
```
#### ✅ 前端修复:
```javascript
// Axios 会自动设置,但可以强制指定
axios.patch('/api/v1/plate-orders/123/', data, {
headers: {
'Content-Type': 'application/json'
}
});
// Fetch 必须手动设置
fetch('/api/v1/plate-orders/123/', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(data)
});
```
---
## 🛠️ 完整的前端解决方案
### 推荐的 Axios 配置React/Vue
```javascript
// api.js
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:8000/api/v1',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器:自动添加 Token
api.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// 响应拦截器:处理 401 自动刷新 Token
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
}).catch(err => Promise.reject(err));
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
window.location.href = '/login';
return Promise.reject(error);
}
try {
const response = await axios.post(
'http://localhost:8000/api/token/refresh/',
{ refresh: refreshToken }
);
const { access } = response.data;
localStorage.setItem('access_token', access);
processQueue(null, access);
originalRequest.headers.Authorization = `Bearer ${access}`;
return api(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
export default api;
// 使用示例
// import api from './api';
// api.patch('/plate-orders/123/', { urgency_level: '加急' });
```
---
## 🔬 诊断工具
### 1. 后端权限诊断
```bash
cd /home/f/coding/flower
uv run python diagnose_permissions.py <username>
```
### 2. 手动测试 API
```bash
# 获取 Token登录
curl -X POST http://localhost:8000/api/token/ \
-H "Content-Type: application/json" \
-d '{"username": "your_username", "password": "your_password"}'
# 使用 Token 测试 PATCH
curl -X PATCH http://localhost:8000/api/v1/plate-orders/1/ \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"urgency_level": "加急"}'
```
### 3. 前端调试代码
```javascript
// 在控制台运行,检查 Token 是否存在
console.log('Access Token:', localStorage.getItem('access_token'));
console.log('Refresh Token:', localStorage.getItem('refresh_token'));
// 手动测试 PATCH 请求
fetch('/api/v1/plate-orders/1/', {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ urgency_level: '加急' })
})
.then(res => res.json())
.then(data => console.log('Success:', data))
.catch(err => console.error('Error:', err));
```
---
## 📊 错误代码对照表
| 状态码 | 含义 | 可能原因 |
|--------|------|----------|
| 401 | Unauthorized | Token 缺失、格式错误、已过期 |
| 403 | Forbidden | Token 有效但用户缺少权限 |
| 400 | Bad Request | 请求数据格式错误 |
| 405 | Method Not Allowed | 接口不支持该 HTTP 方法 |
---
## ✅ 检查清单
在联系后端开发人员之前,前端请确认:
- [ ] 请求头包含 `Authorization: Bearer <token>`
- [ ] Token 格式正确(不是 `undefined``null`
- [ ] Content-Type 设置为 `application/json`
- [ ] 请求 URL 正确(末尾有 `/`
- [ ] Token 未过期(可以用同样的 Token 测试 GET 请求)
- [ ] 浏览器控制台无 CORS 错误
- [ ] Network 标签显示请求实际发送了
如果以上都确认无误但仍然 401请提供
1. Network 标签中完整的 Request Headers
2. 完整的 Response包括状态码和响应体
3. 用户名(用于后端检查权限配置)
---
## 📞 获取帮助
如果问题仍未解决:
1. 运行诊断脚本并提供输出
2. 提供前端的完整错误信息
3. 提供用户名供后端检查权限
4. 提供 Network 标签的截图
---
**最后更新**: 2025-11-19