mi-assessment/uniapp/api/request.js
zpc 4387b15de0 feat(mine): 完成我的页面改造
- 实现未登录/已登录两种状态样式
- 添加常用功能入口:我的订单、往期测评、联系我们、邀请新用户
- 添加其他功能入口:关于、用户协议、隐私政策、退出登录
- 实现退出登录二次确认弹窗
- 修复 uni.scss 中 SCSS 导入路径问题
- 整理 .gitignore 文件,移除 unpackage 构建目录
2026-02-10 00:12:01 +08:00

343 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 请求封装模块
* 封装 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
}