| | |
| | | import type { AxiosInstance, AxiosRequestConfig } from 'axios'; |
| | | import type { AxiosInstance, AxiosRequestConfig, CreateAxiosDefaults } from 'axios'; |
| | | import axios from 'axios'; |
| | | import { ElMessage } from 'element-plus'; |
| | | import { NO_AUTH_API_LIST } from '../api/ai/chat'; |
| | | import { LOGIN_URL, TEL_LOGIN_URL } from '../api/ai/user'; |
| | | import { Logger } from '../model/logger/Logger'; |
| | | import emitter from './mitt'; |
| | | import { debounce, decodeFormData } from './util'; |
| | | import { AUTH_URL, MAIN_URL, SECONDARY_URL } from '/@/constants'; |
| | | import { Local, Session } from '/@/utils/storage'; |
| | | import { Local, LoginInfo, Session } from '/@/utils/storage'; |
| | | // import JSONbig from 'json-bigint'; |
| | | |
| | | //#region ====================== 后端 res.Code ====================== |
| | | // // 摘要: |
| | | // // 成功 |
| | | // Success = 0, |
| | | // // |
| | | // // 摘要: |
| | | // // 确认(权限验证使用) |
| | | // Confirm = -1, |
| | | // // |
| | | // // 摘要: |
| | | // // 提示(验证失败后使用) |
| | | // Prompt = -2, |
| | | // // |
| | | // // 摘要: |
| | | // // 警告(业务异常使用) |
| | | // Alert = -3, |
| | | // // |
| | | // // 摘要: |
| | | // // 错误(未捕获系统异常使用) |
| | | // Error = -4, |
| | | // // |
| | | // // 摘要: |
| | | // // 超时(暂不使用) |
| | | // TimeOut = -5 |
| | | //#region ====================== 后端 res.err_code ====================== |
| | | export const enum ErrorCode { |
| | | /** @description 权限验证失败 */ |
| | | Message = 'MESSAGE', |
| | | /** @description 内部错误 */ |
| | | Exception = 'EXCEPTION', |
| | | /** @description 无权使用 */ |
| | | Auth = 'AUTH', |
| | | } |
| | | //#endregion |
| | | const initRequestInterceptor = (request: AxiosInstance,isAuth=false) => { |
| | | |
| | | const emitNoAuth = () => { |
| | | emitter.emit('logout'); |
| | | emitter.emit('openLoginDlg'); |
| | | }; |
| | | |
| | | export const handleNormalAuth = () => { |
| | | const accessSession = Local.get(accessSessionKey); |
| | | if (!accessSession) { |
| | | emitter.emit('logout'); |
| | | emitter.emit('openLoginDlg'); |
| | | } |
| | | return !!accessSession; |
| | | }; |
| | | |
| | | export const handleNoAuth = debounce(() => { |
| | | emitNoAuth(); |
| | | }); |
| | | const initRequestInterceptor = (request: AxiosInstance) => { |
| | | // 添加请求拦截器 |
| | | request.interceptors.request.use( |
| | | (config) => { |
| | | // // 在发送请求之前做些什么 token |
| | | // if (Local.get(accessTokenKey)) { |
| | | // (<any>config.headers).common['Authorization'] = `${Session.Local('token')}`; |
| | | // } |
| | | |
| | | // 获取本地的 token |
| | | const accessToken = Local.get(accessTokenKey); |
| | | if (accessToken) { |
| | | // 将 token 添加到请求报文头中‘ |
| | | if(isAuth){ |
| | | config.headers!['Authorization'] = `Bearer ${accessToken}`; |
| | | |
| | | }else{ |
| | | config.headers['Referrer-Policy'] = undefined; |
| | | |
| | | } |
| | | config.headers['hswatersession'] = Local.get('hswatersession'); |
| | | |
| | | // 判断 accessToken 是否过期 |
| | | const jwt: any = decryptJWT(accessToken); |
| | | const exp = getJWTDate(jwt.exp as number); |
| | | const isExpired = new Date() >= exp; |
| | | // token 已经过期 |
| | | if (isExpired) { |
| | | // 获取刷新 token |
| | | const refreshAccessToken = Local.get(refreshAccessTokenKey); |
| | | |
| | | // 携带刷新 token |
| | | if (refreshAccessToken) { |
| | | config.headers!['X-Authorization'] = `Bearer ${refreshAccessToken}`; |
| | | } |
| | | } |
| | | // get请求映射params参数 |
| | | if (config.method?.toLowerCase() === 'get' && config.data) { |
| | | let url = config.url + '?' + tansParams(config.data); |
| | | url = url.slice(0, -1); |
| | | config.data = {}; |
| | | config.url = url; |
| | | const accessSession = Local.get(accessSessionKey); |
| | | if (accessSession) { |
| | | // 将 token 添加到请求报文头中 |
| | | config.headers['hswatersession'] = accessSession; |
| | | } |
| | | if (!NO_AUTH_API_LIST.includes(config.url)) { |
| | | if (!accessSession && config.url !== LOGIN_URL && config.url !== TEL_LOGIN_URL) { |
| | | handleNoAuth(config.url); |
| | | throw '权限验证失败'; |
| | | } |
| | | } |
| | | |
| | | return config; |
| | | }, |
| | | (error) => { |
| | |
| | | (res) => { |
| | | // 获取状态码和返回数据 |
| | | const status = res.status; |
| | | const serve = res.data; |
| | | // code 为 -1 就是权限验证失败 |
| | | if (serve?.code === -1) { |
| | | clearAccessTokens(); |
| | | window.location.reload(); |
| | | const serveData = res.data; |
| | | if (!serveData) { |
| | | ElMessage.error('请求失败'); |
| | | throw new Error('请求失败'); |
| | | } |
| | | |
| | | // 处理 401 |
| | | if (status === 401) { |
| | | clearAccessTokens(); |
| | |
| | | if (status >= 400) { |
| | | throw new Error(res.statusText || 'Request Error.'); |
| | | } |
| | | // 处理规范化结果错误 |
| | | if (serve && serve.hasOwnProperty('errors') && serve.errors) { |
| | | throw new Error(JSON.stringify(serve.errors || 'Request Error.')); |
| | | } |
| | | // 读取响应报文头 token 信息 |
| | | |
| | | // 只能叫 access-token |
| | | const accessToken = res.headers['access-token']; |
| | | // 只能叫 x-access-token' |
| | | const refreshAccessToken = res.headers['x-access-token']; |
| | | |
| | | // 判断是否是无效 token |
| | | if (accessToken === 'invalid_token') { |
| | | // ElMessage.error('登录失效'); |
| | | clearAccessTokens(); |
| | | window.location.reload(); |
| | | } |
| | | // 判断是否存在刷新 token,如果存在则存储在本地 |
| | | else if (refreshAccessToken && accessToken && accessToken !== 'invalid_token') { |
| | | Local.set(accessTokenKey, accessToken); |
| | | Local.set(refreshAccessTokenKey, refreshAccessToken); |
| | | } |
| | | |
| | | if (!serve.json_ok && !isAuth) { |
| | | // ElMessage.warning(serve.json_msg) |
| | | throw new Error('响应错误'); |
| | | } |
| | | |
| | | // 响应拦截及自定义处理 |
| | | if (serve.data === 401) { |
| | | clearAccessTokens(); |
| | | } else if (serve.code === undefined) { |
| | | return Promise.resolve(res.data); |
| | | // return res.data; |
| | | } else if (serve.code !== 200) { |
| | | const message = JSON.stringify(serve.message); |
| | | |
| | | ElMessage.error(message); |
| | | throw new Error(message); |
| | | if (!serveData.json_ok) { |
| | | switch (serveData?.err_code) { |
| | | case ErrorCode.Auth: |
| | | if (res.config.url !== LOGIN_URL && res.config.url !== TEL_LOGIN_URL) { |
| | | handleNoAuth(); |
| | | throw '权限验证失败'; |
| | | } |
| | | break; |
| | | case ErrorCode.Exception: |
| | | const param = res.config.data ? `\n 请求参数:${JSON.stringify(decodeFormData(res.config.data))}\n` : ''; |
| | | ElMessage.error('内部错误!'); |
| | | Logger.error(`${res.config.url} 响应失败${param}`, serveData?.json_msg && new Error(serveData?.json_msg)); |
| | | return res.data; |
| | | } |
| | | } |
| | | return res.data; |
| | | }, |
| | | (error) => { |
| | | if (typeof error === 'string') { |
| | | // ElMessage.error(error); |
| | | return Promise.reject(error); |
| | | } |
| | | // 处理响应错误 |
| | | if (error.response) { |
| | | if (error.response.status === 401) { |
| | | clearAccessTokens(); |
| | | } |
| | | } |
| | | |
| | | // 对响应错误做点什么 |
| | | if (error.message.indexOf('timeout') != -1) { |
| | | ElMessage.error('网络超时'); |
| | |
| | | ); |
| | | }; |
| | | // 配置新建一个 axios 实例 |
| | | const service = axios.create({ |
| | | baseURL: MAIN_URL, |
| | | timeout: 50000, |
| | | headers: { 'Content-Type': 'application/json;charset=utf-8 ' }, |
| | | const createAxiosInstance = (option: Partial<CreateAxiosDefaults<any>> = {}) => { |
| | | return axios.create({ |
| | | baseURL: MAIN_URL, |
| | | timeout: 1200000, |
| | | headers: { |
| | | 'Content-Type': 'application/x-www-form-urlencoded', |
| | | }, |
| | | ...option, |
| | | }); |
| | | }; |
| | | |
| | | const service = createAxiosInstance(); |
| | | |
| | | export const mainRequest = service; |
| | | |
| | | //#region ====================== 流响应数据 ====================== |
| | | const streamInstance = createAxiosInstance({ |
| | | adapter: 'fetch', |
| | | responseType: 'stream', |
| | | }); |
| | | const decoder = new TextDecoder(); |
| | | const readStream = async (stream: ReadableStream, cb: (value) => void): Promise<any> => { |
| | | const reader = stream.getReader(); |
| | | let lastValue = ''; |
| | | const p = new Promise(async (resolve, reject) => { |
| | | let fullValue = ''; |
| | | while (1) { |
| | | const { done, value } = await reader.read(); |
| | | if (done) { |
| | | break; |
| | | } |
| | | // const txt = decoder.decode(Uint8Array.from([...lastValue, ...value])); |
| | | const txt = decoder.decode(value); |
| | | const txtArr = txt.split('\n'); |
| | | txtArr[0] = lastValue + txtArr[0]; |
| | | txtArr.forEach((value, index, array) => { |
| | | // 一般不会出现连续换行,只可能最后一个是换行 |
| | | if (index === array.length - 1) { |
| | | lastValue = value; |
| | | } else { |
| | | const decodeValue = decodeURIComponent(value); |
| | | fullValue += decodeValue; |
| | | cb(decodeValue); |
| | | } |
| | | }); |
| | | } |
| | | resolve(fullValue); |
| | | }); |
| | | return p; |
| | | }; |
| | | |
| | | const authService = axios.create({ |
| | | // baseURL: MAIN_URL, |
| | | timeout: 50000, |
| | | headers: { 'Content-Type': 'application/json;charset=utf-8 ' }, |
| | | export const streamReq = async (config: AxiosRequestConfig<any>, callback: (value) => void) => { |
| | | const response = await streamInstance(config); |
| | | const stream = response as unknown as ReadableStream; |
| | | return readStream(stream, (value) => { |
| | | const jsonValue = JSON.parse(value); |
| | | callback(jsonValue); |
| | | }); |
| | | }; |
| | | //#endregion |
| | | |
| | | }); |
| | | initRequestInterceptor(service); |
| | | initRequestInterceptor(authService,true) |
| | | initRequestInterceptor(streamInstance); |
| | | |
| | | export function secondaryRequest(config: AxiosRequestConfig<any>) { |
| | | return service({ |
| | |
| | | * 用于访问登录接口 |
| | | */ |
| | | export function authRequest(config: AxiosRequestConfig<any>) { |
| | | return authService({ |
| | | return service({ |
| | | ...config, |
| | | baseURL: AUTH_URL, |
| | | }); |
| | |
| | | * @description 域名前缀 |
| | | * 防止类似于 http://sqi.beng35.com/airp 和 http://sqi.beng35.com/test 公用同一个 token 或 userInfo |
| | | */ |
| | | const subDomainName = window.location.pathname |
| | | .split('/') |
| | | .filter((item) => !!item) |
| | | .join('-'); |
| | | const domainPrefix = subDomainName ? `${subDomainName}-` : ''; |
| | | export const getDomainPrefix = (win: Window) => { |
| | | const subDomainName = win.location.pathname |
| | | .split('/') |
| | | .filter((item) => !!item) |
| | | .join('-'); |
| | | const domainPrefix = subDomainName ? `${subDomainName}-` : ''; |
| | | return domainPrefix; |
| | | }; |
| | | |
| | | // token 键定义 |
| | | export const accessTokenKey = domainPrefix + 'access-token'; |
| | | export const refreshAccessTokenKey = `x-${accessTokenKey}`; |
| | | export const sessionName = 'access-session'; |
| | | export const userName = 'userName'; |
| | | |
| | | export const getSessionKey = (win: Window) => { |
| | | return getDomainPrefix(win) + sessionName; |
| | | }; |
| | | export const getUserNameKey = (win: Window) => { |
| | | return getDomainPrefix(win) + userName; |
| | | }; |
| | | |
| | | export const accessSessionKey = getSessionKey(window); |
| | | export const userNameKey = getUserNameKey(window); |
| | | |
| | | export const refreshAccessTokenKey = `x-${accessSessionKey}`; |
| | | |
| | | // userInfo键定义 |
| | | export const userInfoKey = domainPrefix + 'userInfo'; |
| | | export const userInfoKey = getDomainPrefix(window) + 'userInfo'; |
| | | |
| | | // 获取 token |
| | | export const getToken = () => { |
| | | return Local.get(accessTokenKey); |
| | | export const getSession = () => { |
| | | return Local.get(accessSessionKey); |
| | | }; |
| | | |
| | | // 清除 token |
| | | export const clearAccessTokens = async () => { |
| | | Local.remove(accessTokenKey); |
| | | Local.remove(refreshAccessTokenKey); |
| | | // Local.remove(accessSessionKey); |
| | | LoginInfo.remove(); |
| | | // 清除用户信息(每次刷新都需要利用用户信息去请求对应权限菜单) |
| | | Local.remove(userInfoKey); |
| | | // 清除其他 |