# 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 # 或在 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 ``` ### 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 格式正确(不是 `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