Files
admin-ui/docs/API-ERROR-HANDLING.md
2910410219 29838b030f 添加会话模型和API Key配置功能
- 在模型模块中新增会话开关状态字段,支持会话模型的管理。
- 更新模型选择器,增加系统模型的API Key配置弹窗,提升用户体验。
- 优化错误处理逻辑,确保接口错误由全局拦截器处理,减少冗余提示。
- 更新相关样式以增强界面可读性和美观性。
2026-05-11 20:01:03 +08:00

717 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
# API 错误处理规范
> 本文档定义了项目中 API 请求的错误处理标准,确保错误提示的一致性和用户体验。
## 📋 目录
- [核心原则](#核心原则)
- [全局拦截器机制](#全局拦截器机制)
- [API 层规范](#api-层规范)
- [页面层规范](#页面层规范)
- [完整示例](#完整示例)
- [常见问题](#常见问题)
---
## 核心原则
### ✅ 避免重复提示
- **全局拦截器**已经处理了大部分错误提示
- **页面层**不应再显示固定的错误提示
- 只在需要自定义错误处理时使用 `errorMode: 'page'`
### ✅ 优先使用后端 message
- 全局拦截器会自动提取后端返回的 `message` 字段
- 页面层使用 `getApiErrorMessage` 工具函数提取错误信息
- 避免写死前端错误文案
### ✅ 业务逻辑与错误提示分离
- `catch` 块只处理必要的业务逻辑(数据清空、状态重置等)
- 错误提示交给全局拦截器或使用 `getApiErrorMessage`
---
## 全局拦截器机制
### 位置
`src/utils/request.ts`
### 错误处理流程
```typescript
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data;
const code = res.code;
// 业务成功
if (code === 200 || code === 0) {
return res;
}
// 业务失败
const errorMode = response.config.requestOptions?.errorMode || 'global';
if (errorMode === 'global') {
// 全局模式:自动显示后端 message
ElMessage.error(res.message || res.msg || '操作失败');
}
// 抛出错误供页面 catch
return Promise.reject(new Error(res.message || res.msg));
},
(error) => {
// 网络错误、超时等
ElMessage.error('网络请求失败,请稍后重试');
return Promise.reject(error);
}
);
```
### 错误模式
| 模式 | 说明 | 使用场景 |
|------|------|----------|
| `global`(默认) | 全局拦截器自动显示错误 | 大部分接口(推荐) |
| `page` | 页面自己处理错误 | 需要自定义错误处理时 |
---
## API 层规范
### 1. 默认配置(推荐 95% 的场景)
```typescript
// src/api/xxx/index.ts
/**
* 获取列表
* 使用默认配置,全局拦截器自动处理错误
*/
export function getList(params: ListParams) {
return request({
url: '/api/xxx/list',
method: 'get',
params
});
}
/**
* 创建数据
* 使用默认配置
*/
export function createItem(data: CreateParams) {
return request({
url: '/api/xxx/create',
method: 'post',
data
});
}
/**
* 更新数据
* 使用默认配置
*/
export function updateItem(data: UpdateParams) {
return request({
url: '/api/xxx/update',
method: 'put',
data
});
}
/**
* 删除数据
* 使用默认配置
*/
export function deleteItem(id: string) {
return request({
url: `/api/xxx/delete/${id}`,
method: 'delete'
});
}
```
### 2. 页面自定义错误处理(特殊场景)
```typescript
// src/api/xxx/index.ts
/**
* 批量导入
* 需要页面自定义错误处理(显示详细的导入结果)
*/
export function batchImport(data: ImportParams) {
return request({
url: '/api/xxx/import',
method: 'post',
data,
requestOptions: {
errorMode: 'page' // 页面自己处理错误
}
});
}
/**
* 复杂表单提交
* 需要根据不同错误类型做不同处理
*/
export function submitComplexForm(data: FormData) {
return request({
url: '/api/xxx/submit',
method: 'post',
data,
requestOptions: {
errorMode: 'page'
}
});
}
```
### 3. 何时使用 `errorMode: 'page'`
仅在以下情况使用:
- ✅ 需要根据不同错误码做不同处理
- ✅ 需要自定义错误提示格式
- ✅ 需要在错误后执行特殊业务逻辑
- ✅ 需要显示详细的错误信息(如批量操作结果)
---
## 页面层规范
### 1. 默认场景 - 全局错误处理
```typescript
// ✅ 推荐写法
const getList = async () => {
loading.value = true;
try {
const res = await listApi(params);
tableData.value = res.data?.list || [];
total.value = res.data?.total || 0;
} catch (error) {
// 错误已由全局拦截器处理
// 这里只处理必要的业务逻辑
tableData.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
// ✅ 简单场景可以不写 catch
const deleteItem = async (id: string) => {
try {
await deleteApi(id);
ElMessage.success('删除成功');
getList(); // 刷新列表
} catch (error) {
// 错误已由全局拦截器处理
}
};
// ✅ 更简洁的写法(如果不需要处理错误)
const deleteItem = async (id: string) => {
await deleteApi(id);
ElMessage.success('删除成功');
getList();
};
```
### 2. 页面自定义错误处理
```typescript
import { getApiErrorMessage } from '/@/utils/request';
// ⚠️ 仅在 API 设置了 errorMode: 'page' 时使用
const saveData = async () => {
loading.value = true;
try {
await saveApi(formData);
ElMessage.success('保存成功');
closeDialog();
} catch (error) {
// 使用 getApiErrorMessage 提取后端错误信息
ElMessage.error(getApiErrorMessage(error, '保存失败'));
} finally {
loading.value = false;
}
};
// 根据不同错误做不同处理
const deleteItem = async (id: string) => {
try {
await deleteApi(id);
ElMessage.success('删除成功');
getList();
} catch (error) {
const msg = getApiErrorMessage(error, '删除失败');
// 根据错误信息做不同处理
if (msg.includes('被引用')) {
ElMessage.warning('该数据已被其他数据引用,无法删除');
showRelatedData(id);
} else if (msg.includes('权限')) {
ElMessage.error('您没有删除权限');
} else {
ElMessage.error(msg);
}
}
};
// 批量操作显示详细结果
const batchImport = async (file: File) => {
try {
const res = await importApi(file);
ElMessage.success(`导入成功 ${res.data.successCount} 条,失败 ${res.data.failCount}`);
if (res.data.failCount > 0) {
showFailDetails(res.data.failList);
}
} catch (error) {
ElMessage.error(getApiErrorMessage(error, '导入失败'));
}
};
```
### 3. getApiErrorMessage 工具函数
```typescript
/**
* 从错误对象中提取错误信息
* @param error - 错误对象
* @param fallback - 默认错误信息
* @returns 错误信息字符串
*/
export function getApiErrorMessage(error: any, fallback: string = '操作失败'): string {
// 优先从 response.data 中获取
if (error?.response?.data?.message) {
return error.response.data.message;
}
if (error?.response?.data?.msg) {
return error.response.data.msg;
}
// 从 Error.message 中获取
if (error?.message && error.message !== 'Network Error') {
return error.message;
}
// 返回默认值
return fallback;
}
```
**使用示例:**
```typescript
import { getApiErrorMessage } from '/@/utils/request';
try {
await someApi();
} catch (error) {
// 使用后端返回的 message如果没有则显示 '操作失败'
ElMessage.error(getApiErrorMessage(error, '操作失败'));
}
```
---
## 完整示例
### 示例 1标准 CRUD 操作
```typescript
// ==================== API 层 ====================
// src/api/user/index.ts
export function getUserList(params: ListParams) {
return request({
url: '/api/user/list',
method: 'get',
params
});
}
export function createUser(data: UserForm) {
return request({
url: '/api/user/create',
method: 'post',
data
});
}
export function updateUser(data: UserForm) {
return request({
url: '/api/user/update',
method: 'put',
data
});
}
export function deleteUser(id: string) {
return request({
url: `/api/user/delete/${id}`,
method: 'delete'
});
}
// ==================== 页面层 ====================
// src/views/user/index.vue
import { getUserList, createUser, updateUser, deleteUser } from '/@/api/user';
// 获取列表
const getList = async () => {
loading.value = true;
try {
const res = await getUserList(queryParams);
tableData.value = res.data?.list || [];
total.value = res.data?.total || 0;
} catch (error) {
// 错误已由全局拦截器处理
tableData.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
// 新增/编辑
const onSubmit = async () => {
try {
await formRef.value?.validate();
if (isEdit.value) {
await updateUser(formData);
ElMessage.success('修改成功');
} else {
await createUser(formData);
ElMessage.success('添加成功');
}
dialogVisible.value = false;
getList();
} catch (error) {
// 错误已由全局拦截器处理
}
};
// 删除
const onDelete = async (row: User) => {
try {
await ElMessageBox.confirm(`确定要删除用户"${row.name}"吗?`, '提示', {
type: 'warning'
});
await deleteUser(row.id);
ElMessage.success('删除成功');
getList();
} catch (error) {
if (error === 'cancel') {
// 用户取消操作
return;
}
// 错误已由全局拦截器处理
}
};
```
### 示例 2需要自定义错误处理
```typescript
// ==================== API 层 ====================
// src/api/model/index.ts
export function listModel(params: ListParams) {
return request({
url: '/api/model/list',
method: 'get',
params,
requestOptions: { errorMode: 'page' } // 页面自己处理错误
});
}
export function createModel(data: ModelForm) {
return request({
url: '/api/model/create',
method: 'post',
data,
requestOptions: { errorMode: 'page' }
});
}
// ==================== 页面层 ====================
// src/views/model/index.vue
import { getApiErrorMessage } from '/@/utils/request';
import { listModel, createModel } from '/@/api/model';
// 获取列表
const getList = async () => {
loading.value = true;
try {
const res = await listModel(queryParams);
tableData.value = res.data?.list || [];
total.value = res.data?.total || 0;
} catch (error) {
// 使用 getApiErrorMessage 提取后端错误
ElMessage.error(getApiErrorMessage(error, '获取列表失败'));
tableData.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
// 创建
const onCreate = async () => {
try {
await createModel(formData);
ElMessage.success('创建成功');
dialogVisible.value = false;
getList();
} catch (error) {
// 使用 getApiErrorMessage 提取后端错误
ElMessage.error(getApiErrorMessage(error, '创建失败'));
}
};
```
---
## 常见问题
### Q1: 什么时候使用 `errorMode: 'page'`
**A:** 仅在以下情况使用:
- 需要根据不同错误码做不同处理
- 需要自定义错误提示格式
- 需要在错误后执行特殊业务逻辑
- 需要显示详细的错误信息
**大部分情况95%)使用默认的全局错误处理即可。**
---
### Q2: 为什么不能在页面写固定的错误提示?
**A:** 因为全局拦截器已经显示了错误,页面再显示会导致**重复提示**
```typescript
// ❌ 错误写法 - 会重复提示
try {
await getList();
} catch (error) {
ElMessage.error('获取列表失败'); // 全局拦截器已经显示过了
}
// ✅ 正确写法
try {
await getList();
} catch (error) {
// 错误已由全局拦截器处理
tableData.value = [];
}
```
---
### Q3: 如何显示后端返回的错误信息?
**A:** 有两种方式:
1. **使用默认全局处理(推荐)**
```typescript
// API 层不设置 errorMode
export function getList() {
return request({ url: '/api/list', method: 'get' });
}
// 页面层不写错误提示
try {
await getList();
} catch (error) {
// 全局拦截器会自动显示后端的 message
}
```
2. **使用 getApiErrorMessage**
```typescript
// API 层设置 errorMode: 'page'
export function getList() {
return request({
url: '/api/list',
method: 'get',
requestOptions: { errorMode: 'page' }
});
}
// 页面层使用 getApiErrorMessage
import { getApiErrorMessage } from '/@/utils/request';
try {
await getList();
} catch (error) {
ElMessage.error(getApiErrorMessage(error, '获取列表失败'));
}
```
---
### Q4: catch 块应该写什么?
**A:** 根据场景决定:
```typescript
// 场景 1只需要清空数据
try {
const res = await getList();
tableData.value = res.data?.list || [];
} catch (error) {
// 错误已由全局拦截器处理
tableData.value = [];
}
// 场景 2需要重置状态
try {
await uploadFile(file);
} catch (error) {
// 错误已由全局拦截器处理
resetUploadState();
fileList.value = [];
}
// 场景 3不需要任何处理
try {
await deleteItem(id);
ElMessage.success('删除成功');
getList();
} catch (error) {
// 错误已由全局拦截器处理
}
// 场景 4需要自定义错误处理API 设置了 errorMode: 'page'
try {
await saveData();
ElMessage.success('保存成功');
} catch (error) {
ElMessage.error(getApiErrorMessage(error, '保存失败'));
}
```
---
### Q5: 如何处理用户取消操作?
**A:** 使用 `if (error === 'cancel')` 判断:
```typescript
const onDelete = async (row: any) => {
try {
await ElMessageBox.confirm('确定要删除吗?', '提示', {
type: 'warning'
});
await deleteApi(row.id);
ElMessage.success('删除成功');
getList();
} catch (error) {
if (error === 'cancel') {
// 用户取消操作,不显示错误
return;
}
// 其他错误已由全局拦截器处理
}
};
```
---
### Q6: 如何处理表单验证失败?
**A:** 表单验证失败不会进入 catch无需特殊处理
```typescript
const onSubmit = async () => {
try {
// 表单验证失败会直接 return不会进入 catch
await formRef.value?.validate();
await saveApi(formData);
ElMessage.success('保存成功');
} catch (error) {
// 这里只会捕获 API 请求错误
// 错误已由全局拦截器处理
}
};
```
---
## 快速检查清单
写新接口时,检查以下几点:
- [ ] **API 层**:是否需要设置 `errorMode: 'page'`
- 大部分情况不需要
- 只在需要自定义错误处理时设置
- [ ] **页面层**catch 块是否正确?
- ✅ 只处理业务逻辑(数据清空、状态重置)
- ❌ 不写固定的 `ElMessage.error('xxx失败')`
- ✅ 如果 API 设置了 `errorMode: 'page'`,使用 `getApiErrorMessage`
- [ ] **是否避免了重复提示?**
- ✅ 全局拦截器 OR 页面 getApiErrorMessage
- ❌ 全局拦截器 + 页面固定提示
---
## 工具函数导入
```typescript
// 导入错误提取工具
import { getApiErrorMessage } from '/@/utils/request';
// 使用示例
try {
await someApi();
} catch (error) {
ElMessage.error(getApiErrorMessage(error, '操作失败'));
}
```
---
## 总结
### 核心规则
1. **默认使用全局错误处理**95% 的场景)
- API 层不设置 `errorMode`
- 页面层 catch 不写固定错误提示
2. **特殊场景使用页面自定义处理**5% 的场景)
- API 层设置 `errorMode: 'page'`
- 页面层使用 `getApiErrorMessage`
3. **避免重复提示**
- 全局拦截器已经处理了错误
- 页面不要再显示固定错误
### 记住这个公式
```
全局错误处理(默认) = 不设置 errorMode + catch 不写错误提示
页面自定义处理(特殊) = errorMode: 'page' + getApiErrorMessage
```
---
**文档版本:** v1.0
**最后更新:** 2026-05-11
**维护者:** 开发团队