first commit
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import { BASE_URL, API_PREFIX, ERROR_MESSAGES } from './config'
|
||||
import * as auth from './auth'
|
||||
|
||||
// 业务错误对象(携带 code,页面可按 code 定制处理)
|
||||
export class ApiError extends Error {
|
||||
code: number
|
||||
details?: string[]
|
||||
constructor(code: number, msg: string, details?: string[]) {
|
||||
super(msg)
|
||||
this.code = code
|
||||
this.details = details
|
||||
}
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
data?: any
|
||||
header?: Record<string, any>
|
||||
auth?: boolean // 默认 true,需要鉴权
|
||||
showError?: boolean // 默认 true,业务错误自动 toast
|
||||
}
|
||||
|
||||
// 登录失效 / 账号禁用后的跳转处理(由 app.ts 注入,避免循环依赖)
|
||||
let onAuthExpired: (() => void) | null = null
|
||||
export function setAuthExpiredHandler(handler: () => void): void {
|
||||
onAuthExpired = handler
|
||||
}
|
||||
|
||||
// 单例刷新锁,避免并发请求同时刷新
|
||||
let refreshingPromise: Promise<boolean> | null = null
|
||||
|
||||
function doRefresh(): Promise<boolean> {
|
||||
if (!refreshingPromise) {
|
||||
refreshingPromise = new Promise<boolean>((resolve) => {
|
||||
const refreshToken = auth.getRefreshToken()
|
||||
const accessToken = auth.getAccessToken()
|
||||
if (!refreshToken || !accessToken) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
wx.request({
|
||||
url: `${BASE_URL}${API_PREFIX}/user/refresh-token`,
|
||||
method: 'POST',
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
data: { refresh_token: refreshToken, access_token: accessToken },
|
||||
success: (res) => {
|
||||
const body = res.data as { code: number; data?: LoginTokenResponse }
|
||||
if (body && body.code === 0 && body.data) {
|
||||
auth.setTokens({
|
||||
accessToken: body.data.access_token,
|
||||
refreshToken: body.data.refresh_token,
|
||||
expireTime: body.data.expire_time,
|
||||
})
|
||||
resolve(true)
|
||||
} else {
|
||||
resolve(false)
|
||||
}
|
||||
},
|
||||
fail: () => resolve(false),
|
||||
})
|
||||
}).finally(() => {
|
||||
refreshingPromise = null
|
||||
})
|
||||
}
|
||||
return refreshingPromise
|
||||
}
|
||||
|
||||
interface LoginTokenResponse {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expire_time: number
|
||||
}
|
||||
|
||||
function showToast(title: string): void {
|
||||
wx.showToast({ title, icon: 'none', duration: 2000 })
|
||||
}
|
||||
|
||||
function redirectToLogin(): void {
|
||||
if (onAuthExpired) {
|
||||
onAuthExpired()
|
||||
} else {
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
}
|
||||
|
||||
export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
const {
|
||||
url,
|
||||
method = 'GET',
|
||||
data,
|
||||
header = {},
|
||||
auth: needAuth = true,
|
||||
showError = true,
|
||||
} = options
|
||||
|
||||
const fullUrl = url.startsWith('http') ? url : `${BASE_URL}${API_PREFIX}${url}`
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const doCall = (): void => {
|
||||
const finalHeader: Record<string, any> = {
|
||||
'Content-Type': 'application/json',
|
||||
...header,
|
||||
}
|
||||
if (needAuth) {
|
||||
const token = auth.getAccessToken()
|
||||
if (token) finalHeader['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
wx.request({
|
||||
url: fullUrl,
|
||||
method,
|
||||
data,
|
||||
header: finalHeader,
|
||||
success: (res) => {
|
||||
const body = res.data as { code: number; msg: string; data?: T; details?: string[] }
|
||||
|
||||
// 网络层异常:响应体无 code
|
||||
if (!body || typeof body.code !== 'number') {
|
||||
if (showError) showToast('网络异常,请稍后重试')
|
||||
reject(new Error('network error'))
|
||||
return
|
||||
}
|
||||
|
||||
const code = body.code
|
||||
|
||||
// 成功
|
||||
if (code === 0) {
|
||||
resolve(body.data as T)
|
||||
return
|
||||
}
|
||||
|
||||
// Token 失效:尝试刷新后重试一次
|
||||
if (
|
||||
needAuth &&
|
||||
(code === 11001 || code === 11002 || code === 11004 || res.statusCode === 401)
|
||||
) {
|
||||
doRefresh().then((ok) => {
|
||||
if (ok) {
|
||||
doCall()
|
||||
} else {
|
||||
auth.clearTokens()
|
||||
redirectToLogin()
|
||||
if (showError) showToast('登录已过期,请重新登录')
|
||||
reject(new ApiError(code, body.msg || '登录已过期'))
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 账号禁用(HTTP 200,需单独处理)
|
||||
if (code === 21001) {
|
||||
auth.clearTokens()
|
||||
redirectToLogin()
|
||||
if (showError) showToast(ERROR_MESSAGES[21001] || '账号已被禁用')
|
||||
reject(new ApiError(code, body.msg || '账号已被禁用'))
|
||||
return
|
||||
}
|
||||
|
||||
// 其余业务错误
|
||||
if (showError) {
|
||||
const msg =
|
||||
(body.details && body.details[0]) || ERROR_MESSAGES[code] || body.msg || '请求失败'
|
||||
showToast(msg)
|
||||
}
|
||||
reject(new ApiError(code, body.msg || '请求失败', body.details))
|
||||
},
|
||||
fail: () => {
|
||||
if (showError) showToast('网络异常,请检查网络后重试')
|
||||
reject(new Error('request fail'))
|
||||
},
|
||||
})
|
||||
}
|
||||
doCall()
|
||||
})
|
||||
}
|
||||
|
||||
// 文件上传(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}`
|
||||
|
||||
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 (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))
|
||||
}
|
||||
} else {
|
||||
showToast('上传失败,请稍后重试')
|
||||
reject(new Error('upload fail'))
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
showToast('上传失败,请检查网络后重试')
|
||||
reject(new Error('upload fail'))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user