/** * 网络请求工具类 * 封装统一的网络请求方法 */ import EnvConfig from '@/common/env.js' import md5 from 'js-md5' import { apiWhiteList } from '@/common/config.js' import RouterManager from '@/common/router.js' import { platform } from '@/common/platform/PlatformFactory' class RequestManager { // Token 刷新状态标记 static isRefreshing = false; // 等待刷新的请求队列 static refreshQueue = []; // 缓存对象 static cache = { data: new Map(), // 缓存过期时间(毫秒) expireTime: 5 * 60 * 1000, // 5分钟 // 缓存时间戳 timestamps: new Map() }; /** * 检查缓存是否存在且未过期 * @param {string} cacheKey 缓存键 * @returns {boolean} 缓存是否有效 */ static isCacheValid(cacheKey) { const now = Date.now(); if (this.cache.data.has(cacheKey)) { const timestamp = this.cache.timestamps.get(cacheKey); return now - timestamp < this.cache.expireTime; } return false; } /** * 更新缓存 * @param {string} cacheKey 缓存键 * @param {any} data 缓存数据 */ static updateCache(cacheKey, data) { this.cache.data.set(cacheKey, data); this.cache.timestamps.set(cacheKey, Date.now()); } /** * 刷新 Token * 使用 Refresh Token 获取新的 Access Token * @returns {Promise} 刷新是否成功 */ static async refreshToken() { const refreshToken = uni.getStorageSync('refreshToken'); if (!refreshToken) { console.log('没有 refreshToken,无法刷新'); return false; } try { const apiBaseUrl = EnvConfig.apiBaseUrl; const baseUrlWithSlash = apiBaseUrl.endsWith('/') ? apiBaseUrl : apiBaseUrl + '/'; const requestUrl = baseUrlWithSlash + 'refresh'; console.log('开始刷新 Token...'); const response = await new Promise((resolve, reject) => { uni.request({ url: requestUrl, method: 'POST', header: { 'content-type': 'application/json' }, data: { refreshToken: refreshToken }, success: (res) => resolve(res), fail: (err) => reject(err) }); }); // 检查 HTTP 状态码 if (response.statusCode === 200 && response.data && response.data.status === 1) { const data = response.data.data; // 更新本地存储的 Token if (data.accessToken) { uni.setStorageSync('token', data.accessToken); uni.setStorageSync('accessToken', data.accessToken); } if (data.refreshToken) { uni.setStorageSync('refreshToken', data.refreshToken); } if (data.expiresIn) { // 计算过期时间戳(当前时间 + expiresIn 秒) const expireTime = Date.now() + (data.expiresIn * 1000); uni.setStorageSync('tokenExpireTime', expireTime); } console.log('Token 刷新成功'); return true; } else { console.log('Token 刷新失败:', response.data?.msg || '未知错误'); return false; } } catch (error) { console.error('Token 刷新请求失败:', error); return false; } } /** * 将请求加入等待队列 * @param {Function} resolve Promise resolve 函数 * @param {Function} reject Promise reject 函数 * @param {Object} param 原始请求参数 */ static addToRefreshQueue(resolve, reject, param) { this.refreshQueue.push({ resolve, reject, param }); } /** * 处理等待队列中的请求 * 刷新成功后,使用新 Token 重新执行队列中的所有请求 * @param {boolean} success 刷新是否成功 */ static processRefreshQueue(success) { console.log(`处理刷新队列,共 ${this.refreshQueue.length} 个请求,刷新${success ? '成功' : '失败'}`); this.refreshQueue.forEach(({ resolve, reject, param }) => { if (success) { // 刷新成功,重新执行请求 this.request(param) .then(resolve) .catch(reject); } else { // 刷新失败,拒绝所有等待的请求 reject({ status: -1, msg: '登录已过期' }); } }); // 清空队列 this.refreshQueue = []; } /** * 清除所有 Token 并跳转登录页 */ static clearTokensAndRedirect() { // 清除所有 Token 相关存储 uni.removeStorageSync('token'); uni.removeStorageSync('accessToken'); uni.removeStorageSync('refreshToken'); uni.removeStorageSync('tokenExpireTime'); uni.removeStorageSync('userinfo'); // 保存当前页面用于登录后跳转 var pages = getCurrentPages(); var currentPage = pages[pages.length - 1]; if (currentPage) { var currentRoute = currentPage.route; var currentParams = currentPage.options || {}; if (currentRoute && currentRoute !== 'pages/user/login') { var redirectPath = '/' + currentRoute; if (Object.keys(currentParams).length > 0) { var paramString = Object.keys(currentParams) .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(currentParams[key])}`) .join('&'); redirectPath += '?' + paramString; } uni.setStorageSync('redirect', redirectPath); } } setTimeout(() => { uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' }); }, 100); RouterManager.navigateTo('/pages/user/login', {}, 'navigateTo') .catch(err => { console.error('登录页面跳转失败:', err); }); } /** * 发送带缓存的GET请求 * @param {String} url 请求地址 * @param {Object} data 请求参数 * @param {Boolean} showLoading 是否显示加载提示 * @returns {Promise} 返回请求Promise */ static getCache(url, data = {}, showLoading = false) { // 生成缓存键 const cacheKey = url + JSON.stringify(data); // 检查缓存是否有效 if (this.isCacheValid(cacheKey)) { console.log(cacheKey + '缓存有效'); return Promise.resolve(this.cache.data.get(cacheKey)); } // 如果缓存无效,发起新请求 return this.get(url, data, showLoading).then(result => { // 更新缓存 this.updateCache(cacheKey, result); return result; }); } /** * 判断URL是否在白名单中 * @param {String} url 请求地址 * @returns {Boolean} 是否在白名单中 */ static isUrlInWhitelist(url) { // 检查URL是否包含白名单中的任一项 return apiWhiteList.some(whiteItem => url.indexOf(whiteItem) > -1); } /** * 生成唯一的nonce值 * @returns {String} nonce值 */ static generateNonce() { return md5(Date.now() + Math.random().toString(36).substring(2, 15)); } /** * 发送网络请求 * @param {Object} param 请求参数 * @param {String} param.url 请求地址 * @param {Object} param.data 请求数据 * @param {Function} param.success 成功回调 * @param {Function} param.fail 失败回调 * @param {Function} param.complete 完成回调 * @param {Boolean} param.Loading 是否显示加载提示 * @param {String} backpage 返回页面 * @param {String} backtype 返回类型 * @returns {Promise} 返回请求Promise */ static request(param, backpage, backtype) { return new Promise((resolve, reject) => { // 参数检查 if (!param || typeof param !== 'object') { reject(new Error('请求参数错误')) return } uni.getNetworkType({ success: function (res) { if (res.networkType == 'none') { uni.showToast({ title: '网络连接异常,请检查网络', icon: 'none' }) reject(new Error('网络连接异常')) return } } }) const url = param.url || '' const method = param.method || 'POST' const data = param.data || {} const Loading = param.Loading || false const token = uni.getStorageSync('token') let client = platform.code // 获取API基础URL const apiBaseUrl = EnvConfig.apiBaseUrl // 拼接完整请求地址,确保URL格式正确 let requestUrl = '' if (url.startsWith('http://') || url.startsWith('https://')) { // 如果是完整的URL,直接使用 requestUrl = url } else { // 否则拼接基础URL和相对路径 // 确保基础URL以/结尾,而请求路径不以/开头 const baseUrlWithSlash = apiBaseUrl.endsWith('/') ? apiBaseUrl : apiBaseUrl + '/' const path = url.startsWith('/') ? url.substring(1) : url requestUrl = baseUrlWithSlash + path } // console.log('请求URL:', requestUrl) // 使用正则表达式从URL中提取主机名 const hostRegex = /^(?:https?:\/\/)?([^\/]+)/i const matches = requestUrl.match(hostRegex) const host = matches && matches[1] ? matches[1] : 'localhost' let header = {} // 添加签名和防重放攻击参数 // 1. 添加时间戳 data.timestamp = Math.floor(Date.now() / 1000) // 2. 添加nonce随机字符串 data.nonce = RequestManager.generateNonce() console.log(requestUrl); if (method.toUpperCase() == 'POST') { // 按照键名对参数进行排序 const sortedParams = {} Object.keys(data).sort().forEach(key => { sortedParams[key] = data[key] }) // 组合参数为字符串 let signStr = '' for (const key in sortedParams) { if (typeof sortedParams[key] === 'object') { signStr += key + '=' + JSON.stringify(sortedParams[key]) + '&' } else { signStr += key + '=' + sortedParams[key] + '&' } } // 获取时间戳,组合为密钥 const timestamp = data.timestamp const appSecret = host + timestamp // 添加密钥并去除最后的& signStr = signStr.substring(0, signStr.length - 1) + appSecret // console.log('签名字符串:', signStr) // 使用MD5生成签名 const sign = md5(signStr) data.sign = sign header = { 'content-type': 'application/json', client: client, 'Authorization': token ? `Bearer ${token}` : '', adid: uni.getStorageSync('_ad_id'), clickid: uni.getStorageSync('_click_id') } } else { // GET请求,添加签名 // 按照键名对参数进行排序 const sortedParams = {} Object.keys(data).sort().forEach(key => { sortedParams[key] = data[key] }) // 组合参数为字符串 let signStr = '' for (const key in sortedParams) { signStr += key + '=' + sortedParams[key] + '&' } // 获取时间戳,组合为密钥 const timestamp = data.timestamp const appSecret = host + timestamp // 添加密钥并去除最后的& signStr = signStr.substring(0, signStr.length - 1) + appSecret // console.log('签名字符串:', signStr) // 使用MD5生成签名 const sign = md5(signStr) // 添加签名到请求参数 data.sign = sign header = { 'content-type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '', client: client, } } // 显示加载提示 if (!Loading) { uni.showLoading({ title: '加载中...' }) } // 发起网络请求 uni.request({ url: requestUrl, method: method.toUpperCase(), header: header, data: data, success: res => { console.log("res.data.status", res.data.status, "res.statusCode", res.statusCode) // 处理 HTTP 401 未授权(Token 过期或无效) if (res.statusCode === 401) { console.log('Token过期或无效'); // 白名单接口直接返回错误,不处理 if (RequestManager.isUrlInWhitelist(requestUrl)) { reject({ status: -1, msg: '登录已过期' }); return; } // 检查是否有 token(用户是否曾经登录过) const currentToken = uni.getStorageSync('token'); if (!currentToken) { // 用户从未登录过,直接返回错误,不跳转 console.log('用户未登录,返回错误'); reject({ status: -1, msg: '请先登录' }); return; } // 检查是否有 refreshToken const refreshToken = uni.getStorageSync('refreshToken'); if (!refreshToken) { console.log('没有 refreshToken,清除token并返回错误'); // 清除过期的token,但不跳转登录页 uni.removeStorageSync('token'); uni.removeStorageSync('accessToken'); uni.removeStorageSync('tokenExpireTime'); uni.removeStorageSync('userinfo'); reject({ status: -1, msg: '登录已过期' }); return; } // 如果正在刷新中,将当前请求加入队列等待 if (RequestManager.isRefreshing) { console.log('Token 正在刷新中,将请求加入队列'); RequestManager.addToRefreshQueue(resolve, reject, param); return; } // 标记开始刷新 RequestManager.isRefreshing = true; // 尝试刷新 Token RequestManager.refreshToken() .then(success => { // 刷新完成,重置标记 RequestManager.isRefreshing = false; if (success) { console.log('Token 刷新成功,重试原请求'); // 刷新成功,重试原请求 RequestManager.request(param) .then(resolve) .catch(reject); // 处理队列中的其他请求 RequestManager.processRefreshQueue(true); } else { console.log('Token 刷新失败'); // 刷新失败,清除 Token,但不跳转登录页 uni.removeStorageSync('token'); uni.removeStorageSync('accessToken'); uni.removeStorageSync('refreshToken'); uni.removeStorageSync('tokenExpireTime'); uni.removeStorageSync('userinfo'); reject({ status: -1, msg: '登录已过期' }); // 拒绝队列中的所有请求 RequestManager.processRefreshQueue(false); } }) .catch(error => { console.error('Token 刷新异常:', error); RequestManager.isRefreshing = false; uni.removeStorageSync('token'); uni.removeStorageSync('accessToken'); uni.removeStorageSync('refreshToken'); uni.removeStorageSync('tokenExpireTime'); uni.removeStorageSync('userinfo'); reject({ status: -1, msg: '登录已过期' }); RequestManager.processRefreshQueue(false); }); return; } var pages = getCurrentPages() if (res.data.status == 1) { // 请求成功 resolve(res.data) } else if (res.data.status == 2222) { // 特殊状态码处理 resolve(res.data) } else if (res.data.status == -9) { let pages = getCurrentPages() console.log(pages[pages.length - 1].route) setTimeout(() => { uni.showToast({ title: res.data.msg, icon: 'none', success() { setTimeout(() => { // #ifdef H5 uni.navigateTo({ url: '/pages/user/bangdingweb' }) // #endif // #ifdef MP-WEIXIN uni.navigateTo({ url: '/pages/user/bangding' }) // #endif }, 1500) } }) }, 100) reject(res.data) } else if (res.data.status == 0) { setTimeout(function () { uni.showToast({ title: res.data.msg, icon: 'none' }) }, 100) reject(res.data) } else if (res.data.status < 0) { var pages = getCurrentPages() if (res.data.msg != null) { if (res.data.msg.indexOf("没有找到用户信息") > -1) { // 清除所有Token相关存储 uni.removeStorageSync('token'); uni.removeStorageSync('accessToken'); uni.removeStorageSync('refreshToken'); uni.removeStorageSync('tokenExpireTime'); uni.removeStorageSync('userinfo'); } } // 白名单接口直接返回错误,不跳转登录页 console.log(requestUrl); if (RequestManager.isUrlInWhitelist(requestUrl)) { reject(res.data) return; } // 清除所有Token相关存储 uni.removeStorageSync('token'); uni.removeStorageSync('accessToken'); uni.removeStorageSync('refreshToken'); uni.removeStorageSync('tokenExpireTime'); uni.removeStorageSync('userinfo'); // 不再自动跳转登录页,只返回错误让调用方处理 // 调用方可以根据业务需要决定是否跳转登录页 reject(res.data) } else { reject(res.data) } typeof param.success == 'function' && param.success(res.data) }, fail: e => { console.error('网络请求失败:', e) uni.showToast({ title: e.errMsg || '请求失败', icon: 'none' }) typeof param.fail == 'function' && param.fail(e) reject(e) }, complete: () => { uni.hideLoading() typeof param.complete == 'function' && param.complete() } }) }) } /** * 发送GET请求 * @param {String} url 请求地址 * @param {Object} data 请求参数 * @param {Boolean} showLoading 是否显示加载提示 * @returns {Promise} 返回请求Promise */ static get(url, data = {}, showLoading = true) { return this.request({ url, data, method: 'GET', Loading: !showLoading }) } /** * 发送POST请求 * @param {String} url 请求地址 * @param {Object} data 请求参数 * @param {Boolean} showLoading 是否显示加载提示 * @returns {Promise} 返回请求Promise */ static post(url, data = {}, showLoading = true) { return this.request({ url, data, method: 'POST', Loading: !showLoading }) } } export default RequestManager;