# 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  
**维护者：** 开发团队
