Files
zt-closet-wxmini-frontend/miniprogram/utils/request.ts
T
2026-09-11 01:45:04 +08:00

218 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'))
},
})
})
}