612 lines
19 KiB
JavaScript
612 lines
19 KiB
JavaScript
/**
|
||
* 网络请求工具类
|
||
* 封装统一的网络请求方法
|
||
*/
|
||
|
||
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<boolean>} 刷新是否成功
|
||
*/
|
||
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;
|