bfe39e45a9
5. 修改统一认证登录和管理员登录是通过接口形式进行,存储返回的accessToken。 6. 修改交叉评查的部分样式
1563 lines
37 KiB
Markdown
1563 lines
37 KiB
Markdown
# 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
|
||
<!-- Vue自定义指令:v-permission -->
|
||
<template>
|
||
<button v-permission="'document:document:delete'" @click="deleteDoc">
|
||
删除文档
|
||
</button>
|
||
</template>
|
||
|
||
<script>
|
||
// permission-directive.js
|
||
export default {
|
||
mounted(el, binding) {
|
||
const permission = binding.value;
|
||
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
|
||
|
||
// 简化版:根据角色判断(实际应该调用后端接口获取完整权限列表)
|
||
const hasPermission = checkPermission(userInfo, permission);
|
||
|
||
if (!hasPermission) {
|
||
el.style.display = 'none'; // 隐藏按钮
|
||
// 或者禁用按钮
|
||
// el.disabled = true;
|
||
}
|
||
}
|
||
};
|
||
|
||
function checkPermission(userInfo, permission) {
|
||
// 简化逻辑:管理员拥有所有权限
|
||
if (userInfo.roles?.includes('系统管理员')) return true;
|
||
|
||
// 实际应该维护一个角色-权限映射表或调用后端接口
|
||
const rolePermissions = {
|
||
'文档审查员': ['document:document:view', 'document:document:update'],
|
||
'文档管理员': ['document:document:view', 'document:document:create', 'document:document:update', 'document:document:delete']
|
||
};
|
||
|
||
return userInfo.roles?.some(role => rolePermissions[role]?.includes(permission)) || false;
|
||
}
|
||
</script>
|
||
```
|
||
|
||
**推荐做法**:
|
||
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
|
||
<template>
|
||
<div class="document-list">
|
||
<h1>文档列表</h1>
|
||
|
||
<!-- 搜索框 -->
|
||
<div class="search-bar">
|
||
<input
|
||
v-model="searchKeyword"
|
||
@input="onSearch"
|
||
placeholder="搜索文档标题"
|
||
/>
|
||
<select v-model="searchStatus" @change="onSearch">
|
||
<option value="">全部状态</option>
|
||
<option value="draft">草稿</option>
|
||
<option value="active">活跃</option>
|
||
<option value="reviewed">已评查</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 文档列表 -->
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>标题</th>
|
||
<th>状态</th>
|
||
<th>创建时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="doc in documents" :key="doc.id">
|
||
<td>{{ doc.id }}</td>
|
||
<td>{{ doc.title }}</td>
|
||
<td>{{ doc.status }}</td>
|
||
<td>{{ formatDate(doc.created_at) }}</td>
|
||
<td>
|
||
<button @click="viewDocument(doc.id)">查看</button>
|
||
<button
|
||
v-permission="'document:document:update'"
|
||
@click="editDocument(doc.id)"
|
||
>
|
||
编辑
|
||
</button>
|
||
<button
|
||
v-permission="'document:document:delete'"
|
||
@click="deleteDocument(doc.id)"
|
||
>
|
||
删除
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 分页 -->
|
||
<div class="pagination">
|
||
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
|
||
<span>第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
|
||
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, onMounted } from 'vue';
|
||
import apiClient from '@/services/axios-instance';
|
||
|
||
export default {
|
||
setup() {
|
||
const documents = ref([]);
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(10);
|
||
const totalPages = ref(1);
|
||
const searchKeyword = ref('');
|
||
const searchStatus = ref('');
|
||
|
||
// 加载文档列表
|
||
async function loadDocuments() {
|
||
try {
|
||
const params = {
|
||
limit: pageSize.value,
|
||
offset: (currentPage.value - 1) * pageSize.value,
|
||
order: 'created_at.desc'
|
||
};
|
||
|
||
if (searchKeyword.value) {
|
||
params.title = `ilike.*${searchKeyword.value}*`;
|
||
}
|
||
|
||
if (searchStatus.value) {
|
||
params.status = `eq.${searchStatus.value}`;
|
||
}
|
||
|
||
const response = await apiClient.get('/postgrest/documents', { params });
|
||
documents.value = response.data;
|
||
|
||
// 计算总页数(PostgREST返回Content-Range头)
|
||
const contentRange = response.headers['content-range'];
|
||
if (contentRange) {
|
||
const total = parseInt(contentRange.split('/')[1]);
|
||
totalPages.value = Math.ceil(total / pageSize.value);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载文档失败:', error);
|
||
}
|
||
}
|
||
|
||
// 搜索
|
||
function onSearch() {
|
||
currentPage.value = 1;
|
||
loadDocuments();
|
||
}
|
||
|
||
// 分页
|
||
function prevPage() {
|
||
if (currentPage.value > 1) {
|
||
currentPage.value--;
|
||
loadDocuments();
|
||
}
|
||
}
|
||
|
||
function nextPage() {
|
||
if (currentPage.value < totalPages.value) {
|
||
currentPage.value++;
|
||
loadDocuments();
|
||
}
|
||
}
|
||
|
||
// 删除文档
|
||
async function deleteDocument(id) {
|
||
if (!confirm('确定要删除此文档吗?')) return;
|
||
|
||
try {
|
||
await apiClient.delete('/postgrest/documents', {
|
||
params: { id: `eq.${id}` }
|
||
});
|
||
alert('删除成功');
|
||
loadDocuments();
|
||
} catch (error) {
|
||
console.error('删除失败:', error);
|
||
}
|
||
}
|
||
|
||
// 格式化日期
|
||
function formatDate(dateStr) {
|
||
return new Date(dateStr).toLocaleString('zh-CN');
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadDocuments();
|
||
});
|
||
|
||
return {
|
||
documents,
|
||
currentPage,
|
||
totalPages,
|
||
searchKeyword,
|
||
searchStatus,
|
||
onSearch,
|
||
prevPage,
|
||
nextPage,
|
||
deleteDocument,
|
||
formatDate
|
||
};
|
||
}
|
||
};
|
||
</script>
|
||
```
|
||
|
||
### 8.2 文档详情页面
|
||
|
||
```vue
|
||
<template>
|
||
<div class="document-detail">
|
||
<h1>{{ document.title }}</h1>
|
||
|
||
<div class="meta">
|
||
<p>状态: {{ document.status }}</p>
|
||
<p>创建时间: {{ formatDate(document.created_at) }}</p>
|
||
<p>更新时间: {{ formatDate(document.updated_at) }}</p>
|
||
</div>
|
||
|
||
<div class="content">
|
||
{{ document.content }}
|
||
</div>
|
||
|
||
<div class="actions">
|
||
<button
|
||
v-permission="'document:document:update'"
|
||
@click="editDocument"
|
||
>
|
||
编辑
|
||
</button>
|
||
<button
|
||
v-permission="'document:document:update'"
|
||
@click="changeStatus('reviewed')"
|
||
>
|
||
标记为已评查
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, onMounted } from 'vue';
|
||
import { useRoute } from 'vue-router';
|
||
import apiClient from '@/services/axios-instance';
|
||
|
||
export default {
|
||
setup() {
|
||
const route = useRoute();
|
||
const document = ref({});
|
||
|
||
// 加载文档详情
|
||
async function loadDocument() {
|
||
try {
|
||
const id = route.params.id;
|
||
const response = await apiClient.get('/postgrest/documents', {
|
||
params: { id: `eq.${id}` }
|
||
});
|
||
document.value = response.data[0];
|
||
} catch (error) {
|
||
console.error('加载文档失败:', error);
|
||
alert('加载文档失败');
|
||
}
|
||
}
|
||
|
||
// 更改状态
|
||
async function changeStatus(newStatus) {
|
||
try {
|
||
await apiClient.patch(
|
||
'/postgrest/documents',
|
||
{ status: newStatus },
|
||
{ params: { id: `eq.${document.value.id}` } }
|
||
);
|
||
alert('状态更新成功');
|
||
loadDocument();
|
||
} catch (error) {
|
||
console.error('更新状态失败:', error);
|
||
}
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
return new Date(dateStr).toLocaleString('zh-CN');
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadDocument();
|
||
});
|
||
|
||
return {
|
||
document,
|
||
changeStatus,
|
||
formatDate
|
||
};
|
||
}
|
||
};
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 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
|
||
<template>
|
||
<div v-if="loading">加载中...</div>
|
||
<div v-else>
|
||
<!-- 内容 -->
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
loading: false
|
||
};
|
||
},
|
||
methods: {
|
||
async loadData() {
|
||
this.loading = true;
|
||
try {
|
||
// 加载数据
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
```
|
||
|
||
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
|