| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- /**
- * 和服务端进行交互
- * 它封装了全局 request拦截器、respone拦截器、统一的错误处理、统一做了超时,baseURL设置等
- * <pre>
- * 作者:hugh zhuang
- * 邮箱:3378340995@qq.com
- * 日期:2015-11-02-下午3:29:34
- * 版权:广州流辰信息技术有限公司
- * </pre>
- */
- import axios from 'axios'
- import { Dialog, Toast } from 'vant'
- import { encryptByAes } from '@/utils/encrypt'
- import store from '@/store'
- import router from '@/router'
- import I18n from '@/utils/i18n' // Internationalization 国际化
- import Utils from '@/utils/util'
- import setting from '@/setting.js'
- // 验权
- import { getToken, updateToken, removeRefreshToken } from '@/utils/auth'
- import { refreshAccessToken } from '@/api/oauth2/user'
- import { showFullScreenLoading, tryHideFullScreenLoading } from './loading'
- import requestState from '@/constants/state'
- import { BASE_API, BASE_GATEWAY_API } from '@/api/baseUrl'
- import { HEADER_TOKEN_KEY, HEADER_SYSTEM_ID, HEADER_TENANT_ID, MULTIPLE_DOMAIN, API_DOMAIN_NAMES, ENCRYPT_GET_PARAMS } from '@/constant'
- const TIMEOUT = 3000 * 100 // 请求超时(timeout)时间
- /**
- * 创建axios实例
- */
- const service = axios.create({
- timeout: TIMEOUT, // request timeout
- withCredentials: true, // 跨域安全策略
- headers: {
- 'Cache-Control': 'no-cache',
- 'Content-Type': 'application/json;charset=utf-8'
- }
- })
- // 是否正在刷新的标记
- let isRefreshing = false
- // 重试队列,每一项是一个待执行的回调函数 (token拉完后会逐个调用)
- let requests = []
- // 取消请求标志
- let cancelRequest = false
- // 请求数
- let requestCount = 0
- // 临时token请求路径
- const TEMP_TOKEN_API = '/business/v3/short/apply'
- // 缓存用于获取临时token的实例
- let _tempTokenService = null
- // 获取临时token实例
- function getTempTokenService() {
- if (!_tempTokenService) {
- _tempTokenService = axios.create({
- baseURL: BASE_API(),
- timeout: 10 * 1000,
- headers: {
- 'Cache-Control': 'no-cache',
- 'Content-Type': 'application/json;charset=utf-8'
- }
- })
- }
- return _tempTokenService
- }
- /**
- * 获取临时token,有效时长5min,仅可使用一次
- */
- async function fetchTempToken() {
- const tempTokenService = getTempTokenService()
- try {
- const { data: { data = '' }} = await tempTokenService.post(TEMP_TOKEN_API) || {}
- if (data) {
- return data
- } else {
- throw new Error('获取临时 token 失败!')
- }
- } catch (err) {
- console.error('[fetchTempToken] 错误:', err)
- throw err
- }
- }
- /**
- * 请求(request)拦截器
- *
- * get请求,统一参数放在params里面,后台对应只有@RequestParam
- * `params`是即将与请求一起发送的 URL 参数,必须是一个无格式对象(plain object)或 URLSearchParams 对象
- *
- * post请求,统一参数放在data里面——json格式,后台对应@RequestBody ,其他 后台对应@RequestParam
- * `data` 是作为请求主体被发送的数据
- * 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
- * 在没有设置 `transformRequest` 时,必须是以下类型之一:
- * - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
- * - 浏览器专属:FormData, File, Blob
- * - Node 专属: Stream
- * post 请求 `params` 这个同get 但要注意 后台对应@RequestParam 请求的`Content-Type`是 application/x-www-form-urlencoded 用 qs.stringify 去构造数据
- */
- service.interceptors.request.use(async config => {
- // 如果外部已经指定了 baseURL,则不做自动切换
- if (!config.baseURL) {
- config.baseURL = BASE_API(parseInt(requestCount / 5, 10))
- if (MULTIPLE_DOMAIN) {
- // 轮询多个域名
- requestCount >= ((API_DOMAIN_NAMES.length - 1) * 5) ? requestCount = 0 : requestCount++
- }
- }
- if (config.gateway) {
- config.baseURL = BASE_GATEWAY_API()
- }
- config.isLoading = config.isLoading !== undefined && config.isLoading !== null ? config.isLoading : true
- if (config.isLoading) {
- showFullScreenLoading(config.loading)
- }
- // GET 方法做防缓存/参数加密
- if (config.method.toUpperCase() === 'GET') {
- if (ENCRYPT_GET_PARAMS) {
- config.params = {
- _p: Utils.isNotEmpty(config.params) ? encryptByAes(JSON.stringify(config.params), 'get') : undefined,
- _t: Date.parse(new Date()) / 1000
- }
- } else {
- config.params = {
- ...config.params,
- _t: Date.parse(new Date()) / 1000
- }
- }
- }
- // 判断是否需要token
- if (setting.whiteApiList.indexOf(config.url) !== -1) {
- return config
- }
- // 特殊白名单接口,需校验临时token
- if (setting.whiteApiListWithAuth.indexOf(config.url) !== -1) {
- try {
- // 获取临时token
- const tempToken = await fetchTempToken()
- // 写入请求头
- config.headers[HEADER_TOKEN_KEY] = tempToken
- } catch (err) {
- console.error('[request] 拉取临时 token 失败,接口:', config.url)
- }
- return config
- }
- config.headers[HEADER_TOKEN_KEY] = getToken()
- // 增加系统id
- if (store && store.getters.systemid) {
- config.headers[HEADER_SYSTEM_ID] = store.getters.systemid
- }
- // 租户ID
- if (store && store.getters.tenantid) {
- config.headers[HEADER_TENANT_ID] = store.getters.designTenantid ? store.getters.designTenantid : store.getters.tenantid
- }
- // 租户模式
- if (store && store.getters.isTenantOpen) {
- config.headers[HEADER_TENANT_ID] = store.getters.designTenantid ? store.getters.designTenantid : store.getters.tenantid
- }
- return config
- }, error => {
- tryHideFullScreenLoading()
- // Do something with request error
- Utils.log.error('request' + error) // for debug
- Promise.reject(error)
- })
- /**
- * 响应(respone)拦截器
- */
- service.interceptors.response.use(response => {
- tryHideFullScreenLoading()
- // 下载流直接返回
- if (response.config.responseType === 'arraybuffer') {
- return response
- }
- const dataAxios = response.data
- const { state, message, cause } = dataAxios
- // 后端接口没有返回约定的state字段,则视为异常
- if (state === undefined) {
- const msg = '接口异常,没有返回[state]参数\n' + response.config.url
- Toast({
- message: `${msg}`,
- type: 'html',
- closeOnClick: true,
- className: 'custom-toast',
- duration: 3 * 1000
- })
- return
- }
- // state为200是正确的请求 或者 验证码问题,或者警告类型的错误 自行处理
- if (state === requestState.SUCCESS ||
- state === requestState.UNSUPORT ||
- state === requestState.WARNING ||
- state === requestState.WARN) {
- return dataAxios
- }
- // 处理AccessToken过期,刷新逻辑
- if (state === requestState.TOKEN_EXPIRED) {
- const config = response.config
- if (!isRefreshing) {
- isRefreshing = true
- return refreshAccessToken().then(res => {
- const data = res.data
- updateToken(data) // 更新本地 token
- const newToken = getToken()
- config.headers[HEADER_TOKEN_KEY] = newToken
- // 刷新完成后,把队列中的所有请求重新发一次
- requests.forEach(cb => cb(newToken))
- requests = []
- return service(config) // 重试当前请求
- }).catch(err => {
- console.error('refreshtoken error =>', err)
- removeRefreshToken()
- window.location.href = '/'
- return Promise.reject(err)
- }).finally(() => {
- isRefreshing = false
- })
- } else {
- // 如果已经在刷新 token,则把当前请求挂到队列里,等待刷新完成后再触发
- return new Promise(resolve => {
- // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
- requests.push((token) => {
- config.headers[HEADER_TOKEN_KEY] = token
- resolve(service(config))
- })
- })
- }
- } else if (state === requestState.ILLEGAL_TOKEN || state === requestState.OTHER_CLIENTS) {
- // 6020201:非法的token;6020202:其他客户端登录了;6020301:Token 过期了;
- if (!cancelRequest) {
- cancelRequest = false
- Dialog.confirm({
- title: I18n.t('error.logout.title'),
- message: I18n.t('error.logout.message')
- }).then(() => {
- store.dispatch('ibps/account/fedLogout').then(() => {
- // 终止所有请求
- cancelRequest = true
- router.push({ name: 'login' })
- }).catch(() => {
- cancelRequest = false
- })
- }).catch(() => {
- cancelRequest = false
- }).finally(() => {
- cancelRequest = false
- })
- }
- return Promise.reject(new Error(message))
- } else { // 错误处理
- let errorMsg = ''
- if (Utils.isNotEmpty(message)) { // 有错误消息
- errorMsg = Utils.isNotEmpty(dataAxios.cause) ? I18n.t('error.messageCause', {
- message,
- cause: dataAxios.cause
- }) : I18n.t('error.message', {
- message
- })
- } else if (Utils.isNotEmpty(cause)) { // 只有错误原因
- errorMsg = I18n.t('error.cause', {
- cause
- })
- } else if (I18n.te('error.status.' + state)) { // 有错误编码
- errorMsg = I18n.t('error.status.' + state)
- } else { // 未知
- errorMsg = message || I18n.t('error.unknown', {
- state
- })
- }
- if (response.config.url === '/oauth2/v3/user/login/apply') {
- errorMsg = '错误原因:用户名或密码错误'
- }
- Toast({
- message: `${errorMsg}`,
- type: 'html',
- closeOnClick: true,
- className: 'custom-toast',
- duration: 3 * 1000
- })
- const err = new Error(errorMsg)
- err.state = state
- err.cause = cause
- return Promise.reject(err)
- }
- },
- // 异常处理
- error => {
- tryHideFullScreenLoading()
- console.error('request-error', error) // for debug
- console.log(error.response)
- if (error && error.response) {
- error.message = I18n.t('error.status.' + error.response.status, {
- url: error.response.config.url
- })
- error.errMsg = error.response.status + ':' + (error.response.data?.message || error.response.statusText)
- } else {
- error.state = 500
- error.message = I18n.t('error.network') // '服务器君开小差了,请稍后再试'
- }
- Toast({
- message: error.errMsg || error.message || I18n.t('error.network'),
- type: 'html',
- closeOnClick: true,
- className: 'custom-toast',
- duration: 3 * 1000
- })
- return Promise.reject(error)
- }
- )
- export default service
|