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 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 | null = null function doRefresh(): Promise { if (!refreshingPromise) { refreshingPromise = new Promise((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(options: RequestOptions): Promise { 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((resolve, reject) => { const doCall = (): void => { const finalHeader: Record = { '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(filePath: string, name = 'file', url = '/clothes/upload'): Promise { const token = auth.getAccessToken() const header: Record = {} if (token) header['Authorization'] = `Bearer ${token}` return new Promise((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')) }, }) }) }