# DocAuditAI 前端对接文档 **版本**: v1.0 **最后更新**: 2025-11-17 **适用范围**: 前端开发人员 --- ## 目录 1. [系统概述](#1-系统概述) 2. [认证与授权](#2-认证与授权) 3. [PostgREST API使用](#3-postgrest-api使用) 4. [RBAC权限系统](#4-rbac权限系统) 5. [数据范围过滤](#5-数据范围过滤) 6. [交叉评查权限](#6-交叉评查权限) 7. [错误处理](#7-错误处理) 8. [常用示例](#8-常用示例) 9. [最佳实践](#9-最佳实践) 10. [故障排查](#10-故障排查) --- ## 1. 系统概述 ### 1.1 架构概览 DocAuditAI采用三层架构: ``` ┌─────────────┐ │ 前端应用 │ Vue.js / React / Angular └─────────────┘ ↓ HTTPS ┌─────────────┐ │ FastAPI │ 认证、权限检查、业务逻辑 │ (端口8000) │ └─────────────┘ ↓ ┌─────────────┐ │ PostgREST │ 直接数据库访问(经过RBAC过滤) │ (端口3000) │ └─────────────┘ ↓ ┌─────────────┐ │ PostgreSQL │ 数据存储 │ (端口5432) │ └─────────────┘ ``` ### 1.2 API端点 - **FastAPI主应用**: `http://localhost:8000/api/v1/` - **PostgREST代理**: `http://localhost:8000/api/v1/postgrest/` - **认证端点**: `http://localhost:8000/api/v1/auth/` ### 1.3 技术栈要求 **前端推荐技术栈**: - **HTTP客户端**: Axios (推荐) 或 Fetch API - **状态管理**: Vuex / Pinia (Vue) 或 Redux (React) - **UI框架**: Element Plus / Ant Design / Material-UI --- ## 2. 认证与授权 ### 2.1 JWT认证流程 DocAuditAI使用JWT(JSON Web Token)进行用户认证。 #### 登录流程 ```mermaid sequenceDiagram participant 前端 participant FastAPI participant 数据库 前端->>FastAPI: POST /api/v1/auth/login {username, password} FastAPI->>数据库: 验证用户凭据 数据库-->>FastAPI: 用户信息 + 权限 FastAPI-->>前端: {access_token, token_type, user_info} 前端->>前端: 存储access_token到localStorage ``` #### 登录示例 **请求**: ```javascript // JavaScript/TypeScript import axios from 'axios'; const API_BASE_URL = 'http://localhost:8000/api/v1'; async function login(username, password) { try { const response = await axios.post(`${API_BASE_URL}/auth/login`, { username, password }); // 保存Token localStorage.setItem('access_token', response.data.access_token); localStorage.setItem('user_info', JSON.stringify(response.data.user_info)); return response.data; } catch (error) { console.error('登录失败:', error.response?.data || error.message); throw error; } } // 使用示例 login('user@example.com', 'password123') .then(data => { console.log('登录成功:', data.user_info); }) .catch(err => { console.error('登录失败'); }); ``` **成功响应** (HTTP 200): ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "user_info": { "user_id": 9, "username": "云浮测试", "ou_id": "yunfu002", "ou_name": "云浮测试用户账户", "roles": ["文档审查员", "普通用户"] } } ``` **失败响应** (HTTP 401): ```json { "detail": "用户名或密码错误" } ``` ### 2.2 请求认证 所有需要认证的API请求都必须在请求头中携带JWT Token。 #### Axios全局配置(推荐) ```javascript // axios-instance.js import axios from 'axios'; const API_BASE_URL = 'http://localhost:8000/api/v1'; // 创建Axios实例 const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json' } }); // 请求拦截器:自动添加Token apiClient.interceptors.request.use( config => { const token = localStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, error => Promise.reject(error) ); // 响应拦截器:处理认证错误 apiClient.interceptors.response.use( response => response, error => { if (error.response?.status === 401) { // Token过期或无效,跳转到登录页 localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); window.location.href = '/login'; } return Promise.reject(error); } ); export default apiClient; ``` **使用示例**: ```javascript import apiClient from './axios-instance'; // 查询文档列表(自动携带Token) async function getDocuments() { const response = await apiClient.get('/postgrest/documents?limit=10'); return response.data; } ``` ### 2.3 Token刷新 JWT Token有效期为24小时。Token过期后需要重新登录。 **检查Token是否过期**: ```javascript function isTokenExpired() { const token = localStorage.getItem('access_token'); if (!token) return true; try { const payload = JSON.parse(atob(token.split('.')[1])); const exp = payload.exp * 1000; // 转换为毫秒 return Date.now() >= exp; } catch (e) { return true; } } // 定期检查Token(可选) setInterval(() => { if (isTokenExpired()) { alert('登录已过期,请重新登录'); window.location.href = '/login'; } }, 60000); // 每分钟检查一次 ``` ### 2.4 退出登录 ```javascript async function logout() { try { // 调用后端登出接口(可选,如果后端有黑名单机制) await apiClient.post('/auth/logout'); } catch (error) { console.error('登出失败:', error); } finally { // 清除本地存储 localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); window.location.href = '/login'; } } ``` --- ## 3. PostgREST API使用 ### 3.1 PostgREST代理概述 PostgREST代理提供了对数据库表的直接RESTful访问,**自动集成RBAC权限检查和数据范围过滤**。 **基础URL**: `/api/v1/postgrest/{table_name}` **支持的HTTP方法**: - `GET`: 查询数据 - `POST`: 创建数据 - `PATCH`: 更新数据 - `DELETE`: 删除数据 ### 3.2 查询操作 (GET) #### 3.2.1 查询所有记录 ```javascript // 查询所有文档(自动应用数据范围过滤) async function getAllDocuments() { const response = await apiClient.get('/postgrest/documents'); return response.data; } // 响应示例 [ { "id": 1936, "title": "测试文档1", "status": "active", "ou_id": "yunfu002", "user_id": 9, "created_at": "2025-11-15T10:30:00Z" }, { "id": 1937, "title": "测试文档2", "status": "pending", "ou_id": "yunfu002", "user_id": 9, "created_at": "2025-11-16T14:20:00Z" } ] ``` #### 3.2.2 分页查询 PostgREST使用`limit`和`offset`参数实现分页。 ```javascript // 分页查询:每页10条,第2页 async function getDocumentsPaginated(page = 1, pageSize = 10) { const offset = (page - 1) * pageSize; const response = await apiClient.get('/postgrest/documents', { params: { limit: pageSize, offset: offset } }); return response.data; } // 使用示例 const documents = await getDocumentsPaginated(2, 10); // 第2页,每页10条 ``` #### 3.2.3 过滤查询 PostgREST支持丰富的过滤运算符: | 运算符 | 说明 | 示例 | |--------|------|------| | `eq` | 等于 | `status=eq.active` | | `neq` | 不等于 | `status=neq.deleted` | | `gt` | 大于 | `created_at=gt.2025-01-01` | | `gte` | 大于等于 | `score=gte.80` | | `lt` | 小于 | `priority=lt.5` | | `lte` | 小于等于 | `age=lte.30` | | `like` | 模糊匹配 | `title=like.*测试*` | | `ilike` | 不区分大小写模糊匹配 | `title=ilike.*TEST*` | | `in` | 在列表中 | `status=in.(active,pending)` | | `is` | 是null | `deleted_at=is.null` | **示例**: ```javascript // 查询状态为active的文档 async function getActiveDocuments() { const response = await apiClient.get('/postgrest/documents', { params: { status: 'eq.active' } }); return response.data; } // 查询标题包含"合同"的文档 async function searchDocuments(keyword) { const response = await apiClient.get('/postgrest/documents', { params: { title: `ilike.*${keyword}*` } }); return response.data; } // 多条件查询:状态为active且创建时间在2025年之后 async function getRecentActiveDocuments() { const response = await apiClient.get('/postgrest/documents', { params: { status: 'eq.active', created_at: 'gte.2025-01-01' } }); return response.data; } ``` #### 3.2.4 排序 使用`order`参数指定排序字段和方向。 ```javascript // 按创建时间降序排列 async function getDocumentsSorted() { const response = await apiClient.get('/postgrest/documents', { params: { order: 'created_at.desc' } }); return response.data; } // 多字段排序:先按状态升序,再按创建时间降序 async function getDocumentsMultiSort() { const response = await apiClient.get('/postgrest/documents', { params: { order: 'status.asc,created_at.desc' } }); return response.data; } ``` #### 3.2.5 字段选择 使用`select`参数指定返回的字段。 ```javascript // 只返回id、title、status字段 async function getDocumentsPartial() { const response = await apiClient.get('/postgrest/documents', { params: { select: 'id,title,status' } }); return response.data; } // 响应示例 [ { "id": 1936, "title": "测试文档1", "status": "active" } ] ``` ### 3.3 创建操作 (POST) ```javascript // 创建新文档 async function createDocument(documentData) { const response = await apiClient.post('/postgrest/documents', documentData); return response.data; } // 使用示例 const newDocument = await createDocument({ title: '新文档', content: '文档内容', status: 'draft', ou_id: 'yunfu002', // 后端会自动验证ou_id是否在用户权限范围内 user_id: 9 }); // 成功响应 (HTTP 201) [ { "id": 2001, "title": "新文档", "content": "文档内容", "status": "draft", "ou_id": "yunfu002", "user_id": 9, "created_at": "2025-11-17T10:00:00Z" } ] ``` **重要提示**: - 创建文档时,后端会自动验证`ou_id`和`user_id`是否符合用户的数据范围权限 - 如果尝试创建不在权限范围内的数据,会返回403 Forbidden ### 3.4 更新操作 (PATCH) ```javascript // 更新文档状态 async function updateDocumentStatus(documentId, newStatus) { const response = await apiClient.patch('/postgrest/documents', { status: newStatus }, { params: { id: `eq.${documentId}` } } ); return response.data; } // 使用示例 const updated = await updateDocumentStatus(1936, 'reviewed'); // 成功响应 (HTTP 200) [ { "id": 1936, "title": "测试文档1", "status": "reviewed", "updated_at": "2025-11-17T11:00:00Z" } ] ``` **重要提示**: - 更新操作会自动应用数据范围过滤,用户只能更新自己权限范围内的数据 - 对于交叉评查文档,如果用户参与了该文档的交叉评查任务,即使文档不在常规数据范围内,也可以更新 ### 3.5 删除操作 (DELETE) ```javascript // 删除文档 async function deleteDocument(documentId) { const response = await apiClient.delete('/postgrest/documents', { params: { id: `eq.${documentId}` } }); return response.status === 204; // 成功删除返回204 No Content } // 使用示例 const deleted = await deleteDocument(1936); if (deleted) { console.log('文档删除成功'); } ``` ### 3.6 完整示例:文档管理 ```javascript // document-service.js import apiClient from './axios-instance'; class DocumentService { // 查询文档列表 async getDocuments(filters = {}) { const params = { limit: filters.pageSize || 10, offset: ((filters.page || 1) - 1) * (filters.pageSize || 10), ...filters.where }; if (filters.orderBy) { params.order = filters.orderBy; } const response = await apiClient.get('/postgrest/documents', { params }); return response.data; } // 获取单个文档 async getDocumentById(id) { const response = await apiClient.get('/postgrest/documents', { params: { id: `eq.${id}` } }); return response.data[0]; // PostgREST返回数组,取第一个 } // 创建文档 async createDocument(data) { const response = await apiClient.post('/postgrest/documents', data); return response.data[0]; } // 更新文档 async updateDocument(id, data) { const response = await apiClient.patch( '/postgrest/documents', data, { params: { id: `eq.${id}` } } ); return response.data[0]; } // 删除文档 async deleteDocument(id) { await apiClient.delete('/postgrest/documents', { params: { id: `eq.${id}` } }); return true; } } export default new DocumentService(); ``` **使用示例**: ```javascript import DocumentService from './document-service'; // 1. 查询文档列表(分页、过滤、排序) const documents = await DocumentService.getDocuments({ page: 1, pageSize: 10, where: { status: 'eq.active', title: 'ilike.*合同*' }, orderBy: 'created_at.desc' }); // 2. 获取单个文档 const document = await DocumentService.getDocumentById(1936); // 3. 创建文档 const newDoc = await DocumentService.createDocument({ title: '采购合同', content: '合同内容...', status: 'draft' }); // 4. 更新文档 const updatedDoc = await DocumentService.updateDocument(1936, { status: 'reviewed' }); // 5. 删除文档 await DocumentService.deleteDocument(1936); ``` --- ## 4. RBAC权限系统 ### 4.1 权限模型 DocAuditAI采用**RBAC(基于角色的访问控制)**模型,支持: - **用户-角色关联**: 一个用户可以有多个角色 - **角色-权限关联**: 一个角色可以有多个权限 - **用户-权限关联**: 用户可以直接拥有权限(绕过角色) - **数据范围控制**: 细粒度的数据访问权限(ALL/DEPT/DEPT_AND_SUB/SELF/CUSTOM) #### 权限键格式 权限键格式为:`{module}:{resource}:{action}` **示例**: - `document:document:view` - 查看文档 - `document:document:create` - 创建文档 - `document:document:update` - 更新文档 - `document:document:delete` - 删除文档 - `crossreview:task:view` - 查看交叉评查任务 ### 4.2 自动权限检查 **所有通过PostgREST代理的请求都会自动进行权限检查**,前端无需手动调用权限检查接口。 **权限检查流程**: 1. 用户发起请求(携带JWT Token) 2. 后端解析Token,提取用户ID和角色 3. 根据表名和HTTP方法映射到权限键 4. 检查用户是否拥有该权限 5. 如果有权限,继续处理请求;否则返回403 Forbidden **示例**: ```javascript // GET /api/v1/postgrest/documents // 后端自动检查权限: document:document:view // POST /api/v1/postgrest/documents // 后端自动检查权限: document:document:create // PATCH /api/v1/postgrest/documents?id=eq.1936 // 后端自动检查权限: document:document:update // DELETE /api/v1/postgrest/documents?id=eq.1936 // 后端自动检查权限: document:document:delete ``` ### 4.3 前端权限控制 虽然后端会自动检查权限,但前端也应该根据用户权限**隐藏或禁用**无权操作的按钮和菜单。 #### 4.3.1 获取当前用户权限 ```javascript // 从Token中解析用户信息(包含角色) function getUserInfo() { const userInfoStr = localStorage.getItem('user_info'); if (!userInfoStr) return null; return JSON.parse(userInfoStr); } // 检查用户是否有特定角色 function hasRole(roleName) { const userInfo = getUserInfo(); return userInfo?.roles?.includes(roleName) || false; } // 使用示例 if (hasRole('系统管理员')) { // 显示管理员菜单 } ``` #### 4.3.2 权限指令(Vue示例) ```vue 删除文档 ``` **推荐做法**: 1. 登录时从后端获取用户的完整权限列表(包括通过角色继承的权限和直接分配的权限) 2. 将权限列表存储到Vuex/Pinia/Redux中 3. 前端根据权限列表控制UI元素的显示/隐藏 --- ## 5. 数据范围过滤 ### 5.1 数据范围类型 DocAuditAI支持5种数据范围类型: | 数据范围 | 说明 | 过滤逻辑 | |---------|------|---------| | **ALL** | 全部数据 | 无过滤,可查看所有数据 | | **DEPT** | 本部门数据 | `ou_id = 用户的ou_id` | | **DEPT_AND_SUB** | 本部门及下级部门数据 | `ou_id IN (用户的ou_id_tree)` | | **SELF** | 本人数据 | `user_id = 用户ID` | | **CUSTOM** | 自定义规则 | 根据自定义SQL表达式过滤 | ### 5.2 自动数据范围过滤 **所有通过PostgREST代理的查询请求都会自动应用数据范围过滤**,前端无需关心过滤逻辑。 **示例**: ```javascript // 用户A(数据范围SELF,user_id=9) // 请求: GET /api/v1/postgrest/documents // 后端自动添加过滤: user_id=eq.9 // 用户B(数据范围DEPT,ou_id=yunfu002) // 请求: GET /api/v1/postgrest/documents // 后端自动添加过滤: ou_id=eq.yunfu002 // 用户C(数据范围ALL) // 请求: GET /api/v1/postgrest/documents // 后端无过滤,返回所有文档 ``` ### 5.3 前端注意事项 1. **不要尝试绕过数据范围过滤** 后端会强制覆盖前端提供的`ou_id`或`user_id`参数,尝试绕过会被拒绝或忽略。 ```javascript // ❌ 错误示例:尝试访问其他部门数据 const response = await apiClient.get('/postgrest/documents', { params: { ou_id: 'eq.guangzhou001' // 后端会覆盖为用户自己的ou_id } }); // ✅ 正确示例:正常查询,后端自动应用数据范围 const response = await apiClient.get('/postgrest/documents'); ``` 2. **数据范围对不同操作的影响** - **查询(GET)**: 自动过滤,只返回权限范围内的数据 - **创建(POST)**: 验证ou_id/user_id是否在权限范围内 - **更新(PATCH)**: 只能更新权限范围内的数据 - **删除(DELETE)**: 只能删除权限范围内的数据 --- ## 6. 交叉评查权限 ### 6.1 交叉评查权限概述 交叉评查允许用户**跨部门访问**特定文档,即使这些文档不在其常规数据范围内。 **应用场景**: - 用户A(云浮部门)参与了一个交叉评查任务,该任务包含来自梅州部门的文档 - 用户A可以查看和评审这些梅州文档,尽管其常规数据范围是SELF(只能看自己的) ### 6.2 交叉评查权限逻辑 对于配置了`special_handling='cross_review_mixed'`的表(如`documents`),后端会: 1. **GET请求**: 扩展访问范围(常规数据范围 **OR** 交叉评查文档) ``` 常规过滤: user_id=eq.9 交叉评查扩展: id.in.(1936,1937) 最终过滤: or=(id.in.(1936,1937),user_id.eq.9) ``` 2. **PATCH/DELETE请求**: 检查目标文档是否在交叉评查范围 - 如果是交叉评查文档 → 移除常规数据范围限制,允许操作 - 如果不是 → 保持常规数据范围限制 ### 6.3 前端使用示例 前端无需特殊处理,后端会自动处理交叉评查权限。 ```javascript // 场景:用户9(数据范围SELF)参与了任务183的交叉评查 // 任务183包含文档[1936, 1937](来自其他用户) // 1. 查询文档列表 const documents = await apiClient.get('/postgrest/documents'); // 返回: // - 用户自己的文档(user_id=9) // - 交叉评查文档[1936, 1937] // 2. 更新交叉评查文档 const updated = await apiClient.patch( '/postgrest/documents', { status: 'reviewed' }, { params: { id: 'eq.1936' } } ); // 成功:后端检测到1936是交叉评查文档,允许更新 // 3. 尝试更新其他用户的非交叉评查文档 try { await apiClient.patch( '/postgrest/documents', { status: 'reviewed' }, { params: { id: 'eq.9999' } } // 不在交叉评查范围 ); } catch (error) { console.error('无权更新此文档'); // 403 Forbidden } ``` ### 6.4 获取交叉评查任务 ```javascript // 查询当前用户的交叉评查任务 async function getCrossReviewTasks() { const response = await apiClient.get('/postgrest/cross_examination_tasks'); return response.data; } // 响应示例 [ { "id": 183, "task_name": "2025年第一季度交叉评查", "status": "in_progress", "created_by": 5, "user_ids": [9, 10, 11], // 参与用户 "created_at": "2025-11-10T09:00:00Z" } ] ``` --- ## 7. 错误处理 ### 7.1 HTTP状态码 | 状态码 | 说明 | 处理建议 | |--------|------|---------| | **200 OK** | 请求成功 | 正常处理数据 | | **201 Created** | 创建成功 | 显示成功消息 | | **204 No Content** | 删除成功 | 显示成功消息 | | **400 Bad Request** | 请求参数错误 | 检查请求参数格式 | | **401 Unauthorized** | 未认证或Token无效 | 跳转到登录页 | | **403 Forbidden** | 无权限 | 显示"无权限"提示 | | **404 Not Found** | 资源不存在 | 显示"资源不存在"提示 | | **500 Internal Server Error** | 服务器错误 | 显示"服务器错误"提示 | ### 7.2 错误响应格式 **标准错误响应**: ```json { "detail": "错误描述" } ``` **详细错误响应**(包含更多上下文): ```json { "detail": "权限不足", "error_code": "PERMISSION_DENIED", "permission_required": "document:document:delete", "user_id": 9 } ``` ### 7.3 全局错误处理 ```javascript // axios-instance.js apiClient.interceptors.response.use( response => response, error => { const status = error.response?.status; const detail = error.response?.data?.detail || '未知错误'; switch (status) { case 400: // 请求参数错误 console.error('请求参数错误:', detail); alert(`请求参数错误: ${detail}`); break; case 401: // 未认证或Token无效 console.error('认证失败,跳转到登录页'); localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); window.location.href = '/login'; break; case 403: // 无权限 console.error('权限不足:', detail); alert(`权限不足: ${detail}`); break; case 404: // 资源不存在 console.error('资源不存在:', detail); alert(`资源不存在: ${detail}`); break; case 500: // 服务器错误 console.error('服务器错误:', detail); alert(`服务器错误,请稍后重试`); break; default: console.error('未知错误:', error); alert(`请求失败,请稍后重试`); } return Promise.reject(error); } ); ``` ### 7.4 特定错误处理 ```javascript // 创建文档时处理权限错误 async function createDocumentWithErrorHandling(data) { try { const response = await apiClient.post('/postgrest/documents', data); alert('文档创建成功'); return response.data[0]; } catch (error) { if (error.response?.status === 403) { const detail = error.response.data?.detail || '权限不足'; if (detail.includes('数据范围')) { alert('您无权在此组织单位创建文档'); } else { alert('您没有创建文档的权限'); } } else { alert('创建文档失败,请重试'); } throw error; } } ``` --- ## 8. 常用示例 ### 8.1 文档列表页面 ```vue 文档列表 全部状态 草稿 活跃 已评查 ID 标题 状态 创建时间 操作 {{ doc.id }} {{ doc.title }} {{ doc.status }} {{ formatDate(doc.created_at) }} 查看 编辑 删除 上一页 第 {{ currentPage }} 页 / 共 {{ totalPages }} 页 下一页 ``` ### 8.2 文档详情页面 ```vue {{ document.title }} 状态: {{ document.status }} 创建时间: {{ formatDate(document.created_at) }} 更新时间: {{ formatDate(document.updated_at) }} {{ document.content }} 编辑 标记为已评查 ``` --- ## 9. 最佳实践 ### 9.1 安全最佳实践 1. **始终使用HTTPS** 生产环境必须使用HTTPS传输,防止Token被窃取。 2. **安全存储Token** - 使用`localStorage`或`sessionStorage`存储Token - 不要将Token存储在Cookie中(避免CSRF攻击) - 不要将Token暴露在URL参数中 3. **Token过期处理** - 定期检查Token是否过期 - Token过期后立即跳转到登录页 - 提供Token刷新机制(如果后端支持) 4. **不要绕过前端权限检查** - 即使前端隐藏了按钮,用户仍可能通过开发者工具发起请求 - 后端会强制执行权限检查,前端绕过无效 ### 9.2 性能最佳实践 1. **分页查询** 始终使用分页,避免一次性加载大量数据。 ```javascript // ✅ 好的做法 const response = await apiClient.get('/postgrest/documents', { params: { limit: 10, offset: 0 } }); // ❌ 不好的做法 const response = await apiClient.get('/postgrest/documents'); // 返回所有数据 ``` 2. **字段选择** 只查询需要的字段,减少数据传输量。 ```javascript // ✅ 好的做法 const response = await apiClient.get('/postgrest/documents', { params: { select: 'id,title,status' } }); // ❌ 不好的做法 const response = await apiClient.get('/postgrest/documents'); // 返回所有字段 ``` 3. **缓存数据** 对于不经常变化的数据(如字典表、配置表),使用前端缓存。 ```javascript // 缓存字典数据 let documentTypesCache = null; async function getDocumentTypes() { if (documentTypesCache) { return documentTypesCache; } const response = await apiClient.get('/postgrest/document_types'); documentTypesCache = response.data; // 5分钟后过期 setTimeout(() => { documentTypesCache = null; }, 5 * 60 * 1000); return documentTypesCache; } ``` 4. **批量操作** 尽量减少请求次数,使用批量操作。 ```javascript // ❌ 不好的做法:逐个删除 for (const id of [1, 2, 3]) { await apiClient.delete('/postgrest/documents', { params: { id: `eq.${id}` } }); } // ✅ 好的做法:批量删除 await apiClient.delete('/postgrest/documents', { params: { id: `in.(1,2,3)` } }); ``` ### 9.3 用户体验最佳实践 1. **加载状态** 显示加载指示器,提升用户体验。 ```vue 加载中... ``` 2. **错误提示** 友好的错误提示,避免显示技术细节。 ```javascript // ❌ 不好的做法 alert(error.message); // "Cannot read property 'data' of undefined" // ✅ 好的做法 alert('加载数据失败,请稍后重试'); ``` 3. **操作确认** 对于删除等危险操作,提供确认提示。 ```javascript async function deleteDocument(id) { if (!confirm('确定要删除此文档吗?此操作不可撤销。')) { return; } try { await apiClient.delete('/postgrest/documents', { params: { id: `eq.${id}` } }); alert('删除成功'); } catch (error) { alert('删除失败'); } } ``` --- ## 10. 故障排查 ### 10.1 常见问题 #### 问题1:401 Unauthorized - Token无效 **症状**: 所有API请求返回401错误。 **原因**: - Token已过期 - Token格式错误 - Token未正确携带在请求头中 **解决方法**: 1. 检查Token是否存在:`localStorage.getItem('access_token')` 2. 检查Token格式是否正确(应为`Bearer {token}`) 3. 检查Token是否过期 4. 重新登录获取新Token #### 问题2:403 Forbidden - 权限不足 **症状**: 某些API请求返回403错误。 **原因**: - 用户没有该操作的权限 - 尝试访问不在数据范围内的数据 **解决方法**: 1. 确认用户是否有对应的权限(检查用户角色) 2. 确认数据是否在用户的数据范围内 3. 联系管理员分配权限 #### 问题3:CORS错误 **症状**: 浏览器控制台显示CORS错误。 **原因**: - 前端域名未在后端CORS白名单中 **解决方法**: 1. 联系后端开发人员将前端域名添加到CORS白名单 2. 开发环境可以配置代理绕过CORS **Vue.js开发环境代理配置**: ```javascript // vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } } }; ``` #### 问题4:数据未按预期过滤 **症状**: 查询返回了不应该看到的数据。 **原因**: - 后端数据范围配置错误 - 前端缓存了旧数据 **解决方法**: 1. 清除浏览器缓存和localStorage 2. 检查用户的数据范围配置是否正确 3. 联系后端开发人员检查RBAC配置 ### 10.2 调试技巧 #### 1. 查看请求详情 使用浏览器开发者工具查看请求详情: 1. 打开开发者工具(F12) 2. 切换到Network标签 3. 发起请求 4. 点击请求查看Headers、Payload、Response #### 2. 查看Token内容 ```javascript // 解码JWT Token function decodeToken(token) { try { const payload = JSON.parse(atob(token.split('.')[1])); console.log('Token内容:', payload); return payload; } catch (e) { console.error('Token解码失败:', e); return null; } } // 使用示例 const token = localStorage.getItem('access_token'); decodeToken(token); ``` #### 3. 启用详细日志 ```javascript // axios-instance.js apiClient.interceptors.request.use(config => { console.log('[Request]', config.method.toUpperCase(), config.url, config.params); return config; }); apiClient.interceptors.response.use( response => { console.log('[Response]', response.status, response.data); return response; }, error => { console.error('[Error]', error.response?.status, error.response?.data); return Promise.reject(error); } ); ``` --- ## 附录 ### A. 完整API端点列表 详见:[API端点列表文档](./前端对接文档-API端点列表.md) ### B. PostgREST过滤运算符完整列表 详见:[PostgREST查询参考](./前端对接文档-PostgREST查询参考.md) ### C. 权限列表 详见:[权限列表文档](./前端对接文档-权限列表.md) --- ## 联系支持 如有问题或建议,请联系: - **技术支持**: support@docauditai.com - **开发团队**: dev@docauditai.com --- **文档版本**: v1.0 **最后更新**: 2025-11-17 **维护者**: Claude Code
状态: {{ document.status }}
创建时间: {{ formatDate(document.created_at) }}
更新时间: {{ formatDate(document.updated_at) }}