bigfix
This commit is contained in:
+122
-45
@@ -22,9 +22,35 @@ export interface RequestOptions {
|
||||
silentAuth?: boolean // 非关键请求(如未读红点轮询):401 只失败,不触发全局跳登录
|
||||
}
|
||||
|
||||
// 未登录错误码:调用方可通过 e.code === 11001 区分
|
||||
// 未登录错误码:调用方可通过 e.code === NOT_LOGIN_CODE 区分
|
||||
export const NOT_LOGIN_CODE = 11001
|
||||
|
||||
// 登录失效错误码集合:命中后先尝试刷新 token,刷新失败才清理登录态并跳转登录页
|
||||
export const AUTH_EXPIRED_CODES: number[] = [
|
||||
11000, // 请先登录
|
||||
11001, // 登录状态已失效
|
||||
11002, // 登录状态已过期
|
||||
11004, // 请先登录
|
||||
]
|
||||
|
||||
// 请求超时兜底(毫秒):弱网下 wx.request 默认 60s,用户体验极差
|
||||
const REQUEST_TIMEOUT = 15000
|
||||
const UPLOAD_TIMEOUT = 30000
|
||||
|
||||
function isAuthExpiredCode(code: number, statusCode?: number): boolean {
|
||||
return AUTH_EXPIRED_CODES.indexOf(code) >= 0 || statusCode === 401
|
||||
}
|
||||
|
||||
// 登录失效后的收尾:非关键请求只 reject,不动登录态
|
||||
function failAuth(code: number, msg: string, silentAuth: boolean, showError: boolean) {
|
||||
if (!silentAuth) {
|
||||
auth.clearTokens()
|
||||
handleAuthExpired()
|
||||
if (showError) showToast('登录已过期,请重新登录')
|
||||
}
|
||||
return new ApiError(code, msg || '登录已过期')
|
||||
}
|
||||
|
||||
// 登录失效 / 账号禁用后的跳转处理(由 app.ts 注入,避免循环依赖)
|
||||
let onAuthExpired: (() => void) | null = null
|
||||
export function setAuthExpiredHandler(handler: () => void): void {
|
||||
@@ -48,6 +74,7 @@ function doRefresh(): Promise<boolean> {
|
||||
method: 'POST',
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
data: { refresh_token: refreshToken, access_token: accessToken },
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
success: (res) => {
|
||||
const body = res.data as { code: number; data?: LoginTokenResponse }
|
||||
if (body && body.code === 0 && body.data) {
|
||||
@@ -131,7 +158,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
return
|
||||
}
|
||||
|
||||
const doCall = (): void => {
|
||||
const doCall = (retried = false): void => {
|
||||
const finalHeader: Record<string, any> = {
|
||||
'Content-Type': 'application/json',
|
||||
...header,
|
||||
@@ -146,6 +173,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
method,
|
||||
data,
|
||||
header: finalHeader,
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
success: (res) => {
|
||||
const body = res.data as { code: number; msg: string; data?: T; details?: string[] }
|
||||
|
||||
@@ -164,24 +192,20 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
return
|
||||
}
|
||||
|
||||
// Token 失效:尝试刷新后重试一次
|
||||
if (
|
||||
needAuth &&
|
||||
(code === 11001 || code === 11002 || code === 11004 || res.statusCode === 401)
|
||||
) {
|
||||
// Token 失效:刷新后最多重试一次,避免「刷新成功但资源接口持续 401」引发请求风暴
|
||||
if (needAuth && isAuthExpiredCode(code, res.statusCode)) {
|
||||
if (retried) {
|
||||
reject(failAuth(code, body.msg, silentAuth, showError))
|
||||
return
|
||||
}
|
||||
doRefresh().then((ok) => {
|
||||
if (ok) {
|
||||
doCall()
|
||||
doCall(true)
|
||||
return
|
||||
}
|
||||
// 非关键请求(红点轮询、额度展示等)静默失败:
|
||||
// 只 reject,不动登录态、不跳转,避免把已登录用户悄悄踢成未登录
|
||||
if (!silentAuth) {
|
||||
auth.clearTokens()
|
||||
handleAuthExpired()
|
||||
if (showError) showToast('登录已过期,请重新登录')
|
||||
}
|
||||
reject(new ApiError(code, body.msg || '登录已过期'))
|
||||
reject(failAuth(code, body.msg, silentAuth, showError))
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -209,47 +233,100 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// token 已过期:先静默刷新再发请求,省掉一次必然失败的 401 往返
|
||||
// (刷新失败也不直接拒绝:仍发一次,由后端判定,避免网络抖动就把用户踢下线)
|
||||
if (needAuth && auth.isTokenExpired()) {
|
||||
doRefresh().then(() => {
|
||||
doCall()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
doCall()
|
||||
})
|
||||
}
|
||||
|
||||
export interface UploadOptions {
|
||||
showError?: boolean // 默认 true,业务错误自动 toast
|
||||
timeout?: number // 默认 30s
|
||||
silentAuth?: boolean // 非关键上传:401 只失败,不触发全局跳登录
|
||||
}
|
||||
|
||||
// 文件上传(multipart,字段名 file)
|
||||
export function uploadFile<T = any>(filePath: string, name = 'file', url = '/clothes/upload'): Promise<T> {
|
||||
const token = auth.getAccessToken()
|
||||
const header: Record<string, any> = {}
|
||||
if (token) header['Authorization'] = `Bearer ${token}`
|
||||
export function uploadFile<T = any>(
|
||||
filePath: string,
|
||||
name = 'file',
|
||||
url = '/clothes/upload',
|
||||
options: UploadOptions = {},
|
||||
): Promise<T> {
|
||||
const { showError = true, timeout = UPLOAD_TIMEOUT, silentAuth = false } = options
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: url.startsWith('http') ? url : `${BASE_URL}${API_PREFIX}${url}`,
|
||||
filePath,
|
||||
name,
|
||||
header,
|
||||
success: (res) => {
|
||||
let body: { code: number; msg: string; data?: T; details?: string[] } | null = null
|
||||
try {
|
||||
body = JSON.parse(res.data)
|
||||
} catch (e) {
|
||||
body = null
|
||||
}
|
||||
if (body && typeof body.code === 'number') {
|
||||
// 未登录:直接失败,不发请求
|
||||
if (!auth.isLoggedIn()) {
|
||||
reject(new ApiError(NOT_LOGIN_CODE, '未登录'))
|
||||
return
|
||||
}
|
||||
|
||||
const doUpload = (retried = false): void => {
|
||||
const header: Record<string, any> = {}
|
||||
const token = auth.getAccessToken()
|
||||
if (token) header['Authorization'] = `Bearer ${token}`
|
||||
|
||||
wx.uploadFile({
|
||||
url: url.startsWith('http') ? url : `${BASE_URL}${API_PREFIX}${url}`,
|
||||
filePath,
|
||||
name,
|
||||
header,
|
||||
timeout,
|
||||
success: (res) => {
|
||||
let body: { code: number; msg: string; data?: T; details?: string[] } | null = null
|
||||
try {
|
||||
body = JSON.parse(res.data)
|
||||
} catch (e) {
|
||||
body = null
|
||||
}
|
||||
|
||||
if (!body || typeof body.code !== 'number') {
|
||||
if (showError) showToast('上传失败,请稍后重试')
|
||||
reject(new Error('upload fail'))
|
||||
return
|
||||
}
|
||||
|
||||
if (body.code === 0) {
|
||||
resolve(body.data as T)
|
||||
} else {
|
||||
const msg =
|
||||
(body.details && body.details[0]) || ERROR_MESSAGES[body.code] || body.msg || '上传失败'
|
||||
showToast(msg)
|
||||
reject(new ApiError(body.code, body.msg || '上传失败', body.details))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
showToast('上传失败,请稍后重试')
|
||||
|
||||
// Token 失效:刷新后最多重试一次
|
||||
if (isAuthExpiredCode(body.code, res.statusCode)) {
|
||||
if (retried) {
|
||||
reject(failAuth(body.code, body.msg, silentAuth, showError))
|
||||
return
|
||||
}
|
||||
doRefresh().then((ok) => {
|
||||
if (ok) {
|
||||
doUpload(true)
|
||||
return
|
||||
}
|
||||
reject(failAuth(body.code, body.msg, silentAuth, showError))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const msg =
|
||||
(body.details && body.details[0]) || ERROR_MESSAGES[body.code] || body.msg || '上传失败'
|
||||
if (showError) showToast(msg)
|
||||
reject(new ApiError(body.code, body.msg || '上传失败', body.details))
|
||||
},
|
||||
fail: () => {
|
||||
if (showError) showToast('上传失败,请检查网络后重试')
|
||||
reject(new Error('upload fail'))
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
showToast('上传失败,请检查网络后重试')
|
||||
reject(new Error('upload fail'))
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
doUpload()
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user