feat:新增请求逻辑封装

This commit is contained in:
5g0Wp7Zy 2025-10-11 13:48:29 +08:00
parent 6841fa17d3
commit a9580ca2c2
12 changed files with 303 additions and 77 deletions

7
env/.env vendored
View File

@ -10,7 +10,7 @@ VITE_WX_APPID = 'wx8e64a91083684628'
VITE_APP_PUBLIC_BASE=/
# 后台请求地址
VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
VITE_SERVER_BASEURL = 'https://www.cx.cdzhuojing.cn/ebike'
# 备注如果后台带统一前缀则也要加到后面eg: https://ukw0y1.laf.run/api
# 注意,如果是微信小程序,还有一套请求地址的配置,根据 develop、trial、release 分别设置上传地址,见 `src/utils/index.ts`。
@ -21,10 +21,13 @@ VITE_APP_PROXY_ENABLE = false
VITE_APP_PROXY_PREFIX = '/fg-api'
# 第二个请求地址 (目前alova中可以使用)
VITE_SERVER_BASEURL_SECONDARY = 'https://ukw0y1.laf.run'
VITE_SERVER_BASEURL_SECONDARY = 'https://www.cx.cdzhuojing.cn/ebike'
# 认证模式,'single' | 'double' ==> 单token | 双token
VITE_AUTH_MODE = 'single'
# 原生插件资源复制开关,控制是否启用 copy-native-resources 插件
VITE_COPY_NATIVE_RES_ENABLE = false
# sm2加密key
VITE_SM2_KEY = '04f5084ee12767d932f293508e30e3b0100185042ec0f061dedaf92b793b93f79fd6179d5e47e25b7aec98e00cf90dd56df1f8191012537187e7bbfd2d1de299fc'

View File

@ -115,10 +115,13 @@
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
"abortcontroller-polyfill": "^1.7.8",
"alova": "^3.3.3",
"crypto-js": "^4.2.0",
"dayjs": "1.11.10",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
"sm-crypto": "^0.3.13",
"vue": "^3.4.21",
"z-paging": "2.8.7"
},
@ -187,4 +190,4 @@
"lint-staged": {
"*": "eslint --fix"
}
}
}

View File

@ -1,11 +1,14 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { navigateToInterceptor } from '@/router/interceptor'
import CacheManager from './utils/CacheManager'
onLaunch((options) => {
console.log('App Launch', options)
})
onShow((options) => {
// token
CacheManager.set('token', 'fdSeMVT0SxdGuq8aqAL0CARmFdybUegn')
console.log('App Show', options)
// h5
// https://github.com/unibest-tech/unibest/issues/192

View File

@ -4,7 +4,7 @@ export type AuthMode = 'single' | 'double'
// 单Token响应类型
export interface ISingleTokenRes {
token: string
expiresIn: number // 有效期(秒)
expiresIn?: number // 有效期(秒)
}
// 双Token响应类型

View File

@ -1,20 +1,23 @@
import type { IDoubleTokenRes } from '@/api/types/login'
import type { CustomRequestOptions, IResponse } from '@/http/types'
import { nextTick } from 'vue'
import { LOGIN_PAGE } from '@/router/config'
import { useTokenStore } from '@/store/token'
import { isDoubleTokenMode } from '@/utils'
import { ResultEnum } from './tools/enum'
import { encryptData, encryptQuery, getHeader } from './tools/httpTool'
// 刷新 token 状态管理
let refreshing = false // 防止重复刷新 token 标识
let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
// 刷新 token 状态管理 暂时没有考虑token刷新逻辑
// let refreshing = false // 防止重复刷新 token 标识
// let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
export function http<T>(options: CustomRequestOptions) {
// 1. 返回 Promise 对象
return new Promise<T>((resolve, reject) => {
const _header = getHeader(options)
const _query = encryptQuery(options)
const _data = encryptData(options)
uni.request({
...options,
header: _header,
query: _query,
data: _data,
dataType: 'json',
// #ifndef MP-WEIXIN
responseType: 'json',
@ -27,70 +30,71 @@ export function http<T>(options: CustomRequestOptions) {
// 检查是否是401错误包括HTTP状态码401或业务码401
const isTokenExpired = res.statusCode === 401 || code === 401
if (isTokenExpired) {
const tokenStore = useTokenStore()
if (!isDoubleTokenMode) {
// 未启用双token策略清理用户信息跳转到登录页
tokenStore.logout()
uni.navigateTo({ url: LOGIN_PAGE })
return reject(res)
}
// 业务当前暂时没有考虑token刷新
// if (isTokenExpired) {
// const tokenStore = useTokenStore()
// if (!isDoubleTokenMode) {
// // 未启用双token策略清理用户信息跳转到登录页
// tokenStore.logout()
// uni.navigateTo({ url: LOGIN_PAGE })
// return reject(res)
// }
/* -------- 无感刷新 token ----------- */
const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
// token 失效的,且有刷新 token 的,才放到请求队列里
if (refreshToken) {
taskQueue.push(() => {
resolve(http<T>(options))
})
}
// /* -------- 无感刷新 token ----------- */
// const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
// // token 失效的,且有刷新 token 的,才放到请求队列里
// if (refreshToken) {
// taskQueue.push(() => {
// resolve(http<T>(options))
// })
// }
// 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
if (refreshToken && !refreshing) {
refreshing = true
try {
// 发起刷新 token 请求(使用 store 的 refreshToken 方法)
await tokenStore.refreshToken()
// 刷新 token 成功
refreshing = false
nextTick(() => {
// 关闭其他弹窗
uni.hideToast()
uni.showToast({
title: 'token 刷新成功',
icon: 'none',
})
})
// 将任务队列的所有任务重新请求
taskQueue.forEach(task => task())
}
catch (refreshErr) {
console.error('刷新 token 失败:', refreshErr)
refreshing = false
// 刷新 token 失败,跳转到登录页
nextTick(() => {
// 关闭其他弹窗
uni.hideToast()
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
})
})
// 清除用户信息
await tokenStore.logout()
// 跳转到登录页
setTimeout(() => {
uni.navigateTo({ url: LOGIN_PAGE })
}, 2000)
}
finally {
// 不管刷新 token 成功与否,都清空任务队列
taskQueue = []
}
}
// // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
// if (refreshToken && !refreshing) {
// refreshing = true
// try {
// // 发起刷新 token 请求(使用 store 的 refreshToken 方法)
// await tokenStore.refreshToken()
// // 刷新 token 成功
// refreshing = false
// nextTick(() => {
// // 关闭其他弹窗
// uni.hideToast()
// uni.showToast({
// title: 'token 刷新成功',
// icon: 'none',
// })
// })
// // 将任务队列的所有任务重新请求
// taskQueue.forEach(task => task())
// }
// catch (refreshErr) {
// console.error('刷新 token 失败:', refreshErr)
// refreshing = false
// // 刷新 token 失败,跳转到登录页
// nextTick(() => {
// // 关闭其他弹窗
// uni.hideToast()
// uni.showToast({
// title: '登录已过期,请重新登录',
// icon: 'none',
// })
// })
// // 清除用户信息
// await tokenStore.logout()
// // 跳转到登录页
// setTimeout(() => {
// uni.navigateTo({ url: LOGIN_PAGE })
// }, 2000)
// }
// finally {
// // 不管刷新 token 成功与否,都清空任务队列
// taskQueue = []
// }
// }
return reject(res)
}
// return reject(res)
// }
// 处理其他成功状态HTTP状态码200-299
if (res.statusCode >= 200 && res.statusCode < 300) {

View File

@ -0,0 +1,71 @@
import type { CustomRequestOptions } from '@/http/types'
import CacheManager from '@/utils/CacheManager'
import SM2 from '@/utils/sm2'
import { ContentTypeEnum } from './enum'
const sm2Key = import.meta.env.VITE_SM2_KEY
/**
*
*/
export function getHeader(options: CustomRequestOptions) {
// 获取token
const token = CacheManager.get('token')
const { noToken, contentTypeUrlEncoded, header } = options
// 自定义header
if (header && Object.keys(header).length > 0) {
return { ...header }
}
let _header = {}
if (!noToken) {
_header = {
Authorization: token,
}
}
_header = {
..._header,
'Content-Type': contentTypeUrlEncoded ? ContentTypeEnum.FORM_URLENCODED : ContentTypeEnum.JSON,
}
return _header
}
/**
* query参数
*/
export function encryptQuery(options: CustomRequestOptions) {
const { query } = options
// 如果query存在且不是空对象遍历对象的值进行SM2加密
if (query && Object.keys(query).length > 0) {
for (const key in query) {
if (Object.prototype.hasOwnProperty.call(query, key)) {
query[key] = SM2.sm2Encrypt(query[key], sm2Key)
}
}
}
return query
}
/**
* data参数
*/
export function encryptData(options: CustomRequestOptions) {
const { data, contentTypeUrlEncoded } = options
let _data: any = null
if (data) {
_data = SM2.sm2Encrypt(JSON.stringify(data), sm2Key)
}
if (contentTypeUrlEncoded && data) {
const formData = new FormData()
Object.keys(data).forEach((key) => {
const enKeyData = SM2.sm2Encrypt(data[key], sm2Key)
formData.append(key, enKeyData)
})
_data = formData
}
return _data
}

View File

@ -5,6 +5,10 @@ export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
/** 请求是否不需要token */
noToken?: boolean
/** 内容型是否为x-www-form-urlencoded */
contentTypeUrlEncoded?: boolean
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
export interface HttpRequestResult<T> {

View File

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { getBikecodeAPI } from '@/api/test'
import { safeAreaInsets } from '@/utils/systemInfo'
defineOptions({
@ -21,6 +22,9 @@ console.log('index/index 首页打印了')
onLoad(() => {
console.log('测试 uni API 自动引入: onLoad')
getBikecodeAPI('060002').then((res) => {
console.log('getBikecodeAPI res: ', res)
})
})
// #region gotoAbout

108
src/utils/CacheManager.ts Normal file
View File

@ -0,0 +1,108 @@
interface CacheItem<T> {
value: T
expiry?: number // 过期时间戳(毫秒),可选
}
/**
* TTL
*/
class CacheManager {
/**
*
* @param key
* @param value
* @param ttl undefinednull <=0
*/
static set<T>(key: string, value: T, ttl?: number | null): void {
try {
const cacheItem: CacheItem<T> = { value }
// 仅当 ttl 为正数时才设置过期时间
if (ttl != null && ttl > 0) {
cacheItem.expiry = Date.now() + ttl * 1000
}
// 否则 cacheItem.expiry 保持 undefined表示永久缓存
uni.setStorageSync(key, cacheItem)
}
catch (e) {
console.error('CacheManager.set error:', e)
}
}
/**
*
* @param key
* @param defaultValue
* @returns
*/
static get<T>(key: string, defaultValue: T = null as unknown as T): T {
try {
const raw = uni.getStorageSync(key)
if (!raw)
return defaultValue
const cacheItem = raw as CacheItem<T>
// 若有 expiry 且已过期,则清除并返回默认值
if (cacheItem.expiry !== undefined && Date.now() > cacheItem.expiry) {
uni.removeStorageSync(key)
return defaultValue
}
return cacheItem.value
}
catch (e) {
console.error('CacheManager.get error:', e)
return defaultValue
}
}
/**
*
*/
static remove(key: string): void {
try {
uni.removeStorageSync(key)
}
catch (e) {
console.error('CacheManager.remove error:', e)
}
}
/**
*
*/
static clear(): void {
try {
uni.clearStorageSync()
}
catch (e) {
console.error('CacheManager.clear error:', e)
}
}
/**
*
*/
static has(key: string): boolean {
try {
const raw = uni.getStorageSync(key)
if (!raw)
return false
const cacheItem = raw as CacheItem<unknown>
if (cacheItem.expiry !== undefined && Date.now() > cacheItem.expiry) {
uni.removeStorageSync(key)
return false
}
return true
}
catch (e) {
console.error('CacheManager.has error:', e)
return false
}
}
}
export default CacheManager

View File

@ -121,9 +121,9 @@ export function getEnvBaseUrl() {
let baseUrl = import.meta.env.VITE_SERVER_BASEURL
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = baseUrl
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = baseUrl
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = baseUrl
// 微信小程序端环境区分
if (isMpWeixin) {
@ -157,3 +157,19 @@ export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double'
* /pages/index/index
*/
export const HOME_PAGE = `/${(pages as PageMetaDatum[]).find(page => page.type === 'home')?.path || (pages as PageMetaDatum[])[0].path}`
/**
* url
*/
export function getUrlQuery(url: string) {
const queryStr = url.split('?')[1]
if (!queryStr) {
return {}
}
const query: Record<string, string> = {}
queryStr.split('&').forEach((item) => {
const [key, value] = item.split('=')
query[key] = value
})
return query
}

10
src/utils/sm2.ts Normal file
View File

@ -0,0 +1,10 @@
import smcrypto from 'sm-crypto'
const SM2 = {
sm2Encrypt(text: string, publicKey: string) {
const sm2 = smcrypto.sm2
return sm2.doEncrypt(text, publicKey)
},
}
export default SM2

View File

@ -186,7 +186,7 @@ export default defineConfig(({ command, mode }) => {
: undefined,
},
esbuild: {
drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : ['debugger'],
drop: VITE_DELETE_CONSOLE === 'true' ? ['console'] : [],
},
build: {
sourcemap: false,