import axios from 'axios' import {Message, MessageBox, Notification, Loading} from 'element-ui' import store from '@/store' import {getAccessToken, getRefreshToken, getTenantId, setToken, getVisitTenantId} from '@/utils/auth' import errorCode from '@/utils/errorCode' import {getPath, getTenantEnable} from "@/utils/ruoyi"; import {refreshToken} from "@/api/login"; import { ApiEncrypt } from '@/utils/encrypt' // 需要忽略的提示。忽略后,自动 Promise.reject('error') const ignoreMsgs = [ "无效的刷新令牌", // 刷新令牌被删除时,不用提示 "刷新令牌已过期" // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面 ] // 是否显示重新登录 export let isRelogin = { show: false }; // Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现 // 请求队列 let requestList = [] // 是否正在刷新中 let isRefreshToken = false axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' // 创建axios实例 const service = axios.create({ // axios中请求配置有baseURL选项,表示请求URL公共部分 baseURL: process.env.VUE_APP_BASE_API + '/admin-api/', // 此处的 /admin-api/ 地址,原因是后端的基础路径为 /admin-api/ // 超时 timeout: 30000, // 禁用 Cookie 等信息 withCredentials: false, }) let loadingInstance = null function startLoading() { loadingInstance = Loading.service({ fullscreen: false, text: '拼命加载中...', background: 'rgba(0, 0, 0, 0.1)' }) } function endLoading() { loadingInstance.close() } let needLoadingRequestCount = 0 function showFullScreenLoading() { if (needLoadingRequestCount === 0) { startLoading() } needLoadingRequestCount++ } function tryHideFullScreenLoading() { if (needLoadingRequestCount <= 0) return needLoadingRequestCount-- if (needLoadingRequestCount === 0) { endLoading() } } // request拦截器 service.interceptors.request.use(config => { showFullScreenLoading() // 是否需要设置 token const isToken = (config.headers || {}).isToken === false if (getAccessToken() && !isToken) { config.headers['Authorization'] = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } // 设置租户 if (getTenantEnable()) { const tenantId = getTenantId(); if (tenantId) { config.headers['tenant-id'] = tenantId; } // 只有登录时,才设置 visit-tenant-id 访问租户 const visitTenantId = getVisitTenantId() if (config.headers.Authorization && visitTenantId) { config.headers['visit-tenant-id'] = visitTenantId } } // get请求映射params参数 if (config.method === 'get' && config.params) { let url = config.url + '?'; for (const propName of Object.keys(config.params)) { const value = config.params[propName]; const part = encodeURIComponent(propName) + '=' if (value !== null && typeof(value) !== "undefined") { if (typeof value === 'object') { for (const key of Object.keys(value)) { let params = propName + '[' + key + ']'; const subPart = encodeURIComponent(params) + '=' url += subPart + encodeURIComponent(value[key]) + "&"; } } else { url += part + encodeURIComponent(value) + "&"; } } } url = url.slice(0, -1); config.params = {}; config.url = url; } // 是否 API 加密 if ((config.headers || {}).isEncrypt) { try { // 加密请求数据 if (config.data) { config.data = ApiEncrypt.encryptRequest(config.data) // 设置加密标识头 config.headers[ApiEncrypt.getEncryptHeader()] = 'true' } } catch (error) { console.error('请求数据加密失败:', error) throw error } } return config }, error => { tryHideFullScreenLoading() console.log(error) Promise.reject(error) }) // 响应拦截器 service.interceptors.response.use(async res => { tryHideFullScreenLoading() let { data } = res // 未设置状态码则默认成功状态 // 二进制数据则直接返回,例如说 Excel 导出 if ( res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer' ) { // 注意:如果导出的响应为 json,说明可能失败了,不直接返回进行下载 if (res.data.type !== 'application/json') { return res.data } data = await new Response(res.data).json() } // 检查是否需要解密响应数据 const encryptHeader = ApiEncrypt.getEncryptHeader() const isEncryptResponse = res.headers[encryptHeader] === 'true' || res.headers[encryptHeader.toLowerCase()] === 'true' if (isEncryptResponse && typeof data === 'string') { try { // 解密响应数据 data = ApiEncrypt.decryptResponse(data) res.data = data; } catch (error) { console.error('响应数据解密失败:', error) throw new Error('响应数据解密失败: ' + error.message) } } const code = data.code || 200; // 获取错误信息 const msg = data.msg || errorCode[code] || errorCode['default'] if (ignoreMsgs.indexOf(msg) !== -1) { // 如果是忽略的错误码,直接返回 msg 异常 return Promise.reject(msg) } else if (code === 401) { // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了 if (!isRefreshToken) { isRefreshToken = true; // 1. 如果获取不到刷新令牌,则只能执行登出操作 if (!getRefreshToken()) { return handleAuthorized(); } // 2. 进行刷新访问令牌 try { const refreshTokenRes = await refreshToken() // 2.1 刷新成功,则回放队列的请求 + 当前请求 setToken(refreshTokenRes.data) requestList.forEach(cb => cb()) return service(res.config) } catch (e) {// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。 // 2.2 刷新失败,只回放队列的请求 requestList.forEach(cb => cb()) // 提示是否要登出。即不回放当前请求!不然会形成递归 return handleAuthorized(); } finally { requestList = [] isRefreshToken = false } } else { // 添加到队列,等待刷新获取到新的令牌 return new Promise(resolve => { requestList.push(() => { res.config.headers['Authorization'] = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改 resolve(service(res.config)) }) }) } } else if (code === 500) { Message({ message: msg, type: 'error' }) return Promise.reject(new Error(msg)) } else if (code === 501) { Message({ type: 'error', duration: 0, message: msg }) return Promise.reject(new Error(msg)) } else if (code === 901) { Message({ type: 'error', duration: 0, dangerouslyUseHTMLString: true, message: '
演示模式,无法进行写操作
' + '
 
' + '
参考 https://doc.iocoder.cn/ 教程
' + '
 
' + '
5 分钟搭建本地环境
', }) return Promise.reject(new Error(msg)) } else if (code !== 200) { if (msg === '无效的刷新令牌') { // hard coding:忽略这个提示,直接登出 console.log(msg) } else { Notification.error({ title: msg }) } return Promise.reject('error') } else { return res.data } }, error => { console.log('err' + error) tryHideFullScreenLoading() let {message} = error; if (message === "Network Error") { message = "后端接口连接异常"; } else if (message.includes("timeout")) { message = "系统接口请求超时"; } else if (message.includes("Request failed with status code")) { message = "系统接口" + message.substr(message.length - 3) + "异常"; } Message({ message: message, type: 'error', duration: 5 * 1000 }) return Promise.reject(error) } ) export function getBaseHeader() { const headers = { 'Authorization': "Bearer " + getAccessToken(), 'tenant-id': getTenantId(), } // 如果已登录且存在访问租户ID,添加 visit-tenant-id 请求头 const visitTenantId = getVisitTenantId() if (getAccessToken() && visitTenantId) { headers['visit-tenant-id'] = visitTenantId } return headers } function handleAuthorized() { if (!isRelogin.show) { isRelogin.show = true; MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' } ).then(() => { isRelogin.show = false; store.dispatch('LogOut').then(() => { location.href = getPath('/index'); }) }).catch(() => { isRelogin.show = false; }); } return Promise.reject('无效的会话,或者会话已过期,请重新登录。') } export default service