286 lines
7.9 KiB
TypeScript
286 lines
7.9 KiB
TypeScript
/**
|
||
* 该文件可自行根据业务逻辑进行调整
|
||
*/
|
||
import type { RequestClientOptions } from '@vben/request';
|
||
|
||
import { useAppConfig } from '@vben/hooks';
|
||
import { preferences } from '@vben/preferences';
|
||
import {
|
||
authenticateResponseInterceptor,
|
||
defaultResponseInterceptor,
|
||
errorMessageResponseInterceptor,
|
||
RequestClient,
|
||
} from '@vben/request';
|
||
import { useAccessStore } from '@vben/stores';
|
||
|
||
import { message, Modal } from 'ant-design-vue';
|
||
// eslint-disable-next-line n/no-extraneous-import
|
||
import { Base64 } from 'js-base64';
|
||
|
||
import {
|
||
ENCRYPT_FIELDS,
|
||
isSensitiveField,
|
||
maskSensitiveField,
|
||
TM_TEXT_SUFFIX,
|
||
} from '#/constants/sensitive-fields';
|
||
import { useAuthStore } from '#/store';
|
||
|
||
import { showPublicAccountLock } from '#/utils/publicAccountLock';
|
||
|
||
import { refreshTokenApi } from './core';
|
||
import {downloadByData} from "#/util/tool";
|
||
|
||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||
|
||
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||
const client = new RequestClient({
|
||
...options,
|
||
baseURL,
|
||
});
|
||
|
||
/**
|
||
* 重新认证逻辑
|
||
*/
|
||
/** 防重复:并发 401 只弹一次重新登录提示 */
|
||
let authModalShowing = false;
|
||
async function doReAuthenticate() {
|
||
const accessStore = useAccessStore();
|
||
const authStore = useAuthStore();
|
||
accessStore.setAccessToken(null);
|
||
if (
|
||
preferences.app.loginExpiredMode === 'modal' &&
|
||
accessStore.isAccessChecked
|
||
) {
|
||
accessStore.setLoginExpired(true);
|
||
} else {
|
||
// 被踢/登录失效时用模态框提示,确认后再退出
|
||
if (!authModalShowing) {
|
||
authModalShowing = true;
|
||
await new Promise<void>((resolve) => {
|
||
Modal.warning({
|
||
title: '登录失效',
|
||
content: '账号需重新登录',
|
||
okText: '重新登录',
|
||
centered: true,
|
||
onOk: () => resolve(),
|
||
onCancel: () => resolve(),
|
||
});
|
||
});
|
||
authModalShowing = false;
|
||
}
|
||
await authStore.logout(true);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 刷新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;
|
||
}
|
||
|
||
// 请求头处理
|
||
client.addRequestInterceptor({
|
||
fulfilled: async (config) => {
|
||
const accessStore = useAccessStore();
|
||
|
||
config.headers.Authorization = formatToken(accessStore.accessToken);
|
||
config.headers['Accept-Language'] = preferences.app.locale;
|
||
// 加密
|
||
// 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);
|
||
}
|
||
return config;
|
||
},
|
||
});
|
||
|
||
// response数据解构
|
||
client.addResponseInterceptor<HttpResponse>({
|
||
fulfilled: (response) => {
|
||
const { data: responseData, status } = response;
|
||
|
||
// 这里吧 data 换成了 result
|
||
const { code, result } = responseData;
|
||
// 判断返回状态码
|
||
if (status >= 200 && status <= 500) {
|
||
switch (code) {
|
||
case 0: {
|
||
// 解密
|
||
// eslint-disable-next-line eqeqeq
|
||
if (result != null) {
|
||
return getRes(result);
|
||
}
|
||
return result;
|
||
}
|
||
case 401: {
|
||
response.status = 401;
|
||
// 401 错误,需要重新登录
|
||
authenticateResponseInterceptor({
|
||
client,
|
||
doReAuthenticate,
|
||
doRefreshToken,
|
||
enableRefreshToken: preferences.app.enableRefreshToken,
|
||
formatToken,
|
||
});
|
||
break;
|
||
}
|
||
case 4101: {
|
||
showPublicAccountLock(result || {});
|
||
const lockError = new Error(responseData?.message || '公账日账单已逾期');
|
||
Object.assign(lockError, { silent: true, code: 4101, response });
|
||
throw lockError;
|
||
}
|
||
case 403:
|
||
case 404:
|
||
case 500: {
|
||
throw Object.assign({}, response, { response });
|
||
}
|
||
}
|
||
}
|
||
if (response.data instanceof Blob) {
|
||
return response;
|
||
}
|
||
|
||
throw Object.assign({}, response, { response });
|
||
},
|
||
});
|
||
|
||
// token过期的处理
|
||
client.addResponseInterceptor(
|
||
authenticateResponseInterceptor({
|
||
client,
|
||
doReAuthenticate,
|
||
doRefreshToken,
|
||
enableRefreshToken: preferences.app.enableRefreshToken,
|
||
formatToken,
|
||
}),
|
||
);
|
||
|
||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||
client.addResponseInterceptor(
|
||
errorMessageResponseInterceptor((msg: string, error) => {
|
||
if (error?.silent || Number(error?.code) === 4101 || Number(error?.response?.data?.code) === 4101) {
|
||
return;
|
||
}
|
||
const responseData = error?.response?.data ?? {};
|
||
const errorMessage = responseData?.error ?? responseData?.message ?? '';
|
||
message.error(errorMessage || msg);
|
||
}),
|
||
);
|
||
|
||
return client;
|
||
}
|
||
|
||
export const requestClient = createRequestClient(apiURL, {
|
||
responseReturn: 'result',
|
||
});
|
||
|
||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||
|
||
/**
|
||
* 加解密方法
|
||
* @param obj
|
||
* @param isDecode
|
||
*/
|
||
function getRes(obj: any, isDecode = true) {
|
||
try {
|
||
if (obj == null || typeof obj !== 'object') {
|
||
return obj;
|
||
}
|
||
for (const key in obj) {
|
||
// 跳过脱敏展示字段,避免重复处理
|
||
if (key.endsWith(TM_TEXT_SUFFIX)) {
|
||
continue;
|
||
}
|
||
if (Array.isArray(obj[key])) {
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
return obj;
|
||
} catch (error) {
|
||
console.error('错误', error);
|
||
return obj;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解密
|
||
* @param data
|
||
* @param key
|
||
*/
|
||
function customBase64Decode(data: string, key = '') {
|
||
// 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) {
|
||
console.error(
|
||
`解码Base64字符串【${data}】时发生错误,KEY:【${key}】`,
|
||
error,
|
||
);
|
||
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;
|
||
}
|
||
}
|