import { API_BASE } from '@/config' import type { LoginResult } from '@/api/auth' import { trackApiError } from './tracker' interface RequestOptions { url: string method?: 'GET' | 'POST' | 'PUT' | 'DELETE' data?: any } let isRedirectingToLogin = false /** 重登录期间挂起的请求,登录成功后统一重试 */ let pendingRetries: Array<{ resolve: (v: any) => void; reject: (e: any) => void; options: RequestOptions }> = [] /** 保存登录结果到本地存储 */ export function saveLoginResult(data: LoginResult) { uni.setStorageSync('xc:token', data.token) uni.setStorageSync('xc:userId', data.userId) uni.setStorageSync('xc:nickname', data.nickname) uni.setStorageSync('xc:avatar_url', data.avatar_url) if (data.role) uni.setStorageSync('xc:role', data.role) } /** 执行重登录 */ async function reLogin(): Promise { // H5 环境:demo 登录 if (typeof window !== 'undefined') { const data = await request({ url: '/auth/demo-login', method: 'POST' }) saveLoginResult(data) return } // 小程序环境:wx.login const loginRes = await new Promise((resolve, reject) => { uni.login({ provider: 'weixin', success: resolve, fail: reject }) }) if (!loginRes.code) throw new Error('wx.login failed') const data = await request({ url: '/auth/login', method: 'POST', data: { code: loginRes.code } }) saveLoginResult(data) } export function request(options: RequestOptions): Promise { return new Promise((resolve, reject) => { const token = uni.getStorageSync('xc:token') uni.request({ url: API_BASE + options.url, method: options.method || 'GET', data: options.data, timeout: 15000, // 15秒超时 header: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, success: (res: any) => { const { statusCode, data } = res if (statusCode === 401) { uni.removeStorageSync('xc:token') uni.removeStorageSync('xc:userId') uni.removeStorageSync('xc:nickname') uni.removeStorageSync('xc:avatar_url') uni.removeStorageSync('xc:role') uni.removeStorageSync('xc:currentGroupId') // 加入重试队列 pendingRetries.push({ resolve, reject, options }) if (!isRedirectingToLogin) { isRedirectingToLogin = true reLogin() .then(() => { // 处理快照时已入队的 + reLogin 期间新入队的请求 const retries = [...pendingRetries] pendingRetries = [] retries.forEach(r => request(r.options).then(r.resolve).catch(r.reject)) }) .catch(() => { uni.showToast({ title: '登录失败,请刷新页面', icon: 'none' }) const retries = [...pendingRetries] pendingRetries = [] retries.forEach(r => r.reject({ code: 40100, message: '未登录' })) }) .finally(() => { isRedirectingToLogin = false }) } return } if (data.code === 0) { resolve(data.data) } else { // 埋点:API 业务错误 trackApiError(options.url, statusCode, data.message || '请求失败') // "用户不存在" 不弹 toast(首次登录竞态) if (data.code !== 40400 || data.message !== '用户不存在') { uni.showToast({ title: data.message || '请求失败', icon: 'none' }) } reject(data) } }, fail: (err: any) => { // 埋点:网络错误 trackApiError(options.url, 0, '网络错误') uni.showToast({ title: '网络错误', icon: 'none' }) reject(err) } }) }) }