共计 15518 个字符,预计需要花费 39 分钟才能阅读完成。
[toc]
前言
性能点
此文次要是基于 vuecli3 我的项目中 axios 封装及 api 治理的实际记录及过程中的踩坑播种,性能根本都是依据工作中需要实现。需要背景是,在同一套申请配置下,实现以下次要性能:
- [x] 自定义申请配置
- [x] 设置全局拦截器
- [x] 响应胜利及异样的全局拦挡对立解决
- [x] 避免反复申请(勾销以后反复的申请)
- [x] 路由切换勾销以后所有 pending 状态的申请(可配置白名单)
- [x] 独自勾销收回的某个申请
- [x] api 对立治理
axios 一些个性
在开始之前,首先明确一些 axios 的个性,这些个性会影响到某些性能的实现形式:
- 通过
axios.create()
办法创立的实例对象只有常见的数据申请办法,没有勾销申请、并发申请等办法。可通过Object.keys()
将所有的 key 打印进去比照得悉。 - axios 拦截器是能够累加的,每增加一个拦截器,就会返回一个对应的拦截器 id,也就是无奈通过新增拦挡的形式笼罩或者扭转已有拦截器的配置。但能够利用拦截器 id 通过
axios.interceptors.request.eject(InterceptorId)
办法移除指定拦截器。 - 对于同一个 axios 对象,如果全局拦截器中设置了
CancelToken
属性,就无奈在独自的申请中再通过此属性勾销申请。移除全局拦截器能够解决这个问题,但又会有另一个问题,拦截器移除后就永远生效了,影响是全局的。 - axios 中以别名的模式(axios.get、axios.post)发申请,不同的申请形式参数的写法是不一样的,次要是 put/post/patch 三种办法与其余不太一样
自定义申请配置
根目录下新建 plugins/axios/index.js
文件,自定义 axios 的申请配置。
这里 process.env.VUE_APP_BASEURL
是一个定义好的变量,值为 ”/webapi”;
设置超时工夫 timeout
为 10s。如下:
import axios from 'axios'
axios.defaults.baseURL = process.env.VUE_APP_BASEURL
axios.defaults.timeout = 10000
axios.defaults.headers['custom-defined-header-key'] = 'custom-defined-header-value'
// 自定义申请头:对所有申请办法失效
axios.defaults.headers.common['common-defined-key-b'] = 'custom value: for all methods'
// 自定义申请头:只对 post 办法失效
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
// 自定义申请头:只对 get 办法失效
axios.defaults.headers.get['get-custom-key'] = 'custom value: only for get method';
export default axios
“main.js” 文件:
import request from '@/plugins/axios/index.js'
Vue.prototype.$request = request
这样在组件内就能够通过 this.$request(options)
或者 this.$request.get(options)
的办法来申请数据了。
对常见的响应状况对立解决
这里次要是在 ” 响应拦截器 ” 中,对于一些常见的申请状态码和跟后端约定好的申请返回码做对立的前置解决。
新建 axios.handleResponse.js
文件,用于解决常见的失常响应:
# axios.handleResponse.js
// 解决响应错误码
export default (response) => {
const status = response.status
// 如果 http 响应状态码 response.status 失常,则间接返回数据
if ((status >= 200 && status <= 300) || status === 304) {return response}
// status 不失常的话,依据与后端约定好的 code,做出对应的提醒与解决
// 返回一个带有 code 和 message 属性的对象
else {const code = parseInt(response.data && response.data.code)
// msg 为服务端返回的错误信息,字段名自定义,此处以 msg 为例
let message = (response.data || {}).msg
switch (code) {
case 400:
break
case 4001:
if (process.server) return
message = message || '登录设施数量超出限度'
// store.commit('savehttpResult', { res: response.data})
break
case 403:
message = message || '未登录'
break
case 404:
message = message || '申请地址谬误'
break
case 412:
message = message || '未找到无效 session'
break
default:
// message = message || err.response.data.msg
break
}
return {
code,
message
}
}
}
新建 plugins/axios/axios.handleError.js
文件,用于解决常见的异样响应:
#plugins/axios/axios.handleError.js 文件
export default (err) => {const { response} = err
if (!response.status) {err.code = ''err.message =' 有 response 但没有 response.status 的状况 '}
err.code = response.status
switch (response.status) {
case 200:
err.message = '谬误响应也会有状态码为 200 的状况'
break
case 400:
err.message = '申请谬误(400)'
break
case 401:
err.message = '未受权,请从新登录(401)'
break
case 403:
err.message = '回绝拜访(403)'
break
case 404:
err.message = '申请出错(404)'
break
case 408:
err.message = '申请超时(408)'
break
case 500:
err.message = '服务器谬误(500)'
break
case 501:
err.message = '服务未实现(501)'
break
case 502:
err.message = '网络谬误(502)'
break
case 503:
err.message = '服务不可用(503)'
break
case 504:
err.message = '网络超时(504)'
break
case 505:
err.message = 'HTTP 版本不受反对(505)'
break
default:
err.message = ` 连贯出错,状态码:(${err.response.status})!`
}
return err
}
plugins/axios/index.js
文件中引入并在拦截器中配置:
- 如果申请被勾销,会进入到响应拦截器的第二个参数 err 解决中
#plugins/axios/index.js 文件
import axios from 'axios'
import handleResponse from '@/plugins/axios/axios.handleResponse.js'
import handleError from '@/plugins/axios/axios.handleError.js'
import {Message} from 'element-ui'
const showTip = (tip)=>{
Message({
type: 'warning',
message: tip || '申请出错啦',
duration: 1500
})
}
/**
* 申请拦挡
*/
axios.interceptors.request.use((config) => {
// 在发送申请之前做些什么,例如把用户的登录信息放在申请头上
// config.headers.common['cookie-id'] = cookieId
return config
},
(err) => {
// 对申请谬误做些什么
Promise.reject(err)
}
)
/**
* 响应拦挡
*/
axios.interceptors.response.use((response) => {showTip(err.message)
return Promise.resolve(handleResponse(response)),
}
// 对异样响应解决
(err) => {if (!err) return Promise.reject(err)
if (err.response) {err = handleError(err)
}
// 没有 response(没有状态码)的状况
// eg: 超时;断网;申请反复被勾销;被动勾销申请;else {
// 错误信息 err 传入 isCancel 办法,能够判断申请是否被勾销
if (axios.isCancel(err)) {throw new axios.Cancel(err.message || ` 申请 '${request.config.url}' 被勾销 `)
} else if (err.stack && err.stack.includes('timeout')) {err.message = '申请超时!'} else {err.message = '连贯服务器失败!'}
}
showTip(err.message)
return Promise.reject(err)
}
)
到这里对于一些常见的响应,例如断网、未登录、登录信息生效、超时等,咱们能够申请拦截器中通过 showTip
做出对立的 ui 提醒,就不必每次申请之后再反复得解决这些逻辑了。
接下来就是在 ” 申请拦截器 ” 中配置实现避免反复申请的性能。
避免反复申请
axios 提供了两种勾销申请的办法:
咱们的避免反复申请思路:
在申请拦截器中,通过第二个种办法给每个申请定义 cancelToken
属性,同时申明一个变量pendingPool
,用于并保留 pending 状态的申请及对应的cancelFn
。
在响应拦截器中,无论申请胜利了还是失败了,都通过 api 地址将这个申请从 pendignPool
中删除。
而后每次发动申请前做一个判断,如果 pendingPool
中没有这个申请,失常收回;如果已存在阐明以后申请还是 pending 状态,那么执行 cancelFn
勾销以后反复的申请。
pendingPool
申明为 Map 类型的数据结构,能够不便得通过set
/has
/delete
等进行判断、删除等操作。key 值为 api 地址,value 值为一个对象,保留cancelFn
及global
(global
用于前面的路由切换勾销所有申请,能够临时疏忽)。pendingPool
的大略构造:Map {
'/home/banner' => {cancelFn: [Function: c], global: false }, '/login' => {cancelFn: [Function: c], global: false }
}
申请拦截器中能够拿到每个申请的配置信息 config,增加 cancelToken
属性:
#plugins/axios/index.js 文件
import axios from 'axios'
// 申请中的 api
let pendingPool = new Map()
/**
* 申请拦挡
*/
axios.interceptors.request.use((config) => {
// 对于异样的响应也须要在 pendingPool 中将其删除,但响应拦截器中的异样响应有些获取不到申请信息,这里将其保留在实例上
request.config = Object.assign({}, config)
// 在发送申请之前做些什么,例如把用户的登录信息放在申请头上
// config.headers.common['cookie-id'] = cookieId
config.cancelToken = new axios.CancelToken((cancelFn) => {pendingPool.has(config.url) ? cancelFn(`${config.url}申请反复 `) : pendingPool.set(config.url, { cancelFn, global: config.global})
})
return config
},
(err) => {console.log('申请拦挡 err:', err)
// 对申请谬误做些什么
Promise.reject(err)
}
)
响应拦截器中对有后果(失常及异样)的申请进行删除:
#plugins/axios/index.js 文件
axios.interceptors.response.use(
// 解决失常响应
(response) => {
// 删除
const {config} = response
pendingPool.delete(config.url)
showTip(err.message)
return Promise.resolve(handleResponse(response))
},
// 解决异样响应
(err) => {const { config} = request
// 异样响应删除须要加一个判断:是否为申请被勾销的异样,如果不是才会将这个申请从 pendingPool 中删除。// 否则会呈现一种状况:网速十分慢的状况下,在网速十分慢的状况下多次重复发送同一个申请,第一个申请还在 pending 状态中,// 第二个申请发不进来会间接被 cancel 掉进入到异样响应,而后从 pendignPool 中删除,第三次申请收回的时候就无奈正确判断这个申请是否还是 pending 状态会失常收回
if (!axios.isCancel(err)) pendingPool.delete(config.url)
if (!err) return Promise.reject(err)
if (err.response) {err = handleError(err)
}
// 没有 response(没有状态码)的状况
// eg: 超时;断网;申请反复被勾销;被动勾销申请;else {
// 错误信息 err 传入 isCancel 办法,能够判断申请是否被勾销
if (axios.isCancel(err)) {throw new axios.Cancel(err.message || ` 申请 '${request.config.url}' 被勾销 `)
} else if (err.stack && err.stack.includes('timeout')) {err.message = '申请超时!'} else {err.message = '连贯服务器失败!'}
}
// showTip(err.message)
return Promise.reject(err)
}
)
到这里就实现了避免反复申请的性能。如果同时收回多个雷同的申请,后面申请还在 pending 状态的状况下,前面收回的申请都会被主动勾销并 reject 到申请的 catch 解决中。
独自勾销指定申请
但理论利用中还有一种状况:须要手动勾销指定的某个申请,例如终止文件上传。依据文章结尾提到的 个性 [3] 得悉,此时咱们是无奈独自勾销某个特定申请的。
又因为 个性 [1] 咱们晓得,想要独自勾销指定申请,这个 axios 对象须要满足两个条件:1. 申请拦截器中不能配置 cancelToken
2. 这个 axiso 对象不能通过axios.create()
办法实例化生成。
所以解决思路的大略要点是:
- 须要两个 axios 对象别离解决申请防重和独自勾销特定申请
- 两个 axiso 对象的申请配置像
baseURL
等申请头信息须要是一样的 - 间接用 axios 对象(除了申请头信息,不做任何其余配置)发须要独自勾销的申请,这里申明为
intactRequest
- 通过
axios.create()
办法实例化生成一个新 axios 对象,做最欠缺的配置(避免反复申请、革除所有 pending 状态申请等、响应拦挡等),作为次要发申请的对象,这里申明为request
根目录下新建 plugins/axios/axios.setConfig.js
文件,导出一个自定义 axios 默认配置的办法:
#"axios.setConfig.js" 文件
/**
* @param {axios} axios 实例
* @param {config} 自定义配置对象,可笼罩掉默认的自定义配置
*/
export default (axios, config = {}) => {
const defaultConfig = {
baseURL: process.env.VUE_APP_BASEURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
'custom-defined-header-key': 'custom-defined-header-value',
// 自定义申请头:对所有申请办法失效
common: {'common-defined-key-b': 'custom value: for all methods'},
// 自定义申请头:只对 post 办法失效
post: {'post-custom-key': 'custom value: only for post method'},
// 自定义申请头:只对 get 办法失效
get: {'get-custom-key': 'custom value: only for get method'}
}
}
Object.assign(axios.defaults, defaultConfig, config)
return axios
}
批改 plugins/axiso/index.js
文件,通过 setConfig
办法生成两个具备雷同申请头信息的 axios 对象,并且对 request
对象做申请防重、响应封装解决:
import axios from 'axios'
import setConfig from '@/plugins/axios/axios.setConfig.js'
/**
* intactRequest 是只在 axios 根底上更改了申请配置。* 而 request 是基于 axios 创立的实例,实例只有常见的数据申请办法,没有 axios.isCancel/ axios.CancelToken 等办法,* 也就是没有 ** 勾销申请 ** 和 ** 批量申请 ** 的办法。* 所以如果须要在实例中调用勾销某个申请的办法(例如勾销上传),请用 intactRequest。*/
let intactRequest = setConfig(axios)
let request = setConfig(intactRequest.create())
// 申请中的 api
let pendingPool = new Map()
/**
* 申请拦挡
*/
request.interceptors.request.use(//...)
/**
* 响应拦挡
*/
request.interceptors.response.use(// ...)
export {intactRequest, request}
批改 main.js
文件,把两个对象都挂载到 Vue 示实例上:
import Vue from 'vue'
import * as requests from '@/plugins/axios/index'
Vue.prototype.$request = requests.request
Vue.prototype.$intactRequest = requests.intactRequest
这样就实现了通过 this.$requese
收回的反复申请能够主动被勾销掉,并且对立解决一些常见的响应;通过 this.$intactRequest
收回的申请能够通过在申请中给 config.cancelToken
设置”cancel token“来手动勾销。
一键革除所有 pending 状态申请
在路由切换时能够勾销以后仍在 pending 状态的申请从而优化性能、节约资源。
下面的 pendingPool
曾经保留了所有 pending 状态的申请,封装一个办法,拿到其中每个申请而后执行 cancelFn
,而后每次路由切换的时候执行这个办法即可。但不排除有些 api 申请是全局的不能被勾销。所以这个办法根底上新增白名单和申请的global
参数。
在 plugins/axios/index.js
中新增 clearPendingPool
办法,
/**
* 革除所有 pending 状态的申请
* @param {Array} whiteList 白名单,外面的申请不会被勾销
* 返回值 被勾销了的 api 申请
*/
function clearPendingPool(whiteList = []) {if (!pendingPool.size) return
// const pendingUrlList = [...pendingPool.keys()].filter((url) => !whiteList.includes(url))
const pendingUrlList = Array.from(pendingPool.keys()).filter((url) => !whiteList.includes(url))
if (!pendingUrlList.length) return
pendingUrlList.forEach((pendingUrl) => {
// 革除掉所有非全局的 pending 状态下的申请
if (!pendingPool.get(pendingUrl).global) {pendingPool.get(pendingUrl).cancelFn()
pendingPool.delete(pendingUrl)
}
})
return pendingUrlList
}
request.clearPendingPool = clearPendingPool
在路由的配置文件 src/router/idnex.js
中,引入 request,并在路由全局前置守卫中执行 clearPendingPool
办法:
import {request} from '@/plugins/axios/index'
// 路由全局前置守卫
router.beforeEach((to, from, next) => {
// 路由变动时勾销以后所有非全局的 pending 状态的申请
request.clearPendingPool()
next()})
到这里就实现了路由切换勾销 pending 状态的申请。能够通过两种形式指定某些 api 不被勾销:
- 执行
clearPendingPool
时传入一个白名单列表:
const globalApi = [
'/global/banner',
'/global/activity'
]
request.clearPendingPool(globalApi)
- 发动申请的时候携带
global
参数,默认为 false:
this.$request.get('/global/banner',{params:{page: 1},
global: true
})
this.$request.post('/user/login',{
name: 'xxx',
pwd:'123456'
},{global: true})
移除拦截器
依据 个性 [2] 晓得拦截器是能够累加也能够移除的。封装两个移除的全局拦挡的办法并挂载到 request
对象上。目前看来还没有理论利用场景,只是做一下记录。
plugins/axios/index.js
文件中新增两个办法:
/**
* 申请拦挡
*/
const requestInterceptorId = request.interceptors.request.use(
// ...)/**
* 响应拦挡
*/
const responseInterceptorId = request.interceptors.response.use(
// ...)// 移除全局的申请拦截器
function removeRequestInterceptors() {request.interceptors.request.eject(requestInterceptorId)
}
// 移除全局的响应拦截器
function removeResponseInterceptors() {request.interceptors.response.eject(responseInterceptorId)
}
request.removeRequestInterceptors = removeRequestInterceptors
request.removeResponseInterceptors = removeResponseInterceptors
通过 this.$request.removeRequestInterceptors
和this.$request.removeResponseInterceptors
调用即可。
api 治理
这里咱们把所有的 api 地址及对应的申请形式放在一起治理,而后组件中通过别名间接调用即可。
先具体理解一下 个性 [3] 提到 aixos 不同的申请形式参数的写法问题:次要因为 post
、put
、patch
三种办法相比其余办法多一个 data
属性,也就是须要 在申请体中携带的数据 ,其余办法会主动疏忽data
属性;而 params
属性是所有办法都有的,与申请一起发送的 URL 参数。
axios 发动申请大略有两种写法:
- 间接通过 axios:
axios(config)
的模式 - 通过 axios 别名的模式:
axiso[method]()
通过别名的模式发动申请,post
、put
、patch
三种办法须要接管三个参数:axios[method](api, data, headersConfig)
,第二个参数 data
就是须要在申请体中携带的参数;而且他办法只接管两个参数,不须要第二个参数data
。
根目录下新建一个 src/api/index.js
文件:
/**api 治理页面
* apiMap: 对立治理所有 api 地址、对应的申请形式及自定义别名
* 导出一个对象 requestMap,属性名为 apiMap 中定义的别名,也就是调用时的名称,值为理论申请办法
* 办法接管两个对象参数,第一个为须要传递的数据,第二个为申请头的配置信息。* 语法:api[alias](paramsOrData, headersConfig)
* 第一个参数:如果为 put/post/patch 办法中的一种,会被转化为 data 属性;其余则是 params
* 第二个参数:申请头信息
*
* let xx = await this.$api.getBanner({account: '18038018084', psw: '2'})
* let vv = await this.$api.login({account: '18038018084', psw: '2'})
*
* 如果相似 post 的办法须要通过 url 后缀模式传递参数,在第二个参数 config 加上 params 属性即可:* let vv = await this.$api.login({account: '18038018084', psw: '2'},{params: {} })
*
* 自定义申请头信息:* let xx = await this.$api.getBanner({}, {timeout: 1000, headers:{ aaa: 111}})
*/
import {request} from '@/plugins/axios/index'
// import qs from 'qs'
// console.log('qs:', qs)
const apiMap = {getBanner: { method: 'get', url: '/home/banner'},
login: {method: 'post', url: '/login'}
}
function injectRequest(apiObj) {const requestMap = {}
Object.keys(apiObj).forEach((alias) => {let { method, url, config} = apiObj[alias]
method = method.toUpperCase()
requestMap[alias] = (dataOrParams = {}, instanceConf = {}) => {const keyName = ['PUT', 'POST', 'PATCH'].includes(method) ? 'data' : 'params'
return request({
method,
url,
// [keyName]: method === 'POST' ? qs.stringify(dataOrParams) : dataOrParams,
[keyName]: dataOrParams,
...Object.assign(config || {}, instanceConf)
})
}
})
return requestMap
}
export default injectRequest(apiMap)
在 mains.js
中引入并挂载到 vue 实例:
import Vue from 'vue'
import api from '@/api/index.js'
Vue.prototype.$api = api
调用示例:
// 申请头信息
const headersConfig = {
timeout: 5000,
global: true,
headers:{aaa: 'vvv'}
}
// get 申请
this.$api.getBanner({page: 1,})
// get 申请,自定义申请头信息
this.$api.getBanner({page: 1,}, headersConfig)
// post 申请
this.$api.login({
account: 'laowang',
pwd: 'xxxx'
})
// post 申请,自定义申请头信息
this.$api.login({
account: 'laowang',
pwd: 'xxxx'
}, headersConfig)
总结
最终我的项目中相干文件的目录构造:
├── CHANGELOG.md
├── README.md
├── package.json
├── src
│ ├── api
│ │ └── index.js // api 治理
│ ├── plugins
│ │ ├── axios // axios 封装
│ │ │ ├── axios.handleError.js
│ │ │ ├── axios.handleResponse.js
│ │ │ ├── axios.setConfig.js
│ │ │ └── index.js
│ ├── router
│ │ ├── index.js
└── yarn.lock
src/plugins/axios/index.js
文件最终:
import axios from 'axios'
import setConfig from '@/plugins/axios/axios.setConfig.js'
import handleResponse from '@/plugins/axios/axios.handleResponse.js'
import handleError from '@/plugins/axios/axios.handleError.js'
// import store from '@/store/index'
// import router from '@/router/index.js'
import {Message} from 'element-ui'
const showTip = (tip)=>{
Message({
type: 'warning',
message: tip || '申请出错啦',
duration: 1500
})
}
/**
* intactRequest 是只在 axios 根底上更改了申请配置。* 而 request 是基于 axios 创立的实例,实例只有常见的数据申请办法,没有 axios.isCancel/ axios.CancelToken 等办法,* 也就是没有 ** 勾销申请 ** 和 ** 批量申请 ** 的办法。* 所以如果须要在实例中调用勾销某个申请的办法(例如勾销上传),请用 intactRequest。*/
let intactRequest = setConfig(axios)
let request = setConfig(intactRequest.create())
// 申请中的 api
let pendingPool = new Map()
/**
* 申请拦挡
*/
const requestInterceptorId = request.interceptors.request.use((config) => {
// 对于异样的响应也须要在 pendingPool 中将其删除,但响应拦截器中的异样响应有些获取不到申请信息,这里将其保留在实例上
request.config = Object.assign({}, config)
// 在发送申请之前做些什么
// config.headers.common['cookie-id'] = cookieId
config.cancelToken = new axios.CancelToken((cancelFn) => {pendingPool.has(config.url) ? cancelFn(`${config.url}申请反复 `) : pendingPool.set(config.url, { cancelFn, global: config.global})
})
return config
},
(err) => {console.log('申请拦挡 err:', err)
// 对申请谬误做些什么
return Promise.reject(err)
}
)
/**
* 响应拦挡
*/
const responseInterceptorId = request.interceptors.response.use((response) => {const { config} = response
pendingPool.delete(config.url)
// console.log('响应 response suc:', response)
showTip(err.message)
return Promise.resolve(handleResponse(response))
},
// 对异样响应解决
(err) => {const { config} = request
if (!axios.isCancel(err)) pendingPool.delete(config.url)
if (!err) return Promise.reject(err)
if (err.response) {err = handleError(err)
}
// 没有 response(没有状态码)的状况
// eg: 超时;断网;申请反复被勾销;被动勾销申请;else {
// 错误信息 err 传入 isCancel 办法,能够判断申请是否被勾销
if (axios.isCancel(err)) {throw new axios.Cancel(err.message || ` 申请 '${request.config.url}' 被勾销 `)
} else if (err.stack && err.stack.includes('timeout')) {err.message = '申请超时!'} else {err.message = '连贯服务器失败!'}
}
showTip(err.message)
return Promise.reject(err)
}
)
// 移除全局的申请拦截器
function removeRequestInterceptor() {request.interceptors.request.eject(requestInterceptorId)
}
// 移除全局的响应拦截器
function removeResponseInterceptor() {request.interceptors.response.eject(responseInterceptorId)
}
/**
* 革除所有 pending 状态的申请
* @param {Array} whiteList 白名单,外面的申请不会被勾销
* 返回值 被勾销了的 api 申请
*/
function clearPendingPool(whiteList = []) {if (!pendingPool.size) return
// const pendingUrlList = [...pendingPool.keys()].filter((url) => !whiteList.includes(url))
const pendingUrlList = Array.from(pendingPool.keys()).filter((url) => !whiteList.includes(url))
if (!pendingUrlList.length) return
pendingUrlList.forEach((pendingUrl) => {
// 革除掉所有非全局的 pending 状态下的申请
if (!pendingPool.get(pendingUrl).global) {pendingPool.get(pendingUrl).cancelFn()
pendingPool.delete(pendingUrl)
}
})
return pendingUrlList
}
request.removeRequestInterceptor = removeRequestInterceptor
request.removeResponseInterceptor = removeResponseInterceptor
request.clearPendingPool = clearPendingPool
export {intactRequest, request}
限时秒杀阿里云服务器 ECS、云数据库 MySQL、对象存储 OSS 等多种代金券