HaniBlindBox/honey_box/common/request.js
2026-02-02 08:06:59 +08:00

612 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 网络请求工具类
* 封装统一的网络请求方法
*/
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;