import axios from 'axios' import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus' import { getToken } from '@/utils/auth' import errorCode from '@/utils/errorCode' import { tansParams, blobValidate } from '@/utils/shierfeng' import cache from '@/plugins/cache' import { saveAs } from 'file-saver' import useUserStore from '@/store/modules/user' import { applyTestBatchHeader, getStoredTestBatchId, recoverUnavailableTestBatch } from '@/utils/testBatchContext' import { decryptTransportErrorResponse, decryptTransportResponse, encryptTransportRequest, invalidateTransportKeyMeta, resetTransportRequestConfig, shouldRetryTransportWithFreshKey } from '@/utils/transportCrypto' let downloadLoadingInstance; // 是否显示重新登录 export let isRelogin = { show: false }; async function rejectUnavailableTestBatch(data) { const message = data.msg || '当前测试批次不可用,已切回正式数据模式' await recoverUnavailableTestBatch({ loadStore: async () => { const { default: useTestBatchStore } = await import('@/store/modules/testBatch') return useTestBatchStore() }, notify: recoveryMessage => ElMessage({ message: recoveryMessage, type: 'error', duration: 5 * 1000 }), message }) const error = new Error(message) error.reason = data.reason return Promise.reject(error) } function isTestBatchUnavailable(data) { return data?.reason === 'TEST_BATCH_UNAVAILABLE' } axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' // 创建axios实例 const service = axios.create({ // axios中请求配置有baseURL选项,表示请求URL公共部分 baseURL: import.meta.env.VITE_APP_BASE_API, // 超时 timeout: 10000 }) /** * 统一处理请求发送前的公共逻辑。 * * @param {Object} config Axios 请求配置 * @returns {Promise} 最终发送的请求配置 */ // request拦截器 service.interceptors.request.use(async config => { // 是否需要设置 token const isToken = (config.headers || {}).isToken === false // 是否需要防止数据重复提交 const isRepeatSubmit = (config.headers || {}).repeatSubmit === false // 间隔时间(ms),小于此时间视为重复提交 const interval = (config.headers || {}).interval || 1000 if (getToken() && !isToken) { config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } applyTestBatchHeader(config, getToken(), getStoredTestBatchId()) if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { const requestObj = { url: config.url, data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, time: new Date().getTime() } const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小 const limitSize = 5 * 1024 * 1024; // 限制存放数据5M if (requestSize >= limitSize) { console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。') return config; } const sessionObj = cache.session.getJSON('sessionObj') if (sessionObj === undefined || sessionObj === null || sessionObj === '') { cache.session.setJSON('sessionObj', requestObj) } else { const s_url = sessionObj.url; // 请求地址 const s_data = sessionObj.data; // 请求数据 const s_time = sessionObj.time; // 请求时间 if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { const message = '数据正在处理,请勿重复提交'; console.warn(`[${s_url}]: ` + message) return Promise.reject(new Error(message)) } else { cache.session.setJSON('sessionObj', requestObj) } } } // 在参数拼接前完成传输层加密,避免明文查询串提前写入 URL。 config = await encryptTransportRequest(config) // get请求映射params参数 if (config.method === 'get' && config.params) { let url = config.url + '?' + tansParams(config.params); url = url.slice(0, -1); config.params = {}; config.url = url; } return config }, error => { console.log(error) return Promise.reject(error) }) /** * 统一处理响应成功场景下的解密与业务状态码判断。 * * @param {Object} res Axios 响应对象 * @returns {Promise} 业务响应数据 */ // 响应拦截器 service.interceptors.response.use(async res => { // 响应若命中了传输层加密,这里先还原为原始业务 JSON。 res = await decryptTransportResponse(res) if (isTestBatchUnavailable(res.data)) { return rejectUnavailableTestBatch(res.data) } // 未设置状态码则默认成功状态 const code = res.data.code || 200; // 获取错误信息 const msg = errorCode[code] || res.data.msg || errorCode['default'] // 二进制数据则直接返回 if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') { return res.data } if (code === 401) { if (!isRelogin.show) { isRelogin.show = true; ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { isRelogin.show = false; useUserStore().logOut().then(() => { location.href = '/index'; }) }).catch(() => { isRelogin.show = false; }); } return Promise.reject('无效的会话,或者会话已过期,请重新登录。') } else if (code === 500) { ElMessage({ message: msg, type: 'error' }) const error = new Error(msg) error.response = res return Promise.reject(error) } else if (code === 601) { ElMessage({ message: msg, type: 'warning' }) return Promise.reject(new Error(msg)) } else if (code !== 200) { ElNotification.error({ title: msg }) return Promise.reject('error') } else { return Promise.resolve(res.data) } }, async error => { // 错误响应也可能是加密信封,先尝试解密再进入统一错误提示流程。 error = await decryptTransportErrorResponse(error) if (isTestBatchUnavailable(error.response?.data)) { return rejectUnavailableTestBatch(error.response.data) } // 若后端提示密钥失效,则清空本地公钥缓存并基于原始请求重试一次。 if (shouldRetryTransportWithFreshKey(error) && error.config && !error.config.__transportRetried) { invalidateTransportKeyMeta() error.config.__transportRetried = true error.config.headers = error.config.headers || {} error.config.headers.repeatSubmit = false resetTransportRequestConfig(error.config) return service.request(error.config) } console.log('err' + error) const response = error.response const responseStatus = response?.status const responseCode = response?.data?.code const responseMsg = response?.data?.msg if (responseMsg) { const messageType = responseStatus === 429 || responseCode === 429 ? 'warning' : 'error' ElMessage({ message: responseMsg, type: messageType, duration: 5 * 1000 }) return Promise.reject(new Error(responseMsg)) } 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.slice(-3) + "异常"; } ElMessage({ message: message, type: 'error', duration: 5 * 1000 }) return Promise.reject(error) } ) /** * 通用文件下载方法。 * * @param {string} url 下载接口地址 * @param {*} params 请求参数 * @param {string} filename 下载文件名 * @param {Object} config 额外请求配置 * @returns {Promise} */ // 通用下载方法 export function download(url, params, filename, config) { downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", }) return service.post(url, params, { transformRequest: [(params) => { return tansParams(params) }], headers: { 'Content-Type': 'application/x-www-form-urlencoded', encrypt: false, encryptResponse: false }, responseType: 'blob', ...config }).then(async (data) => { const isBlob = blobValidate(data); if (isBlob) { const blob = new Blob([data]) saveAs(blob, filename) } else { const resText = await data.text(); const rspObj = JSON.parse(resText); const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] ElMessage.error(errMsg); } downloadLoadingInstance.close(); }).catch((r) => { console.error(r) ElMessage.error('下载文件出现错误,请联系管理员!') downloadLoadingInstance.close(); }) } export default service