/** * 请求封装模块 * 封装 uni.request 支持 GET/POST/PUT/DELETE * 实现 JWT Token 自动携带 * 实现 401 状态码拦截和自动刷新 Token * 支持请求重试机制 * 支持请求取消机制 * Requirements: 1.2, 1.4 */ import { getToken, setToken, removeToken, getRefreshToken, setRefreshToken, removeRefreshToken, removeUserInfo } from '../utils/storage' import config from '../config/index' // 从统一配置获取 API 基础地址 const BASE_URL = config.API_BASE_URL // 存储进行中的请求任务,用于取消 const pendingRequests = new Map() // Token 刷新状态 let isRefreshing = false // 等待 Token 刷新的请求队列 let refreshSubscribers = [] /** * 生成请求唯一标识 * @param {Object} options 请求配置 * @returns {string} */ function generateRequestKey(options) { const { url, method = 'GET', data = {} } = options return `${method}:${url}:${JSON.stringify(data)}` } /** * 处理401错误 - 清除token并跳转登录 * Property 2: Auth Redirect on Invalid Token */ export function handleUnauthorized() { removeToken() removeRefreshToken() removeUserInfo() // 跳转到登录页 const pages = getCurrentPages() const currentPage = pages[pages.length - 1] const currentPath = currentPage ? `/${currentPage.route}` : '' uni.navigateTo({ url: `/pages/login/index?redirect=${encodeURIComponent(currentPath)}` }) } /** * 订阅 Token 刷新完成事件 * @param {Function} callback 回调函数 */ function subscribeTokenRefresh(callback) { refreshSubscribers.push(callback) } /** * 通知所有订阅者 Token 已刷新 * @param {string} newToken 新的 Token */ function onTokenRefreshed(newToken) { refreshSubscribers.forEach(callback => callback(newToken)) refreshSubscribers = [] } /** * 刷新 Token * @returns {Promise} 新的 Token 或 null */ async function refreshToken() { const refreshTokenValue = getRefreshToken() if (!refreshTokenValue) { return null } try { const response = await new Promise((resolve, reject) => { uni.request({ url: `${BASE_URL}/refresh`, method: 'POST', data: { refreshToken: refreshTokenValue }, header: { 'Content-Type': 'application/json' }, timeout: config.REQUEST_TIMEOUT, success: (res) => resolve(res), fail: (err) => reject(err) }) }) const { statusCode, data: responseData } = response if (statusCode === 200 && responseData.code === 0 && responseData.data) { const { token, refreshToken: newRefreshToken } = responseData.data // 保存新的 Token setToken(token) if (newRefreshToken) { setRefreshToken(newRefreshToken) } return token } return null } catch (error) { console.error('Token refresh failed:', error) return null } } /** * 延迟函数 * @param {number} ms 延迟毫秒数 */ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)) /** * 取消指定请求 * @param {string} requestKey 请求标识 */ export function cancelRequest(requestKey) { if (pendingRequests.has(requestKey)) { const task = pendingRequests.get(requestKey) if (task && typeof task.abort === 'function') { task.abort() } pendingRequests.delete(requestKey) } } /** * 取消所有进行中的请求 */ export function cancelAllRequests() { pendingRequests.forEach((task) => { if (task && typeof task.abort === 'function') { task.abort() } }) pendingRequests.clear() } /** * 取消指定URL前缀的所有请求 * @param {string} urlPrefix URL前缀 */ export function cancelRequestsByPrefix(urlPrefix) { pendingRequests.forEach((task, key) => { if (key.includes(urlPrefix)) { if (task && typeof task.abort === 'function') { task.abort() } pendingRequests.delete(key) } }) } /** * 发起请求(带重试、取消和 Token 刷新机制) * @param {Object} options 请求配置 * @param {number} retryCount 当前重试次数 * @returns {Promise} */ export function request(options, retryCount = 0) { const { url, method = 'GET', data = {}, header = {}, needAuth = true, retry = true, cancelable = true, cancelPrevious = false, skipRefresh = false // 是否跳过 Token 刷新(用于刷新 Token 请求本身) } = options const requestKey = generateRequestKey(options) if (cancelPrevious && pendingRequests.has(requestKey)) { cancelRequest(requestKey) } return new Promise((resolve, reject) => { const token = getToken() const requestHeader = { 'Content-Type': 'application/json', ...header } if (needAuth && token) { requestHeader['Authorization'] = `Bearer ${token}` } const requestTask = uni.request({ url: `${BASE_URL}${url}`, method, data, header: requestHeader, timeout: config.REQUEST_TIMEOUT, success: async (res) => { if (cancelable) { pendingRequests.delete(requestKey) } const { statusCode, data: responseData } = res // 处理 401 未授权 - 尝试刷新 Token if (statusCode === 401 && needAuth && !skipRefresh) { // 如果正在刷新 Token,将请求加入队列等待 if (isRefreshing) { subscribeTokenRefresh((newToken) => { // 使用新 Token 重试请求 const newOptions = { ...options, header: { ...header, Authorization: `Bearer ${newToken}` } } request(newOptions, retryCount) .then(resolve) .catch(reject) }) return } isRefreshing = true try { const newToken = await refreshToken() if (newToken) { isRefreshing = false onTokenRefreshed(newToken) // 使用新 Token 重试当前请求 const newOptions = { ...options, header: { ...header, Authorization: `Bearer ${newToken}` } } const result = await request(newOptions, retryCount) resolve(result) return } } catch (refreshError) { console.error('Token refresh error:', refreshError) } isRefreshing = false refreshSubscribers = [] // 刷新失败,清除登录状态并跳转登录页 handleUnauthorized() reject(new Error('登录已过期,请重新登录')) return } if (statusCode >= 400) { const errorMsg = responseData?.message || '请求失败' reject(new Error(errorMsg)) return } if (responseData.success === false) { reject(new Error(responseData.message || '操作失败')) return } resolve(responseData) }, fail: async (err) => { if (cancelable) { pendingRequests.delete(requestKey) } if (err.errMsg && err.errMsg.includes('abort')) { reject(new Error('请求已取消')) return } console.error('Request failed:', err) if (retry && retryCount < config.REQUEST_RETRY_COUNT) { console.log(`请求失败,${config.REQUEST_RETRY_DELAY}ms 后进行第 ${retryCount + 1} 次重试...`) await delay(config.REQUEST_RETRY_DELAY) try { const result = await request(options, retryCount + 1) resolve(result) } catch (retryErr) { reject(retryErr) } return } reject(new Error('网络连接失败,请检查网络设置')) } }) if (cancelable && requestTask) { pendingRequests.set(requestKey, requestTask) } }) } /** * GET 请求 */ export function get(url, data = {}, options = {}) { return request({ url, method: 'GET', data, ...options }) } /** * POST 请求 */ export function post(url, data = {}, options = {}) { return request({ url, method: 'POST', data, ...options }) } /** * PUT 请求 */ export function put(url, data = {}, options = {}) { return request({ url, method: 'PUT', data, ...options }) } /** * DELETE 请求 */ export function del(url, data = {}, options = {}) { return request({ url, method: 'DELETE', data, ...options }) } export default { request, get, post, put, del, handleUnauthorized, cancelRequest, cancelAllRequests, cancelRequestsByPrefix }