name: api-error-handling description: | 接口错误提示能力 - 提供统一的接口错误提示标准和实施方案,基于 wot-design-uni 和 Alova useRequest 回调模式。
触发条件(满足任意一项即触发):
- 编写任何 API 接口调用代码(使用 useRequest)
- 处理 useRequest 的 onError 回调
- 实现全局错误拦截逻辑
- 用户提及"接口错误提示"、"错误处理"、"Toast 提示"等关键词
- 从 Vue2 迁移 API 调用(需要添加错误处理)
- 实现表单提交、列表加载等涉及 API 的功能
必须协同的技能:
- api-migration(API 接口迁移时)
- z-paging-integration(分页列表时)
- use-wd-form(表单提交时)
禁止项:
- 禁止使用 try/catch 包装 send() 函数
- 禁止在组件内手动显示错误 Toast(全局拦截器已处理)
- 禁止使用 immediate: true(必须手动触发请求)
- 禁止在 onError 中重复显示错误提示
覆盖场景:几乎所有 API 调用都需要此技能,包括列表查询、详情查询、表单提交、数据删除、状态更新等。
接口错误提示能力
⚠️ 多技能协同
常见组合场景:
- API 调用:与
api-migration协同 - 分页列表:与
z-paging-integration+api-migration协同 - 表单提交:与
use-wd-form+api-migration协同
参阅 .claude/skills/check-trigger.md 了解完整的技能触发检查流程。
⚠️ 实施前必读(Critical)
🚨 禁止直接编写代码!必须先完成:
✅ 第一步:阅读参考文件
- 必读:
src/http/alova.ts(全局错误拦截实现) - 推荐:
src/pages-sub/repair/pool-list.vue(错误处理示例) - 必读:本技能文件的完整内容
- 必读:
✅ 第二步:理解核心规范
- 必须使用
useRequestHook - 必须设置
immediate: false - 必须使用
onSuccess/onError回调 - 禁止使用
try/catch包装send()
- 必须使用
✅ 第三步:明确错误处理层次
- 全局层:自动错误提示(已在 alova.ts 实现)
- 组件层:业务逻辑处理(onError 回调)
🚫 常见错误(严禁犯)
| ❌ 错误写法 | ✅ 正确写法 | 说明 |
|---|---|---|
try { await send() } |
send() + onError() |
禁止 try/catch |
immediate: true |
immediate: false |
禁止自动执行 |
不写 onError |
onError(() => {}) |
必须处理错误 |
| 组件内手动 Toast | 依赖全局拦截 | 全局已自动提示 |
1. 核心架构约束
本项目接口请求遵循以下强制规范:
- 必须使用 useRequest:所有接口调用都必须通过 Alova 的
useRequest管理状态 - 必须设置 immediate: false:禁止自动执行请求,必须手动触发
- 必须使用回调钩子:使用
onSuccess、onError、onComplete处理请求结果 - 禁止使用 try/catch:不允许使用 try/catch 包装 send 函数调用
2. 双层错误处理机制
| 层级 | 职责 | 实现位置 |
|---|---|---|
| 全局拦截层 | 自动错误提示(默认) | src/http/alova.ts responded |
| 组件回调层 | 日志记录、状态恢复 | useRequest 的 onError 回调钩子 |
2.1 设计原则
- 一致性: 全部使用 wot-design-uni Toast(通过 useGlobalToast)
- 用户友好: 错误信息通俗易懂,避免技术术语
- 分级处理: 根据错误严重程度选择合适的提示方式
- 职责分离: 全局层自动提示,组件层专注业务逻辑
- 可配置性: 支持静默处理和自定义处理
3. 错误级别定义
| 级别 | 错误类型 | 处理方式 | 示例 |
|---|---|---|---|
| L1 - 致命 | 认证过期、服务器宕机 | Message 弹框 + 跳转处理 | 登录已过期,请重新登录 |
| L2 - 严重 | 权限不足、数据不存在 | Toast 错误提示 + 交互指导 | 您没有权限执行此操作 |
| L3 - 一般 | 业务逻辑错误、参数错误 | Toast 错误提示 | 手机号格式不正确 |
| L4 - 轻微 | 网络波动、超时 | Toast 警告提示(短暂) | 网络异常,请稍后重试 |
4. 核心工具类
4.1 ApiErrorHandler 工具类
// src/utils/api-error-handler.ts
import { useGlobalToast } from "@/hooks/useGlobalToast";
/** 错误级别枚举 */
export enum ErrorLevel {
FATAL = "fatal",
SEVERE = "severe",
NORMAL = "normal",
LIGHT = "light",
}
/** API 错误信息接口 */
export interface ApiErrorInfo {
level: ErrorLevel;
message: string;
code?: number | string;
}
/** 错误处理选项 */
export interface ErrorHandlerOptions {
shouldShowError?: boolean;
}
/**
* API 错误处理器
* @description 统一的接口错误提示处理工具,供全局拦截器和组件层使用
*/
export class ApiErrorHandler {
/**
* 统一错误处理入口
* @example ApiErrorHandler.handle({ level: ErrorLevel.NORMAL, message: '操作失败' })
*/
static handle(error: ApiErrorInfo, options: ErrorHandlerOptions = {}): void {
const { shouldShowError = true } = options;
if (!shouldShowError) return;
const toast = useGlobalToast();
switch (error.level) {
case ErrorLevel.FATAL:
this.handleFatalError(error.message, error.code);
break;
case ErrorLevel.SEVERE:
toast.error({ msg: error.message, duration: 3000 });
break;
case ErrorLevel.NORMAL:
toast.error({ msg: error.message, duration: 2000 });
break;
case ErrorLevel.LIGHT:
toast.warning({ msg: error.message, duration: 1500 });
break;
}
}
/**
* 处理致命错误
* @description 使用 Message 弹框,并提供跳转处理
*/
private static handleFatalError(message: string, code?: number | string): void {
uni.showModal({
title: "系统错误",
content: message,
showCancel: false,
success: () => {
uni.reLaunch({ url: "/pages/index/index" });
},
});
}
/**
* 映射 HTTP 状态码到错误信息
* @example const error = ApiErrorHandler.mapStatusCode(404, '用户不存在')
*/
static mapStatusCode(statusCode: number, originalMessage?: string): ApiErrorInfo {
const errorMap: Record<number, { level: ErrorLevel; message: string }> = {
400: { level: ErrorLevel.NORMAL, message: "请求参数错误" },
401: { level: ErrorLevel.FATAL, message: "登录已过期,请重新登录" },
403: { level: ErrorLevel.SEVERE, message: "权限不足,无法访问" },
404: { level: ErrorLevel.NORMAL, message: "请求的资源不存在" },
500: { level: ErrorLevel.SEVERE, message: "服务器内部错误" },
502: { level: ErrorLevel.LIGHT, message: "网关错误,请稍后重试" },
503: { level: ErrorLevel.SEVERE, message: "服务暂时不可用" },
};
const defaultError = { level: ErrorLevel.NORMAL, message: "请求失败,请稍后重试" };
const error = errorMap[statusCode] || defaultError;
return { ...error, code: statusCode, message: originalMessage || error.message };
}
/**
* 映射业务错误码到错误信息
* @example const error = ApiErrorHandler.mapBusinessCode('1001', '余额不足')
*/
static mapBusinessCode(code: string | number, message: string): ApiErrorInfo {
return { level: ErrorLevel.NORMAL, message, code };
}
}
5. Alova 响应拦截器配置
// src/http/alova.ts 关键部分
import { ApiErrorHandler, ErrorLevel } from "@/utils/api-error-handler";
import { useGlobalToast } from "@/hooks/useGlobalToast";
const alovaInstance = createAlova({
baseURL: import.meta.env.VITE_APP_PROXY_PREFIX,
// ... 其他配置 ...
responded: onResponseRefreshToken((response, method) => {
const { config } = method;
const { requestType } = config;
const { statusCode, data: rawData, errMsg } = response as UniNamespace.RequestSuccessCallbackResult;
// 处理特殊请求类型(上传/下载)
if (requestType === "upload" || requestType === "download") {
return response;
}
const toast = useGlobalToast();
const shouldShowToast = config.meta?.toast !== false;
// 处理 HTTP 状态码错误
if (statusCode !== 200) {
const errorInfo = ApiErrorHandler.mapStatusCode(statusCode);
if (shouldShowToast) {
ApiErrorHandler.handle(errorInfo);
}
throw new Error(`${errorInfo.message}:${errMsg}`);
}
// 处理业务逻辑错误
const { code, message, data } = rawData as IResponse;
if (code !== ResultEnum.Success && code !== String(ResultEnum.Success)) {
const errorInfo = ApiErrorHandler.mapBusinessCode(code, message);
if (shouldShowToast) {
ApiErrorHandler.handle(errorInfo);
}
throw new Error(`请求错误[${code}]:${message}`);
}
// 处理成功响应,返回业务数据
return data;
}),
});
6. 组件层使用规范
6.1 标准列表请求场景
<template>
<view class="page-container">
<wd-button :loading="loading" @click="handleRefresh">刷新数据</wd-button>
<view v-if="repairData?.list?.length">
<view v-for="item in repairData.list" :key="item.repairId" class="list-item">
{{ item.title }}
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { useRequest } from "alova/client";
import { getRepairOrderList } from "@/api/repair";
import { ref, onMounted } from "vue";
import type { RepairListParams } from "@/types/repair";
/** 查询参数 */
const queryParams = ref<RepairListParams>({
page: 1,
row: 10,
status: undefined,
});
/**
* 请求管理 - 使用 useRequest + 链式回调钩子
* 🔴 强制规范:必须设置 immediate: false
*/
const {
loading,
data: repairData,
send: loadRepairList,
} = useRequest(() => getRepairOrderList(queryParams.value), { immediate: false })
.onSuccess((result) => {
console.log("维修工单列表加载成功:", result);
})
.onError((error) => {
console.error("维修工单列表加载失败:", error);
// 不需要重复显示错误提示
})
.onComplete(() => {
uni.stopPullDownRefresh();
});
/** 刷新数据 */
function handleRefresh() {
loadRepairList();
}
/** 页面加载时手动触发 */
onMounted(() => {
loadRepairList();
});
</script>
6.2 表单提交场景
<template>
<view class="form-container">
<wd-input v-model="formData.title" label="标题" placeholder="请输入标题" />
<wd-input v-model="formData.description" label="描述" placeholder="请输入描述" />
<wd-button :loading="submitting" @click="handleSubmit">提交</wd-button>
</view>
</template>
<script setup lang="ts">
import { useRequest } from "alova/client";
import { createRepairOrder } from "@/api/repair";
import { reactive } from "vue";
import { useGlobalToast } from "@/hooks/useGlobalToast";
import type { CreateRepairReq } from "@/types/repair";
const toast = useGlobalToast();
/** 表单数据 */
const formData = reactive<CreateRepairReq>({
title: "",
description: "",
repairType: "其他维修",
});
/**
* 表单提交请求管理 - 链式回调写法
* 🔴 强制规范:必须设置 immediate: false
*/
const { loading: submitting, send: submitRepair } = useRequest((data: CreateRepairReq) => createRepairOrder(data), {
immediate: false,
})
.onSuccess((result) => {
console.log("创建成功:", result);
toast.success("维修工单创建成功");
// 重置表单
Object.assign(formData, {
title: "",
description: "",
repairType: "其他维修",
});
})
.onError((error) => {
console.error("创建失败:", error);
});
/** 表单提交处理 */
function handleSubmit() {
// 表单验证
if (!formData.title) {
toast.warning("请输入标题");
return;
}
// 手动触发请求
submitRepair(formData);
}
</script>
6.3 静默请求场景
当需要禁用自动错误提示时,使用 meta.toast: false:
<template>
<view class="page-container">
<wd-button @click="handleSilentRequest">静默请求</wd-button>
</view>
</template>
<script setup lang="ts">
import { useRequest } from "alova/client";
import { getRepairDetail } from "@/api/repair";
import { ApiErrorHandler, ErrorLevel } from "@/utils/api-error-handler";
/**
* 静默请求 - 禁用全局错误提示 - 链式回调写法
* @description 使用 meta.toast: false 禁用自动错误提示,在 onError 中自定义处理
*/
const { send: loadDetail } = useRequest((repairId: string) => getRepairDetail({ repairId }).setMeta({ toast: false }), {
immediate: false,
})
.onSuccess((result) => {
console.log("详情加载成功:", result);
})
.onError((error) => {
console.error("详情加载失败:", error);
// 自定义错误处理逻辑
ApiErrorHandler.handle({
level: ErrorLevel.LIGHT,
message: "加载失败,将使用缓存数据",
});
// 或者使用静默处理,不显示任何提示
// 直接使用缓存数据等兜底逻辑
});
/** 触发静默请求 */
function handleSilentRequest() {
loadDetail("REP_001");
}
</script>
6.4 分页加载更多场景
<template>
<view class="list-container">
<view v-for="item in activityList" :key="item.activitiesId" class="list-item">
{{ item.title }}
</view>
<view v-if="hasMore" class="load-more" @click="handleLoadMore">
{{ loadingMore ? "加载中..." : "加载更多" }}
</view>
</view>
</template>
<script setup lang="ts">
import { useRequest } from "alova/client";
import { getActivityList } from "@/api/activity";
import { ref, onMounted } from "vue";
import type { Activity } from "@/types/activity";
const currentPage = ref(1);
const hasMore = ref(true);
const activityList = ref<Activity[]>([]);
/**
* 首次加载请求 - 链式回调写法
* 🔴 强制规范:必须设置 immediate: false
*/
const { loading, send: loadList } = useRequest((page: number) => getActivityList({ page, row: 10 }), {
immediate: false,
})
.onSuccess((result) => {
activityList.value = result.activitiess || [];
currentPage.value = 1;
hasMore.value = result.activitiess?.length >= 10;
})
.onError((error) => {
console.error("加载失败:", error);
// 错误提示已自动处理
});
/**
* 加载更多请求 - 链式回调写法
* 🔴 强制规范:必须设置 immediate: false
*/
const { loading: loadingMore, send: loadMore } = useRequest((page: number) => getActivityList({ page, row: 10 }), {
immediate: false,
})
.onSuccess((result) => {
if (result?.activitiess?.length) {
activityList.value.push(...result.activitiess);
currentPage.value++;
hasMore.value = result.activitiess.length >= 10;
} else {
hasMore.value = false;
}
})
.onError((error) => {
console.error("加载更多失败:", error);
// 错误提示已自动处理
});
/** 下拉刷新 */
function handleRefresh() {
loadList(1);
}
/** 上拉加载更多 */
function handleLoadMore() {
if (!loadingMore.value && hasMore.value) {
loadMore(currentPage.value + 1);
}
}
/** 页面加载时手动触发 */
onMounted(() => {
loadList(1);
});
</script>
7. 错误处理职责划分
7.1 职责分离原则
| 层级 | 职责 | 代码位置 |
|---|---|---|
| 全局拦截层 | 自动错误提示(默认行为,可通过 meta.toast 禁用) | src/http/alova.ts responded |
| 组件回调层 | 日志记录、状态恢复、业务逻辑处理 | useRequest 的 onError 回调 |
7.2 onError 回调处理指南
| 场景 | 在 onError 中的处理 | 说明 |
|---|---|---|
| 默认情况 | 仅记录日志 | 错误提示已由全局拦截器自动处理 |
| 静默请求 | 自定义错误处理 | 使用 meta.toast: false 后需手动处理 |
| 状态恢复 | 重置 loading 状态、恢复数据 | 用于 UI 状态管理 |
| 兜底逻辑 | 使用缓存数据、显示占位内容 | 保证用户体验 |
8. Toast 工具使用
import { useGlobalToast } from "@/hooks/useGlobalToast";
const toast = useGlobalToast();
// 1. 成功提示 - 操作成功时使用
toast.success("操作成功");
// 2. 错误提示 - 业务错误时使用
toast.error("操作失败:用户名已存在");
// 3. 警告提示 - 轻微问题或提醒时使用
toast.warning("请先选择楼栋");
// 4. 信息提示 - 普通信息时使用
toast.info("正在处理中...");
// 5. 带配置的提示
toast.error({ msg: "网络连接异常,请检查网络设置", duration: 3000 });
9. 关键注意事项
- 向后兼容: 保持现有 API 接口不变,新增可选参数
- 渐进式改造: 先新功能使用新方案,逐步改造旧功能
- 多端适配: 确保在不同平台下(H5、小程序、APP)的一致性表现
- 禁止 try/catch: 严格遵循 api-migration 规范,使用回调钩子处理
- 性能考虑: 避免频繁的错误提示影响性能
- 不要重复提示: 全局拦截器已自动处理错误提示,onError 回调中不要重复显示