2024-06-02 20:50:51 +08:00
|
|
|
|
/**
|
|
|
|
|
* 该文件可自行根据业务逻辑进行调整
|
|
|
|
|
*/
|
2024-09-25 23:09:48 +08:00
|
|
|
|
import type { HttpResponse } from '@vben/request';
|
|
|
|
|
|
2024-07-28 14:29:05 +08:00
|
|
|
|
import { useAppConfig } from '@vben/hooks';
|
2024-08-07 08:57:56 +08:00
|
|
|
|
import { $t } from '@vben/locales';
|
2024-07-23 00:03:59 +08:00
|
|
|
|
import { preferences } from '@vben/preferences';
|
2024-08-19 22:59:42 +08:00
|
|
|
|
import {
|
|
|
|
|
authenticateResponseInterceptor,
|
|
|
|
|
errorMessageResponseInterceptor,
|
|
|
|
|
RequestClient,
|
2024-12-06 09:27:11 +08:00
|
|
|
|
stringify,
|
2024-08-19 22:59:42 +08:00
|
|
|
|
} from '@vben/request';
|
2024-07-30 21:10:28 +08:00
|
|
|
|
import { useAccessStore } from '@vben/stores';
|
2024-06-02 20:50:51 +08:00
|
|
|
|
|
2024-08-07 08:57:56 +08:00
|
|
|
|
import { message, Modal } from 'ant-design-vue';
|
|
|
|
|
import { isEmpty, isNull } from 'lodash-es';
|
2024-06-02 20:50:51 +08:00
|
|
|
|
|
2024-07-30 21:10:28 +08:00
|
|
|
|
import { useAuthStore } from '#/store';
|
2024-08-07 08:57:56 +08:00
|
|
|
|
import {
|
|
|
|
|
decryptBase64,
|
|
|
|
|
decryptWithAes,
|
|
|
|
|
encryptBase64,
|
|
|
|
|
encryptWithAes,
|
|
|
|
|
generateAesKey,
|
|
|
|
|
} from '#/utils/encryption/crypto';
|
|
|
|
|
import * as encryptUtil from '#/utils/encryption/jsencrypt';
|
2024-06-02 20:50:51 +08:00
|
|
|
|
|
2024-08-07 08:57:56 +08:00
|
|
|
|
const { apiURL, clientId, enableEncrypt } = useAppConfig(
|
|
|
|
|
import.meta.env,
|
|
|
|
|
import.meta.env.PROD,
|
|
|
|
|
);
|
|
|
|
|
|
2024-10-12 11:29:06 +08:00
|
|
|
|
/**
|
|
|
|
|
* 是否已经处在登出过程中了 一个标志位
|
|
|
|
|
* 主要是防止一个页面会请求多个api 都401 会导致登出执行多次
|
|
|
|
|
*/
|
|
|
|
|
let isLogoutProcessing = false;
|
2024-07-28 14:29:05 +08:00
|
|
|
|
|
|
|
|
|
function createRequestClient(baseURL: string) {
|
2024-06-02 20:50:51 +08:00
|
|
|
|
const client = new RequestClient({
|
2024-08-07 08:57:56 +08:00
|
|
|
|
// 后端地址
|
2024-07-28 14:29:05 +08:00
|
|
|
|
baseURL,
|
2024-08-07 08:57:56 +08:00
|
|
|
|
// 消息提示类型
|
|
|
|
|
errorMessageMode: 'message',
|
2024-10-18 15:02:39 +08:00
|
|
|
|
// 是否返回原生响应 比如:需要获取响应头时使用该属性
|
2024-08-07 08:57:56 +08:00
|
|
|
|
isReturnNativeResponse: false,
|
|
|
|
|
// 需要对返回数据进行处理
|
|
|
|
|
isTransformResponse: true,
|
2024-08-19 22:59:42 +08:00
|
|
|
|
});
|
2024-07-11 20:11:11 +08:00
|
|
|
|
|
2024-08-19 22:59:42 +08:00
|
|
|
|
/**
|
|
|
|
|
* 重新认证逻辑
|
|
|
|
|
*/
|
|
|
|
|
async function doReAuthenticate() {
|
|
|
|
|
console.warn('Access token or refresh token is invalid or expired. ');
|
|
|
|
|
const accessStore = useAccessStore();
|
|
|
|
|
const authStore = useAuthStore();
|
|
|
|
|
accessStore.setAccessToken(null);
|
2024-08-20 22:33:25 +08:00
|
|
|
|
if (
|
|
|
|
|
preferences.app.loginExpiredMode === 'modal' &&
|
|
|
|
|
accessStore.isAccessChecked
|
|
|
|
|
) {
|
2024-08-19 22:59:42 +08:00
|
|
|
|
accessStore.setLoginExpired(true);
|
|
|
|
|
} else {
|
|
|
|
|
await authStore.logout();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-28 14:29:05 +08:00
|
|
|
|
|
2024-08-19 22:59:42 +08:00
|
|
|
|
/**
|
|
|
|
|
* 刷新token逻辑
|
|
|
|
|
*/
|
|
|
|
|
async function doRefreshToken() {
|
2024-08-20 08:44:26 +08:00
|
|
|
|
// 不需要
|
|
|
|
|
// 保留此方法只是为了合并方便
|
|
|
|
|
return '';
|
2024-08-19 22:59:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatToken(token: null | string) {
|
|
|
|
|
return token ? `Bearer ${token}` : null;
|
|
|
|
|
}
|
2024-07-28 14:29:05 +08:00
|
|
|
|
|
2024-08-19 22:59:42 +08:00
|
|
|
|
client.addRequestInterceptor({
|
2024-08-20 08:44:26 +08:00
|
|
|
|
fulfilled: (config) => {
|
2024-08-19 22:59:42 +08:00
|
|
|
|
const accessStore = useAccessStore();
|
2024-08-20 08:44:26 +08:00
|
|
|
|
// 添加token
|
2024-08-19 22:59:42 +08:00
|
|
|
|
config.headers.Authorization = formatToken(accessStore.accessToken);
|
2024-08-07 08:57:56 +08:00
|
|
|
|
/**
|
|
|
|
|
* locale跟后台不一致 需要转换
|
|
|
|
|
*/
|
|
|
|
|
const language = preferences.app.locale.replace('-', '_');
|
2024-08-20 08:44:26 +08:00
|
|
|
|
config.headers['Accept-Language'] = language;
|
2024-11-23 17:56:03 +08:00
|
|
|
|
config.headers['Content-Language'] = language;
|
2024-10-18 15:02:39 +08:00
|
|
|
|
// 添加全局clientId
|
2024-08-20 08:44:26 +08:00
|
|
|
|
config.headers.clientId = clientId;
|
2024-08-07 08:57:56 +08:00
|
|
|
|
|
2024-12-06 09:27:11 +08:00
|
|
|
|
/**
|
|
|
|
|
* 格式化get/delete参数
|
|
|
|
|
* 如果包含自定义的paramsSerializer则不走此逻辑
|
|
|
|
|
*/
|
|
|
|
|
if (
|
|
|
|
|
['DELETE', 'GET'].includes(config.method?.toUpperCase() || '') &&
|
|
|
|
|
config.params &&
|
|
|
|
|
!config.paramsSerializer
|
|
|
|
|
) {
|
|
|
|
|
/**
|
|
|
|
|
* 1. 格式化参数 微服务在传递区间时间选择(后端的params Map类型参数)需要格式化key 否则接收不到
|
|
|
|
|
* 2. 数组参数需要格式化 后端才能正常接收 会变成arr=1&arr=2&arr=3的格式来接收
|
|
|
|
|
*/
|
|
|
|
|
config.paramsSerializer = (params) =>
|
|
|
|
|
stringify(params, { arrayFormat: 'repeat' });
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-25 09:34:21 +08:00
|
|
|
|
const { encrypt } = config;
|
2024-10-18 15:02:39 +08:00
|
|
|
|
// 全局开启请求加密功能 && 该请求开启 && 是post/put请求
|
2024-08-20 08:44:26 +08:00
|
|
|
|
if (
|
|
|
|
|
enableEncrypt &&
|
|
|
|
|
encrypt &&
|
|
|
|
|
['POST', 'PUT'].includes(config.method?.toUpperCase() || '')
|
|
|
|
|
) {
|
|
|
|
|
const aesKey = generateAesKey();
|
|
|
|
|
config.headers['encrypt-key'] = encryptUtil.encrypt(
|
|
|
|
|
encryptBase64(aesKey),
|
|
|
|
|
);
|
2024-08-07 08:57:56 +08:00
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
config.data =
|
|
|
|
|
typeof config.data === 'object'
|
|
|
|
|
? encryptWithAes(JSON.stringify(config.data), aesKey)
|
|
|
|
|
: encryptWithAes(config.data, aesKey);
|
|
|
|
|
}
|
2024-08-19 22:59:42 +08:00
|
|
|
|
return config;
|
2024-07-28 14:29:05 +08:00
|
|
|
|
},
|
2024-08-07 08:57:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
2024-08-20 10:48:51 +08:00
|
|
|
|
// 通用的错误处理, 如果没有进入上面的错误处理逻辑,就会进入这里
|
|
|
|
|
// 主要处理http状态码不为200的情况 必须放在在下面的响应拦截器之前
|
|
|
|
|
client.addResponseInterceptor(
|
|
|
|
|
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
|
|
|
|
|
);
|
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
client.addResponseInterceptor<HttpResponse>({
|
2025-01-17 10:48:52 +08:00
|
|
|
|
fulfilled: async (response) => {
|
2024-08-20 08:44:26 +08:00
|
|
|
|
const encryptKey = (response.headers || {})['encrypt-key'];
|
|
|
|
|
if (encryptKey) {
|
|
|
|
|
/** RSA私钥解密 拿到解密秘钥的base64 */
|
|
|
|
|
const base64Str = encryptUtil.decrypt(encryptKey);
|
|
|
|
|
/** base64 解码 得到请求头的 AES 秘钥 */
|
|
|
|
|
const aesSecret = decryptBase64(base64Str.toString());
|
|
|
|
|
/** 使用aesKey解密 responseData */
|
|
|
|
|
const decryptData = decryptWithAes(
|
|
|
|
|
response.data as unknown as string,
|
|
|
|
|
aesSecret,
|
|
|
|
|
);
|
|
|
|
|
/** 赋值 需要转为对象 */
|
|
|
|
|
response.data = JSON.parse(decryptData);
|
|
|
|
|
}
|
2024-08-07 08:57:56 +08:00
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
const { isReturnNativeResponse, isTransformResponse } = response.config;
|
2024-10-18 15:02:39 +08:00
|
|
|
|
// 是否返回原生响应 比如:需要获取响应时使用该属性
|
2024-08-20 08:44:26 +08:00
|
|
|
|
if (isReturnNativeResponse) {
|
|
|
|
|
return response;
|
2024-08-19 22:59:42 +08:00
|
|
|
|
}
|
2024-08-20 08:44:26 +08:00
|
|
|
|
// 不进行任何处理,直接返回
|
|
|
|
|
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
|
|
|
|
if (!isTransformResponse) {
|
2025-01-17 10:48:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 需要判断下载二进制的情况 正常是返回二进制 报错会返回json
|
|
|
|
|
* 当type为blob且content-type为application/json时 则判断已经下载出错
|
|
|
|
|
* 不能直接去判断blob的类型 因为下载json类型和报错的类型是一致的 需要从响应头判断
|
|
|
|
|
*/
|
|
|
|
|
if (
|
|
|
|
|
response.config.responseType === 'blob' &&
|
|
|
|
|
response.headers['content-type']?.includes?.('application/json')
|
|
|
|
|
) {
|
|
|
|
|
// 这时候的data为blob类型
|
|
|
|
|
const blob = response.data as unknown as Blob;
|
|
|
|
|
// 拿到字符串转json对象
|
|
|
|
|
response.data = JSON.parse(await blob.text());
|
|
|
|
|
// 然后按正常逻辑执行下面的代码(判断业务状态码)
|
|
|
|
|
} else {
|
|
|
|
|
// 不为blob 直接返回
|
|
|
|
|
return response.data;
|
|
|
|
|
}
|
2024-08-20 08:44:26 +08:00
|
|
|
|
}
|
2024-08-07 08:57:56 +08:00
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
const axiosResponseData = response.data;
|
|
|
|
|
if (!axiosResponseData) {
|
|
|
|
|
throw new Error($t('fallback.http.apiRequestFailed'));
|
|
|
|
|
}
|
2024-08-07 08:57:56 +08:00
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
// ruoyi-plus没有采用严格的{code, msg, data}模式
|
|
|
|
|
const { code, data, msg, ...other } = axiosResponseData;
|
2024-08-07 08:57:56 +08:00
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
// 这里逻辑可以根据项目进行修改
|
|
|
|
|
const hasSuccess = Reflect.has(axiosResponseData, 'code') && code === 200;
|
|
|
|
|
if (hasSuccess) {
|
|
|
|
|
let successMsg = msg;
|
2024-08-07 08:57:56 +08:00
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
if (isNull(successMsg) || isEmpty(successMsg)) {
|
|
|
|
|
successMsg = $t(`fallback.http.operationSuccess`);
|
|
|
|
|
}
|
2024-06-02 20:50:51 +08:00
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
if (response.config.successMessageMode === 'modal') {
|
|
|
|
|
Modal.success({
|
|
|
|
|
content: successMsg,
|
|
|
|
|
title: $t('fallback.http.successTip'),
|
|
|
|
|
});
|
|
|
|
|
} else if (response.config.successMessageMode === 'message') {
|
|
|
|
|
message.success(successMsg);
|
|
|
|
|
}
|
|
|
|
|
// 如果有data 直接返回data 没有data将剩余参数(...other)封装为data返回
|
|
|
|
|
// 需要考虑data为null的情况(比如查询为空)
|
|
|
|
|
if (data !== undefined) {
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
return other;
|
2024-08-07 08:57:56 +08:00
|
|
|
|
}
|
2024-08-20 08:44:26 +08:00
|
|
|
|
// 在此处根据自己项目的实际情况对不同的code执行不同的操作
|
|
|
|
|
// 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
|
|
|
|
|
let timeoutMsg = '';
|
|
|
|
|
switch (code) {
|
|
|
|
|
case 401: {
|
2024-10-12 11:29:06 +08:00
|
|
|
|
// 已经在登出过程中 不再执行
|
|
|
|
|
if (isLogoutProcessing) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
isLogoutProcessing = true;
|
2024-08-20 08:44:26 +08:00
|
|
|
|
const _msg = '登录超时, 请重新登录';
|
|
|
|
|
const userStore = useAuthStore();
|
2024-10-12 11:29:06 +08:00
|
|
|
|
userStore.logout().finally(() => {
|
|
|
|
|
message.error(_msg);
|
|
|
|
|
isLogoutProcessing = false;
|
2024-08-20 08:44:26 +08:00
|
|
|
|
});
|
|
|
|
|
// 不再执行下面逻辑
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
default: {
|
|
|
|
|
if (msg) {
|
|
|
|
|
timeoutMsg = msg;
|
2024-08-07 08:57:56 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
// errorMessageMode='modal'的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
|
|
|
|
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
|
|
|
|
|
if (response.config.errorMessageMode === 'modal') {
|
|
|
|
|
Modal.error({
|
|
|
|
|
content: timeoutMsg,
|
|
|
|
|
title: $t('fallback.http.errorTip'),
|
|
|
|
|
});
|
|
|
|
|
} else if (response.config.errorMessageMode === 'message') {
|
|
|
|
|
message.error(timeoutMsg);
|
|
|
|
|
}
|
2024-08-07 08:57:56 +08:00
|
|
|
|
|
2024-08-20 08:44:26 +08:00
|
|
|
|
throw new Error(timeoutMsg || $t('fallback.http.apiRequestFailed'));
|
2024-08-19 22:59:42 +08:00
|
|
|
|
},
|
2024-07-11 20:11:11 +08:00
|
|
|
|
});
|
2024-08-19 22:59:42 +08:00
|
|
|
|
|
|
|
|
|
// token过期的处理
|
|
|
|
|
client.addResponseInterceptor(
|
|
|
|
|
authenticateResponseInterceptor({
|
|
|
|
|
client,
|
|
|
|
|
doReAuthenticate,
|
|
|
|
|
doRefreshToken,
|
|
|
|
|
enableRefreshToken: preferences.app.enableRefreshToken,
|
|
|
|
|
formatToken,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
2024-07-11 20:11:11 +08:00
|
|
|
|
return client;
|
2024-06-02 20:50:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-28 14:29:05 +08:00
|
|
|
|
export const requestClient = createRequestClient(apiURL);
|
2024-08-19 22:59:42 +08:00
|
|
|
|
|
|
|
|
|
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|