feat: HTTP状态码 + 业务码
This commit is contained in:
@@ -53,11 +53,15 @@ export interface LoginResponse {
|
|||||||
* 使用用户名和密码进行登录认证,成功返回双 Token。
|
* 使用用户名和密码进行登录认证,成功返回双 Token。
|
||||||
* 该接口无需登录即可调用。
|
* 该接口无需登录即可调用。
|
||||||
*
|
*
|
||||||
|
* 注意:设置 __skipRefresh=true,避免密码错误返回 HTTP 401 时触发无意义的刷新流程。
|
||||||
|
*
|
||||||
* @param req - 登录请求参数
|
* @param req - 登录请求参数
|
||||||
* @returns 包含 accessToken 和 refreshToken 的响应
|
* @returns 包含 accessToken 和 refreshToken 的响应
|
||||||
*/
|
*/
|
||||||
export function login(req: LoginRequest) {
|
export function login(req: LoginRequest) {
|
||||||
return requestClient.post<LoginResponse>('/account/login', req)
|
return requestClient.post<LoginResponse>('/account/login', req, {
|
||||||
|
__skipRefresh: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,11 +71,15 @@ export function login(req: LoginRequest) {
|
|||||||
* 注册成功后自动登录并返回双 Token。
|
* 注册成功后自动登录并返回双 Token。
|
||||||
* 该接口无需登录即可调用。
|
* 该接口无需登录即可调用。
|
||||||
*
|
*
|
||||||
|
* 注意:设置 __skipRefresh=true,避免意外 401 时触发无意义的刷新流程。
|
||||||
|
*
|
||||||
* @param req - 注册请求参数
|
* @param req - 注册请求参数
|
||||||
* @returns 包含 accessToken 和 refreshToken 的响应
|
* @returns 包含 accessToken 和 refreshToken 的响应
|
||||||
*/
|
*/
|
||||||
export function register(req: RegisterRequest) {
|
export function register(req: RegisterRequest) {
|
||||||
return requestClient.post<LoginResponse>('/account/register', req)
|
return requestClient.post<LoginResponse>('/account/register', req, {
|
||||||
|
__skipRefresh: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,32 +34,17 @@ export function createRefreshTokenInterceptor(config: RefreshInterceptorConfig)
|
|||||||
config
|
config
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 401 业务错误转换为类 AxiosError 结构,统一由 rejected 处理
|
* 透传成功响应,HTTP 401 统一由 onRejected 处理
|
||||||
|
* <p>
|
||||||
|
* 后端改造后,认证错误(BAD_CREDENTIALS、REFRESH_TOKEN_INVALID 等)
|
||||||
|
* 返回 HTTP 401 而非 HTTP 200 + code=401,因此无需在此将业务码转换为 HTTP 错误。
|
||||||
*/
|
*/
|
||||||
async function onFulfilled(response: AxiosResponse<ApiResponse>) {
|
async function onFulfilled(response: AxiosResponse<ApiResponse>) {
|
||||||
const data = response.data
|
return response
|
||||||
// 前一个拦截器在 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 HTTP 401(以及业务 401 转换后的类 AxiosError)
|
* 处理 HTTP 401 错误,启动队列式 Token 刷新机制
|
||||||
*/
|
*/
|
||||||
async function onRejected(error: AxiosError) {
|
async function onRejected(error: AxiosError) {
|
||||||
const { config: reqConfig, response } = error
|
const { config: reqConfig, response } = error
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class RequestClient {
|
|||||||
this.setupInterceptors()
|
this.setupInterceptors()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
/** 注册请求和响应拦截器 */
|
/** 注册请求和响应拦截器 */
|
||||||
private setupInterceptors(): void {
|
private setupInterceptors(): void {
|
||||||
// === 请求拦截器 ===
|
// === 请求拦截器 ===
|
||||||
@@ -86,8 +87,8 @@ class RequestClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// === 通用响应拦截器 ===
|
// === 通用响应拦截器 ===
|
||||||
// 职责:解包 ApiResponse + 发布非 401 的错误事件。
|
// 职责:解包 ApiResponse + 按 HTTP 状态码分类处理错误。
|
||||||
// 401 全部透传,由 refreshInterceptor 统一处理。
|
// HTTP 401 全部透传,由 refreshInterceptor 统一处理。
|
||||||
this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
// @ts-expect-error 拦截器解包 axiosResponse → ApiResponse
|
// @ts-expect-error 拦截器解包 axiosResponse → ApiResponse
|
||||||
(response) => {
|
(response) => {
|
||||||
@@ -97,30 +98,41 @@ class RequestClient {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// 401 透传给刷新拦截器,此处不处理
|
// code !== 0 但 HTTP 2xx — 未改造端点或特殊场景,按业务错误处理
|
||||||
if (data.code === 401) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit(AppEvents.REQUEST_ERROR, {
|
eventBus.emit(AppEvents.REQUEST_ERROR, {
|
||||||
type: 'business',
|
type: 'business',
|
||||||
code: data.code,
|
code: data.code,
|
||||||
|
httpStatus: response.status,
|
||||||
message: data.message || '请求失败'
|
message: data.message || '请求失败'
|
||||||
})
|
})
|
||||||
return Promise.reject(data)
|
return Promise.reject(data)
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
const status = error.response?.status
|
||||||
|
|
||||||
// HTTP 401 透传给刷新拦截器
|
// HTTP 401 透传给刷新拦截器
|
||||||
if (error.response?.status === 401) {
|
if (status === 401) {
|
||||||
throw error
|
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, {
|
eventBus.emit(AppEvents.REQUEST_ERROR, {
|
||||||
type: error.response ? 'http' : 'network',
|
type: error.response ? 'http' : 'network',
|
||||||
code: error.response?.status,
|
code: status,
|
||||||
message: error.response
|
httpStatus: status,
|
||||||
? `请求错误 (${error.response.status})`
|
message: error.response ? `请求错误 (${status})` : '网络异常,请检查网络连接'
|
||||||
: '网络异常,请检查网络连接'
|
|
||||||
})
|
})
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
@@ -150,7 +162,7 @@ class RequestClient {
|
|||||||
},
|
},
|
||||||
refreshTokenQueue: this.refreshTokenQueue
|
refreshTokenQueue: this.refreshTokenQueue
|
||||||
})
|
})
|
||||||
// @ts-expect-error 拦截器解包 axiosResponse → ApiResponse
|
// 拦截器解包 axiosResponse → ApiResponse
|
||||||
this.instance.interceptors.response.use(onFulfilled, onRejected)
|
this.instance.interceptors.response.use(onFulfilled, onRejected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export interface RequestErrorEvent {
|
|||||||
type: 'business' | 'http' | 'network'
|
type: 'business' | 'http' | 'network'
|
||||||
/** 业务状态码(business 类型时有效) */
|
/** 业务状态码(business 类型时有效) */
|
||||||
code?: number
|
code?: number
|
||||||
|
/** HTTP 状态码(http/business 类型时有效) */
|
||||||
|
httpStatus?: number
|
||||||
/** 错误消息 */
|
/** 错误消息 */
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ type AppEventsMap = {
|
|||||||
/** 请求错误(业务错误 / HTTP 错误 / 网络错误) */
|
/** 请求错误(业务错误 / HTTP 错误 / 网络错误) */
|
||||||
'app:request:error': {
|
'app:request:error': {
|
||||||
type: 'business' | 'http' | 'network'
|
type: 'business' | 'http' | 'network'
|
||||||
|
/** HTTP 状态码(http/business 类型时有效) */
|
||||||
|
httpStatus?: number
|
||||||
code?: number
|
code?: number
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user