diff --git a/src/api/modules/account/login.ts b/src/api/modules/account/login.ts index f8b71a4..db3d913 100644 --- a/src/api/modules/account/login.ts +++ b/src/api/modules/account/login.ts @@ -53,11 +53,15 @@ export interface LoginResponse { * 使用用户名和密码进行登录认证,成功返回双 Token。 * 该接口无需登录即可调用。 * + * 注意:设置 __skipRefresh=true,避免密码错误返回 HTTP 401 时触发无意义的刷新流程。 + * * @param req - 登录请求参数 * @returns 包含 accessToken 和 refreshToken 的响应 */ export function login(req: LoginRequest) { - return requestClient.post('/account/login', req) + return requestClient.post('/account/login', req, { + __skipRefresh: true + }) } /** @@ -67,11 +71,15 @@ export function login(req: LoginRequest) { * 注册成功后自动登录并返回双 Token。 * 该接口无需登录即可调用。 * + * 注意:设置 __skipRefresh=true,避免意外 401 时触发无意义的刷新流程。 + * * @param req - 注册请求参数 * @returns 包含 accessToken 和 refreshToken 的响应 */ export function register(req: RegisterRequest) { - return requestClient.post('/account/register', req) + return requestClient.post('/account/register', req, { + __skipRefresh: true + }) } /** diff --git a/src/api/request/refreshInterceptor.ts b/src/api/request/refreshInterceptor.ts index 4414940..c7bb79a 100644 --- a/src/api/request/refreshInterceptor.ts +++ b/src/api/request/refreshInterceptor.ts @@ -34,32 +34,17 @@ export function createRefreshTokenInterceptor(config: RefreshInterceptorConfig) config /** - * 将 401 业务错误转换为类 AxiosError 结构,统一由 rejected 处理 + * 透传成功响应,HTTP 401 统一由 onRejected 处理 + *

+ * 后端改造后,认证错误(BAD_CREDENTIALS、REFRESH_TOKEN_INVALID 等) + * 返回 HTTP 401 而非 HTTP 200 + code=401,因此无需在此将业务码转换为 HTTP 错误。 */ async function onFulfilled(response: AxiosResponse) { - const data = response.data - // 前一个拦截器在 code !== 401 时已将 AxiosResponse 解包为 ApiResponse, - // 此时 response.data 是业务数据本身,可能为 null(如 updateStatus 返回 data: null) - if (!data || data.code !== 401) { - return response - } - - const reqConfig = response.config as InternalRequestConfig - // 跳过刷新或已重试过 → 终端 401,直接重新认证 - if (reqConfig.__isRetryRequest || reqConfig.__skipRefresh) { - await doReAuthenticate() - return Promise.reject(data) - } - // 构造类 AxiosError,统一由 rejected 走刷新流程 - const fakeError = { - config: response.config, - response: { status: 401, data } - } as AxiosError - return onRejected(fakeError) + return response } /** - * 处理 HTTP 401(以及业务 401 转换后的类 AxiosError) + * 处理 HTTP 401 错误,启动队列式 Token 刷新机制 */ async function onRejected(error: AxiosError) { const { config: reqConfig, response } = error diff --git a/src/api/request/requestClient.ts b/src/api/request/requestClient.ts index ce3c0cd..a673a3f 100644 --- a/src/api/request/requestClient.ts +++ b/src/api/request/requestClient.ts @@ -58,6 +58,7 @@ class RequestClient { this.setupInterceptors() } + // @ts-ignore /** 注册请求和响应拦截器 */ private setupInterceptors(): void { // === 请求拦截器 === @@ -86,8 +87,8 @@ class RequestClient { ) // === 通用响应拦截器 === - // 职责:解包 ApiResponse + 发布非 401 的错误事件。 - // 401 全部透传,由 refreshInterceptor 统一处理。 + // 职责:解包 ApiResponse + 按 HTTP 状态码分类处理错误。 + // HTTP 401 全部透传,由 refreshInterceptor 统一处理。 this.instance.interceptors.response.use( // @ts-expect-error 拦截器解包 axiosResponse → ApiResponse (response) => { @@ -97,30 +98,41 @@ class RequestClient { return data } - // 401 透传给刷新拦截器,此处不处理 - if (data.code === 401) { - return response - } - + // code !== 0 但 HTTP 2xx — 未改造端点或特殊场景,按业务错误处理 eventBus.emit(AppEvents.REQUEST_ERROR, { type: 'business', code: data.code, + httpStatus: response.status, message: data.message || '请求失败' }) return Promise.reject(data) }, (error) => { + const status = error.response?.status + // HTTP 401 透传给刷新拦截器 - if (error.response?.status === 401) { + if (status === 401) { throw error } + // HTTP 400/403/404/409/405/500 — 从 body 提取 ApiResponse + const body = error.response?.data as ApiResponse | undefined + if (body && status) { + eventBus.emit(AppEvents.REQUEST_ERROR, { + type: 'business', + code: body.code, + httpStatus: status, + message: body.message || `请求错误 (${status})` + }) + return Promise.reject(body) + } + + // 网络异常(无 response) eventBus.emit(AppEvents.REQUEST_ERROR, { type: error.response ? 'http' : 'network', - code: error.response?.status, - message: error.response - ? `请求错误 (${error.response.status})` - : '网络异常,请检查网络连接' + code: status, + httpStatus: status, + message: error.response ? `请求错误 (${status})` : '网络异常,请检查网络连接' }) return Promise.reject(error) } @@ -150,7 +162,7 @@ class RequestClient { }, refreshTokenQueue: this.refreshTokenQueue }) - // @ts-expect-error 拦截器解包 axiosResponse → ApiResponse + // 拦截器解包 axiosResponse → ApiResponse this.instance.interceptors.response.use(onFulfilled, onRejected) } diff --git a/src/api/request/types.ts b/src/api/request/types.ts index a3b0f09..595f2d3 100644 --- a/src/api/request/types.ts +++ b/src/api/request/types.ts @@ -28,6 +28,8 @@ export interface RequestErrorEvent { type: 'business' | 'http' | 'network' /** 业务状态码(business 类型时有效) */ code?: number + /** HTTP 状态码(http/business 类型时有效) */ + httpStatus?: number /** 错误消息 */ message: string } diff --git a/src/utils/eventBus.ts b/src/utils/eventBus.ts index 08e190b..6921c1f 100644 --- a/src/utils/eventBus.ts +++ b/src/utils/eventBus.ts @@ -7,6 +7,8 @@ type AppEventsMap = { /** 请求错误(业务错误 / HTTP 错误 / 网络错误) */ 'app:request:error': { type: 'business' | 'http' | 'network' + /** HTTP 状态码(http/business 类型时有效) */ + httpStatus?: number code?: number message: string }