2024-06-02 20:50:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 该文件可自行根据业务逻辑进行调整
|
|
|
|
|
|
*/
|
2025-01-19 17:41:26 +08:00
|
|
|
|
import type { RequestClientOptions } from '@vben/request';
|
2024-09-25 23:09:48 +08:00
|
|
|
|
|
2024-07-28 14:29:05 +08:00
|
|
|
|
import { useAppConfig } from '@vben/hooks';
|
2024-07-23 00:03:59 +08:00
|
|
|
|
import { preferences } from '@vben/preferences';
|
2024-08-19 22:59:42 +08:00
|
|
|
|
import {
|
|
|
|
|
|
authenticateResponseInterceptor,
|
2025-01-20 18:38:49 +08:00
|
|
|
|
defaultResponseInterceptor,
|
2024-08-19 22:59:42 +08:00
|
|
|
|
errorMessageResponseInterceptor,
|
|
|
|
|
|
RequestClient,
|
|
|
|
|
|
} from '@vben/request';
|
2024-07-30 21:10:28 +08:00
|
|
|
|
import { useAccessStore } from '@vben/stores';
|
2024-06-02 20:50:51 +08:00
|
|
|
|
|
2026-08-01 08:04:31 +08:00
|
|
|
|
import { message, Modal } from 'ant-design-vue';
|
2025-04-11 17:06:02 +08:00
|
|
|
|
// eslint-disable-next-line n/no-extraneous-import
|
|
|
|
|
|
import { Base64 } from 'js-base64';
|
2024-06-02 20:50:51 +08:00
|
|
|
|
|
2026-07-09 15:45:39 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ENCRYPT_FIELDS,
|
|
|
|
|
|
isSensitiveField,
|
|
|
|
|
|
maskSensitiveField,
|
|
|
|
|
|
TM_TEXT_SUFFIX,
|
|
|
|
|
|
} from '#/constants/sensitive-fields';
|
2024-07-30 21:10:28 +08:00
|
|
|
|
import { useAuthStore } from '#/store';
|
2024-06-02 20:50:51 +08:00
|
|
|
|
|
2024-08-19 22:59:42 +08:00
|
|
|
|
import { refreshTokenApi } from './core';
|
2025-04-28 14:54:08 +08:00
|
|
|
|
import {downloadByData} from "#/util/tool";
|
2024-08-19 22:59:42 +08:00
|
|
|
|
|
2024-07-28 14:29:05 +08:00
|
|
|
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
|
|
|
|
|
|
2025-01-19 17:41:26 +08:00
|
|
|
|
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
2024-06-02 20:50:51 +08:00
|
|
|
|
const client = new RequestClient({
|
2025-01-19 17:41:26 +08:00
|
|
|
|
...options,
|
2024-07-28 14:29:05 +08:00
|
|
|
|
baseURL,
|
2024-08-19 22:59:42 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重新认证逻辑
|
|
|
|
|
|
*/
|
2026-08-01 08:04:31 +08:00
|
|
|
|
/** 防重复:并发 401 只弹一次重新登录提示 */
|
|
|
|
|
|
let authModalShowing = false;
|
2024-08-19 22:59:42 +08:00
|
|
|
|
async function doReAuthenticate() {
|
|
|
|
|
|
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 {
|
2026-08-01 08:04:31 +08:00
|
|
|
|
// 被踢/登录失效时用模态框提示,确认后再退出
|
|
|
|
|
|
if (!authModalShowing) {
|
|
|
|
|
|
authModalShowing = true;
|
|
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
|
|
|
Modal.warning({
|
|
|
|
|
|
title: '登录失效',
|
|
|
|
|
|
content: '账号需重新登录',
|
|
|
|
|
|
okText: '重新登录',
|
|
|
|
|
|
centered: true,
|
|
|
|
|
|
onOk: () => resolve(),
|
|
|
|
|
|
onCancel: () => resolve(),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
authModalShowing = false;
|
|
|
|
|
|
}
|
2025-04-11 17:06:02 +08:00
|
|
|
|
await authStore.logout(true);
|
2024-08-19 22:59:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 刷新token逻辑
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function doRefreshToken() {
|
|
|
|
|
|
const accessStore = useAccessStore();
|
|
|
|
|
|
const resp = await refreshTokenApi();
|
|
|
|
|
|
const newToken = resp.data;
|
|
|
|
|
|
accessStore.setAccessToken(newToken);
|
|
|
|
|
|
return newToken;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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({
|
|
|
|
|
|
fulfilled: async (config) => {
|
|
|
|
|
|
const accessStore = useAccessStore();
|
|
|
|
|
|
|
|
|
|
|
|
config.headers.Authorization = formatToken(accessStore.accessToken);
|
|
|
|
|
|
config.headers['Accept-Language'] = preferences.app.locale;
|
2025-03-07 08:18:45 +08:00
|
|
|
|
// 加密
|
|
|
|
|
|
// eslint-disable-next-line eqeqeq
|
|
|
|
|
|
if (config.data != null) {
|
|
|
|
|
|
config.data = getRes(config.data, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
// eslint-disable-next-line eqeqeq
|
|
|
|
|
|
if (config.params != null) {
|
|
|
|
|
|
config.params = getRes(config.params, false);
|
|
|
|
|
|
}
|
2024-08-19 22:59:42 +08:00
|
|
|
|
return config;
|
2024-07-28 14:29:05 +08:00
|
|
|
|
},
|
2024-06-02 20:50:51 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2024-08-19 22:59:42 +08:00
|
|
|
|
// response数据解构
|
2024-09-25 23:09:48 +08:00
|
|
|
|
client.addResponseInterceptor<HttpResponse>({
|
2024-08-19 22:59:42 +08:00
|
|
|
|
fulfilled: (response) => {
|
|
|
|
|
|
const { data: responseData, status } = response;
|
|
|
|
|
|
|
2025-03-07 08:18:45 +08:00
|
|
|
|
// 这里吧 data 换成了 result
|
|
|
|
|
|
const { code, result } = responseData;
|
|
|
|
|
|
// 判断返回状态码
|
2025-04-11 17:06:02 +08:00
|
|
|
|
if (status >= 200 && status <= 500) {
|
2025-03-07 08:18:45 +08:00
|
|
|
|
switch (code) {
|
|
|
|
|
|
case 0: {
|
|
|
|
|
|
// 解密
|
|
|
|
|
|
// eslint-disable-next-line eqeqeq
|
|
|
|
|
|
if (result != null) {
|
|
|
|
|
|
return getRes(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 401: {
|
2025-04-11 17:06:02 +08:00
|
|
|
|
response.status = 401;
|
2025-03-07 08:18:45 +08:00
|
|
|
|
// 401 错误,需要重新登录
|
2025-04-11 17:06:02 +08:00
|
|
|
|
authenticateResponseInterceptor({
|
|
|
|
|
|
client,
|
|
|
|
|
|
doReAuthenticate,
|
|
|
|
|
|
doRefreshToken,
|
|
|
|
|
|
enableRefreshToken: preferences.app.enableRefreshToken,
|
|
|
|
|
|
formatToken,
|
|
|
|
|
|
});
|
|
|
|
|
|
break;
|
2025-03-07 08:18:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
case 403:
|
|
|
|
|
|
case 404:
|
|
|
|
|
|
case 500: {
|
|
|
|
|
|
throw Object.assign({}, response, { response });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-05 13:06:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (response.data instanceof Blob) {
|
2025-04-28 14:54:08 +08:00
|
|
|
|
return response;
|
2024-08-19 22:59:42 +08:00
|
|
|
|
}
|
2024-10-18 22:00:41 +08:00
|
|
|
|
|
2025-05-05 13:06:10 +08:00
|
|
|
|
throw Object.assign({}, response, { response });
|
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,
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
|
|
|
|
|
client.addResponseInterceptor(
|
2024-10-09 22:08:55 +08:00
|
|
|
|
errorMessageResponseInterceptor((msg: string, error) => {
|
2024-09-23 22:59:58 +08:00
|
|
|
|
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
2024-10-09 22:08:55 +08:00
|
|
|
|
// 当前mock接口返回的错误字段是 error 或者 message
|
|
|
|
|
|
const responseData = error?.response?.data ?? {};
|
|
|
|
|
|
const errorMessage = responseData?.error ?? responseData?.message ?? '';
|
|
|
|
|
|
// 如果没有错误信息,则会根据状态码进行提示
|
|
|
|
|
|
message.error(errorMessage || msg);
|
2024-09-23 22:59:58 +08:00
|
|
|
|
}),
|
2024-08-19 22:59:42 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
2024-07-11 20:11:11 +08:00
|
|
|
|
return client;
|
2024-06-02 20:50:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-01-19 17:41:26 +08:00
|
|
|
|
export const requestClient = createRequestClient(apiURL, {
|
2026-08-10 16:55:17 +08:00
|
|
|
|
responseReturn: 'result',
|
2025-01-19 17:41:26 +08:00
|
|
|
|
});
|
2024-08-19 22:59:42 +08:00
|
|
|
|
|
|
|
|
|
|
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
2025-03-07 08:18:45 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加解密方法
|
|
|
|
|
|
* @param obj
|
|
|
|
|
|
* @param isDecode
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getRes(obj: any, isDecode = true) {
|
|
|
|
|
|
try {
|
2026-07-09 15:45:39 +08:00
|
|
|
|
if (obj == null || typeof obj !== 'object') {
|
|
|
|
|
|
return obj;
|
|
|
|
|
|
}
|
2025-03-07 08:18:45 +08:00
|
|
|
|
for (const key in obj) {
|
2026-07-09 15:45:39 +08:00
|
|
|
|
// 跳过脱敏展示字段,避免重复处理
|
|
|
|
|
|
if (key.endsWith(TM_TEXT_SUFFIX)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-03-07 08:18:45 +08:00
|
|
|
|
if (Array.isArray(obj[key])) {
|
2026-07-09 15:45:39 +08:00
|
|
|
|
obj[key] = getRes(obj[key], isDecode);
|
|
|
|
|
|
} else if (obj[key] !== null && typeof obj[key] === 'object') {
|
|
|
|
|
|
obj[key] = getRes(obj[key], isDecode);
|
|
|
|
|
|
} else if (ENCRYPT_FIELDS.has(key)) {
|
|
|
|
|
|
let plainValue = isDecode
|
|
|
|
|
|
? customBase64Decode(obj[key], key)
|
|
|
|
|
|
: customBase64Encode(obj[key]);
|
|
|
|
|
|
const isGarbled =
|
|
|
|
|
|
// eslint-disable-next-line no-control-regex
|
|
|
|
|
|
/[\u0000-\u0008\v\f\u000E-\u001F\u007F-\u00FF\u{E000}-\u{F8FF}]/u.test(
|
|
|
|
|
|
plainValue,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (isGarbled) {
|
|
|
|
|
|
plainValue = obj[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
obj[key] = plainValue;
|
|
|
|
|
|
// 解密后写入脱敏展示字段,明文保留在原字段
|
|
|
|
|
|
if (isDecode && isSensitiveField(key)) {
|
|
|
|
|
|
obj[key + TM_TEXT_SUFFIX] = maskSensitiveField(key, plainValue);
|
2025-03-07 08:18:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return obj;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('错误', error);
|
|
|
|
|
|
return obj;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解密
|
|
|
|
|
|
* @param data
|
2025-04-03 16:17:07 +08:00
|
|
|
|
* @param key
|
2025-03-07 08:18:45 +08:00
|
|
|
|
*/
|
2025-04-03 16:17:07 +08:00
|
|
|
|
function customBase64Decode(data: string, key = '') {
|
2025-03-07 08:18:45 +08:00
|
|
|
|
// return data
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 第一次Base64解码
|
|
|
|
|
|
const decodedFirst = Base64.decode(data);
|
|
|
|
|
|
// 截取从第30个字符开始往后的内容
|
|
|
|
|
|
const subStr = decodedFirst.slice(30);
|
|
|
|
|
|
// 第二次Base64解码
|
|
|
|
|
|
const base64 = Base64.decode(subStr);
|
|
|
|
|
|
return base64.length > 0 ? base64 : data;
|
|
|
|
|
|
} catch (error) {
|
2025-04-03 16:17:07 +08:00
|
|
|
|
console.error(
|
|
|
|
|
|
`解码Base64字符串【${data}】时发生错误,KEY:【${key}】`,
|
|
|
|
|
|
error,
|
|
|
|
|
|
);
|
2025-03-07 08:18:45 +08:00
|
|
|
|
return data;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加密
|
|
|
|
|
|
* @param data
|
|
|
|
|
|
*/
|
|
|
|
|
|
function customBase64Encode(data: string) {
|
|
|
|
|
|
// eslint-disable-next-line eqeqeq
|
|
|
|
|
|
if (data == null || data === '') return data;
|
|
|
|
|
|
// return data
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 和加密方法差不多但是是要生成30个随机字符
|
|
|
|
|
|
let randomString = '';
|
|
|
|
|
|
for (let i = 0; i < 30; i++) {
|
|
|
|
|
|
const randomChar = String.fromCodePoint(
|
|
|
|
|
|
Math.floor(Math.random() * (126 - 32 + 1)) + 32,
|
|
|
|
|
|
);
|
|
|
|
|
|
randomString += randomChar;
|
|
|
|
|
|
}
|
|
|
|
|
|
const encoded = Base64.encode(data);
|
|
|
|
|
|
return Base64.encode(randomString + encoded);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`加密字符串【${data}】时发生错误`, error);
|
|
|
|
|
|
return data;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|