- 实现未登录/已登录两种状态样式 - 添加常用功能入口:我的订单、往期测评、联系我们、邀请新用户 - 添加其他功能入口:关于、用户协议、隐私政策、退出登录 - 实现退出登录二次确认弹窗 - 修复 uni.scss 中 SCSS 导入路径问题 - 整理 .gitignore 文件,移除 unpackage 构建目录
343 lines
8.5 KiB
JavaScript
343 lines
8.5 KiB
JavaScript
/**
|
||
* 请求封装模块
|
||
* 封装 uni.request 支持 GET/POST/PUT/DELETE
|
||
* 实现 JWT Token 自动携带
|
||
* 实现 401 状态码拦截和自动刷新 Token
|
||
* 支持请求重试机制
|
||
* 支持请求取消机制
|
||
* Requirements: 1.2, 1.4
|
||
*/
|
||
|
||
import { getToken, setToken, removeToken, getRefreshToken, setRefreshToken, removeRefreshToken, removeUserInfo } from '../utils/storage'
|
||
import config from '../config/index'
|
||
|
||
// 从统一配置获取 API 基础地址
|
||
const BASE_URL = config.API_BASE_URL
|
||
|
||
// 存储进行中的请求任务,用于取消
|
||
const pendingRequests = new Map()
|
||
|
||
// Token 刷新状态
|
||
let isRefreshing = false
|
||
// 等待 Token 刷新的请求队列
|
||
let refreshSubscribers = []
|
||
|
||
/**
|
||
* 生成请求唯一标识
|
||
* @param {Object} options 请求配置
|
||
* @returns {string}
|
||
*/
|
||
function generateRequestKey(options) {
|
||
const { url, method = 'GET', data = {} } = options
|
||
return `${method}:${url}:${JSON.stringify(data)}`
|
||
}
|
||
|
||
/**
|
||
* 处理401错误 - 清除token并跳转登录
|
||
* Property 2: Auth Redirect on Invalid Token
|
||
*/
|
||
export function handleUnauthorized() {
|
||
removeToken()
|
||
removeRefreshToken()
|
||
removeUserInfo()
|
||
|
||
// 跳转到登录页
|
||
const pages = getCurrentPages()
|
||
const currentPage = pages[pages.length - 1]
|
||
const currentPath = currentPage ? `/${currentPage.route}` : ''
|
||
|
||
uni.navigateTo({
|
||
url: `/pages/login/index?redirect=${encodeURIComponent(currentPath)}`
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 订阅 Token 刷新完成事件
|
||
* @param {Function} callback 回调函数
|
||
*/
|
||
function subscribeTokenRefresh(callback) {
|
||
refreshSubscribers.push(callback)
|
||
}
|
||
|
||
/**
|
||
* 通知所有订阅者 Token 已刷新
|
||
* @param {string} newToken 新的 Token
|
||
*/
|
||
function onTokenRefreshed(newToken) {
|
||
refreshSubscribers.forEach(callback => callback(newToken))
|
||
refreshSubscribers = []
|
||
}
|
||
|
||
/**
|
||
* 刷新 Token
|
||
* @returns {Promise<string|null>} 新的 Token 或 null
|
||
*/
|
||
async function refreshToken() {
|
||
const refreshTokenValue = getRefreshToken()
|
||
|
||
if (!refreshTokenValue) {
|
||
return null
|
||
}
|
||
|
||
try {
|
||
const response = await new Promise((resolve, reject) => {
|
||
uni.request({
|
||
url: `${BASE_URL}/refresh`,
|
||
method: 'POST',
|
||
data: { refreshToken: refreshTokenValue },
|
||
header: { 'Content-Type': 'application/json' },
|
||
timeout: config.REQUEST_TIMEOUT,
|
||
success: (res) => resolve(res),
|
||
fail: (err) => reject(err)
|
||
})
|
||
})
|
||
|
||
const { statusCode, data: responseData } = response
|
||
|
||
if (statusCode === 200 && responseData.code === 0 && responseData.data) {
|
||
const { token, refreshToken: newRefreshToken } = responseData.data
|
||
|
||
// 保存新的 Token
|
||
setToken(token)
|
||
if (newRefreshToken) {
|
||
setRefreshToken(newRefreshToken)
|
||
}
|
||
|
||
return token
|
||
}
|
||
|
||
return null
|
||
} catch (error) {
|
||
console.error('Token refresh failed:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 延迟函数
|
||
* @param {number} ms 延迟毫秒数
|
||
*/
|
||
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||
|
||
/**
|
||
* 取消指定请求
|
||
* @param {string} requestKey 请求标识
|
||
*/
|
||
export function cancelRequest(requestKey) {
|
||
if (pendingRequests.has(requestKey)) {
|
||
const task = pendingRequests.get(requestKey)
|
||
if (task && typeof task.abort === 'function') {
|
||
task.abort()
|
||
}
|
||
pendingRequests.delete(requestKey)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取消所有进行中的请求
|
||
*/
|
||
export function cancelAllRequests() {
|
||
pendingRequests.forEach((task) => {
|
||
if (task && typeof task.abort === 'function') {
|
||
task.abort()
|
||
}
|
||
})
|
||
pendingRequests.clear()
|
||
}
|
||
|
||
|
||
/**
|
||
* 取消指定URL前缀的所有请求
|
||
* @param {string} urlPrefix URL前缀
|
||
*/
|
||
export function cancelRequestsByPrefix(urlPrefix) {
|
||
pendingRequests.forEach((task, key) => {
|
||
if (key.includes(urlPrefix)) {
|
||
if (task && typeof task.abort === 'function') {
|
||
task.abort()
|
||
}
|
||
pendingRequests.delete(key)
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 发起请求(带重试、取消和 Token 刷新机制)
|
||
* @param {Object} options 请求配置
|
||
* @param {number} retryCount 当前重试次数
|
||
* @returns {Promise}
|
||
*/
|
||
export function request(options, retryCount = 0) {
|
||
const {
|
||
url,
|
||
method = 'GET',
|
||
data = {},
|
||
header = {},
|
||
needAuth = true,
|
||
retry = true,
|
||
cancelable = true,
|
||
cancelPrevious = false,
|
||
skipRefresh = false // 是否跳过 Token 刷新(用于刷新 Token 请求本身)
|
||
} = options
|
||
|
||
const requestKey = generateRequestKey(options)
|
||
|
||
if (cancelPrevious && pendingRequests.has(requestKey)) {
|
||
cancelRequest(requestKey)
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const token = getToken()
|
||
|
||
const requestHeader = {
|
||
'Content-Type': 'application/json',
|
||
...header
|
||
}
|
||
|
||
if (needAuth && token) {
|
||
requestHeader['Authorization'] = `Bearer ${token}`
|
||
}
|
||
|
||
const requestTask = uni.request({
|
||
url: `${BASE_URL}${url}`,
|
||
method,
|
||
data,
|
||
header: requestHeader,
|
||
timeout: config.REQUEST_TIMEOUT,
|
||
success: async (res) => {
|
||
if (cancelable) {
|
||
pendingRequests.delete(requestKey)
|
||
}
|
||
|
||
const { statusCode, data: responseData } = res
|
||
|
||
// 处理 401 未授权 - 尝试刷新 Token
|
||
if (statusCode === 401 && needAuth && !skipRefresh) {
|
||
// 如果正在刷新 Token,将请求加入队列等待
|
||
if (isRefreshing) {
|
||
subscribeTokenRefresh((newToken) => {
|
||
// 使用新 Token 重试请求
|
||
const newOptions = { ...options, header: { ...header, Authorization: `Bearer ${newToken}` } }
|
||
request(newOptions, retryCount)
|
||
.then(resolve)
|
||
.catch(reject)
|
||
})
|
||
return
|
||
}
|
||
|
||
isRefreshing = true
|
||
|
||
try {
|
||
const newToken = await refreshToken()
|
||
|
||
if (newToken) {
|
||
isRefreshing = false
|
||
onTokenRefreshed(newToken)
|
||
|
||
// 使用新 Token 重试当前请求
|
||
const newOptions = { ...options, header: { ...header, Authorization: `Bearer ${newToken}` } }
|
||
const result = await request(newOptions, retryCount)
|
||
resolve(result)
|
||
return
|
||
}
|
||
} catch (refreshError) {
|
||
console.error('Token refresh error:', refreshError)
|
||
}
|
||
|
||
isRefreshing = false
|
||
refreshSubscribers = []
|
||
|
||
// 刷新失败,清除登录状态并跳转登录页
|
||
handleUnauthorized()
|
||
reject(new Error('登录已过期,请重新登录'))
|
||
return
|
||
}
|
||
|
||
if (statusCode >= 400) {
|
||
const errorMsg = responseData?.message || '请求失败'
|
||
reject(new Error(errorMsg))
|
||
return
|
||
}
|
||
|
||
if (responseData.success === false) {
|
||
reject(new Error(responseData.message || '操作失败'))
|
||
return
|
||
}
|
||
|
||
resolve(responseData)
|
||
},
|
||
fail: async (err) => {
|
||
if (cancelable) {
|
||
pendingRequests.delete(requestKey)
|
||
}
|
||
|
||
if (err.errMsg && err.errMsg.includes('abort')) {
|
||
reject(new Error('请求已取消'))
|
||
return
|
||
}
|
||
|
||
console.error('Request failed:', err)
|
||
|
||
if (retry && retryCount < config.REQUEST_RETRY_COUNT) {
|
||
console.log(`请求失败,${config.REQUEST_RETRY_DELAY}ms 后进行第 ${retryCount + 1} 次重试...`)
|
||
await delay(config.REQUEST_RETRY_DELAY)
|
||
try {
|
||
const result = await request(options, retryCount + 1)
|
||
resolve(result)
|
||
} catch (retryErr) {
|
||
reject(retryErr)
|
||
}
|
||
return
|
||
}
|
||
|
||
reject(new Error('网络连接失败,请检查网络设置'))
|
||
}
|
||
})
|
||
|
||
if (cancelable && requestTask) {
|
||
pendingRequests.set(requestKey, requestTask)
|
||
}
|
||
})
|
||
}
|
||
|
||
|
||
/**
|
||
* GET 请求
|
||
*/
|
||
export function get(url, data = {}, options = {}) {
|
||
return request({ url, method: 'GET', data, ...options })
|
||
}
|
||
|
||
/**
|
||
* POST 请求
|
||
*/
|
||
export function post(url, data = {}, options = {}) {
|
||
return request({ url, method: 'POST', data, ...options })
|
||
}
|
||
|
||
/**
|
||
* PUT 请求
|
||
*/
|
||
export function put(url, data = {}, options = {}) {
|
||
return request({ url, method: 'PUT', data, ...options })
|
||
}
|
||
|
||
/**
|
||
* DELETE 请求
|
||
*/
|
||
export function del(url, data = {}, options = {}) {
|
||
return request({ url, method: 'DELETE', data, ...options })
|
||
}
|
||
|
||
export default {
|
||
request,
|
||
get,
|
||
post,
|
||
put,
|
||
del,
|
||
handleUnauthorized,
|
||
cancelRequest,
|
||
cancelAllRequests,
|
||
cancelRequestsByPrefix
|
||
}
|