request.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. /**
  2. * 和服务端进行交互
  3. * 它封装了全局 request拦截器、respone拦截器、统一的错误处理、统一做了超时,baseURL设置等
  4. * <pre>
  5. * 作者:hugh zhuang
  6. * 邮箱:3378340995@qq.com
  7. * 日期:2015-11-02-下午3:29:34
  8. * 版权:广州流辰信息技术有限公司
  9. * </pre>
  10. */
  11. import axios from 'axios'
  12. import { Dialog, Toast } from 'vant'
  13. import { encryptByAes } from '@/utils/encrypt'
  14. import store from '@/store'
  15. import router from '@/router'
  16. import I18n from '@/utils/i18n' // Internationalization 国际化
  17. import Utils from '@/utils/util'
  18. import setting from '@/setting.js'
  19. // 验权
  20. import { getToken, updateToken, removeRefreshToken } from '@/utils/auth'
  21. import { refreshAccessToken } from '@/api/oauth2/user'
  22. import { showFullScreenLoading, tryHideFullScreenLoading } from './loading'
  23. import requestState from '@/constants/state'
  24. import { BASE_API, BASE_GATEWAY_API } from '@/api/baseUrl'
  25. import { HEADER_TOKEN_KEY, HEADER_SYSTEM_ID, HEADER_TENANT_ID, MULTIPLE_DOMAIN, API_DOMAIN_NAMES, ENCRYPT_GET_PARAMS } from '@/constant'
  26. const TIMEOUT = 3000 * 100 // 请求超时(timeout)时间
  27. /**
  28. * 创建axios实例
  29. */
  30. const service = axios.create({
  31. timeout: TIMEOUT, // request timeout
  32. withCredentials: true, // 跨域安全策略
  33. headers: {
  34. 'Cache-Control': 'no-cache',
  35. 'Content-Type': 'application/json;charset=utf-8'
  36. }
  37. })
  38. // 是否正在刷新的标记
  39. let isRefreshing = false
  40. // 重试队列,每一项是一个待执行的回调函数 (token拉完后会逐个调用)
  41. let requests = []
  42. // 取消请求标志
  43. let cancelRequest = false
  44. // 请求数
  45. let requestCount = 0
  46. // 临时token请求路径
  47. const TEMP_TOKEN_API = '/business/v3/short/apply'
  48. // 缓存用于获取临时token的实例
  49. let _tempTokenService = null
  50. // 获取临时token实例
  51. function getTempTokenService() {
  52. if (!_tempTokenService) {
  53. _tempTokenService = axios.create({
  54. baseURL: BASE_API(),
  55. timeout: 10 * 1000,
  56. headers: {
  57. 'Cache-Control': 'no-cache',
  58. 'Content-Type': 'application/json;charset=utf-8'
  59. }
  60. })
  61. }
  62. return _tempTokenService
  63. }
  64. /**
  65. * 获取临时token,有效时长5min,仅可使用一次
  66. */
  67. async function fetchTempToken() {
  68. const tempTokenService = getTempTokenService()
  69. try {
  70. const { data: { data = '' }} = await tempTokenService.post(TEMP_TOKEN_API) || {}
  71. if (data) {
  72. return data
  73. } else {
  74. throw new Error('获取临时 token 失败!')
  75. }
  76. } catch (err) {
  77. console.error('[fetchTempToken] 错误:', err)
  78. throw err
  79. }
  80. }
  81. /**
  82. * 请求(request)拦截器
  83. *
  84. * get请求,统一参数放在params里面,后台对应只有@RequestParam
  85. * `params`是即将与请求一起发送的 URL 参数,必须是一个无格式对象(plain object)或 URLSearchParams 对象
  86. *
  87. * post请求,统一参数放在data里面——json格式,后台对应@RequestBody ,其他 后台对应@RequestParam
  88. * `data` 是作为请求主体被发送的数据
  89. * 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
  90. * 在没有设置 `transformRequest` 时,必须是以下类型之一:
  91. * - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  92. * - 浏览器专属:FormData, File, Blob
  93. * - Node 专属: Stream
  94. * post 请求 `params` 这个同get 但要注意 后台对应@RequestParam 请求的`Content-Type`是 application/x-www-form-urlencoded 用 qs.stringify 去构造数据
  95. */
  96. service.interceptors.request.use(async config => {
  97. // 如果外部已经指定了 baseURL,则不做自动切换
  98. if (!config.baseURL) {
  99. config.baseURL = BASE_API(parseInt(requestCount / 5, 10))
  100. if (MULTIPLE_DOMAIN) {
  101. // 轮询多个域名
  102. requestCount >= ((API_DOMAIN_NAMES.length - 1) * 5) ? requestCount = 0 : requestCount++
  103. }
  104. }
  105. if (config.gateway) {
  106. config.baseURL = BASE_GATEWAY_API()
  107. }
  108. config.isLoading = config.isLoading !== undefined && config.isLoading !== null ? config.isLoading : true
  109. if (config.isLoading) {
  110. showFullScreenLoading(config.loading)
  111. }
  112. // GET 方法做防缓存/参数加密
  113. if (config.method.toUpperCase() === 'GET') {
  114. if (ENCRYPT_GET_PARAMS) {
  115. config.params = {
  116. _p: Utils.isNotEmpty(config.params) ? encryptByAes(JSON.stringify(config.params), 'get') : undefined,
  117. _t: Date.parse(new Date()) / 1000
  118. }
  119. } else {
  120. config.params = {
  121. ...config.params,
  122. _t: Date.parse(new Date()) / 1000
  123. }
  124. }
  125. }
  126. // 判断是否需要token
  127. if (setting.whiteApiList.indexOf(config.url) !== -1) {
  128. return config
  129. }
  130. // 特殊白名单接口,需校验临时token
  131. if (setting.whiteApiListWithAuth.indexOf(config.url) !== -1) {
  132. try {
  133. // 获取临时token
  134. const tempToken = await fetchTempToken()
  135. // 写入请求头
  136. config.headers[HEADER_TOKEN_KEY] = tempToken
  137. } catch (err) {
  138. console.error('[request] 拉取临时 token 失败,接口:', config.url)
  139. }
  140. return config
  141. }
  142. config.headers[HEADER_TOKEN_KEY] = getToken()
  143. // 增加系统id
  144. if (store && store.getters.systemid) {
  145. config.headers[HEADER_SYSTEM_ID] = store.getters.systemid
  146. }
  147. // 租户ID
  148. if (store && store.getters.tenantid) {
  149. config.headers[HEADER_TENANT_ID] = store.getters.designTenantid ? store.getters.designTenantid : store.getters.tenantid
  150. }
  151. // 租户模式
  152. if (store && store.getters.isTenantOpen) {
  153. config.headers[HEADER_TENANT_ID] = store.getters.designTenantid ? store.getters.designTenantid : store.getters.tenantid
  154. }
  155. return config
  156. }, error => {
  157. tryHideFullScreenLoading()
  158. // Do something with request error
  159. Utils.log.error('request' + error) // for debug
  160. Promise.reject(error)
  161. })
  162. /**
  163. * 响应(respone)拦截器
  164. */
  165. service.interceptors.response.use(response => {
  166. tryHideFullScreenLoading()
  167. // 下载流直接返回
  168. if (response.config.responseType === 'arraybuffer') {
  169. return response
  170. }
  171. const dataAxios = response.data
  172. const { state, message, cause } = dataAxios
  173. // 后端接口没有返回约定的state字段,则视为异常
  174. if (state === undefined) {
  175. const msg = '接口异常,没有返回[state]参数\n' + response.config.url
  176. Toast({
  177. message: `${msg}`,
  178. type: 'html',
  179. closeOnClick: true,
  180. className: 'custom-toast',
  181. duration: 3 * 1000
  182. })
  183. return
  184. }
  185. // state为200是正确的请求 或者 验证码问题,或者警告类型的错误 自行处理
  186. if (state === requestState.SUCCESS ||
  187. state === requestState.UNSUPORT ||
  188. state === requestState.WARNING ||
  189. state === requestState.WARN) {
  190. return dataAxios
  191. }
  192. // 处理AccessToken过期,刷新逻辑
  193. if (state === requestState.TOKEN_EXPIRED) {
  194. const config = response.config
  195. if (!isRefreshing) {
  196. isRefreshing = true
  197. return refreshAccessToken().then(res => {
  198. const data = res.data
  199. updateToken(data) // 更新本地 token
  200. const newToken = getToken()
  201. config.headers[HEADER_TOKEN_KEY] = newToken
  202. // 刷新完成后,把队列中的所有请求重新发一次
  203. requests.forEach(cb => cb(newToken))
  204. requests = []
  205. return service(config) // 重试当前请求
  206. }).catch(err => {
  207. console.error('refreshtoken error =>', err)
  208. removeRefreshToken()
  209. window.location.href = '/'
  210. return Promise.reject(err)
  211. }).finally(() => {
  212. isRefreshing = false
  213. })
  214. } else {
  215. // 如果已经在刷新 token,则把当前请求挂到队列里,等待刷新完成后再触发
  216. return new Promise(resolve => {
  217. // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
  218. requests.push((token) => {
  219. config.headers[HEADER_TOKEN_KEY] = token
  220. resolve(service(config))
  221. })
  222. })
  223. }
  224. } else if (state === requestState.ILLEGAL_TOKEN || state === requestState.OTHER_CLIENTS) {
  225. // 6020201:非法的token;6020202:其他客户端登录了;6020301:Token 过期了;
  226. if (!cancelRequest) {
  227. cancelRequest = false
  228. Dialog.confirm({
  229. title: I18n.t('error.logout.title'),
  230. message: I18n.t('error.logout.message')
  231. }).then(() => {
  232. store.dispatch('ibps/account/fedLogout').then(() => {
  233. // 终止所有请求
  234. cancelRequest = true
  235. router.push({ name: 'login' })
  236. }).catch(() => {
  237. cancelRequest = false
  238. })
  239. }).catch(() => {
  240. cancelRequest = false
  241. }).finally(() => {
  242. cancelRequest = false
  243. })
  244. }
  245. return Promise.reject(new Error(message))
  246. } else { // 错误处理
  247. let errorMsg = ''
  248. if (Utils.isNotEmpty(message)) { // 有错误消息
  249. errorMsg = Utils.isNotEmpty(dataAxios.cause) ? I18n.t('error.messageCause', {
  250. message,
  251. cause: dataAxios.cause
  252. }) : I18n.t('error.message', {
  253. message
  254. })
  255. } else if (Utils.isNotEmpty(cause)) { // 只有错误原因
  256. errorMsg = I18n.t('error.cause', {
  257. cause
  258. })
  259. } else if (I18n.te('error.status.' + state)) { // 有错误编码
  260. errorMsg = I18n.t('error.status.' + state)
  261. } else { // 未知
  262. errorMsg = message || I18n.t('error.unknown', {
  263. state
  264. })
  265. }
  266. if (response.config.url === '/oauth2/v3/user/login/apply') {
  267. errorMsg = '错误原因:用户名或密码错误'
  268. }
  269. Toast({
  270. message: `${errorMsg}`,
  271. type: 'html',
  272. closeOnClick: true,
  273. className: 'custom-toast',
  274. duration: 3 * 1000
  275. })
  276. const err = new Error(errorMsg)
  277. err.state = state
  278. err.cause = cause
  279. return Promise.reject(err)
  280. }
  281. },
  282. // 异常处理
  283. error => {
  284. tryHideFullScreenLoading()
  285. console.error('request-error', error) // for debug
  286. console.log(error.response)
  287. if (error && error.response) {
  288. error.message = I18n.t('error.status.' + error.response.status, {
  289. url: error.response.config.url
  290. })
  291. error.errMsg = error.response.status + ':' + (error.response.data?.message || error.response.statusText)
  292. } else {
  293. error.state = 500
  294. error.message = I18n.t('error.network') // '服务器君开小差了,请稍后再试'
  295. }
  296. Toast({
  297. message: error.errMsg || error.message || I18n.t('error.network'),
  298. type: 'html',
  299. closeOnClick: true,
  300. className: 'custom-toast',
  301. duration: 3 * 1000
  302. })
  303. return Promise.reject(error)
  304. }
  305. )
  306. export default service