205 lines
5.1 KiB
TypeScript
205 lines
5.1 KiB
TypeScript
/**
|
||
* 统一请求封装
|
||
*
|
||
* - 自动附加 Authorization: Bearer <token>
|
||
* - 401 时自动尝试 refresh_token 刷新,刷新成功后重试原请求
|
||
* - 刷新失败时清除 token 并跳转 login 页面
|
||
* - BASE_URL 从 config.ts 读取(按运行环境自动切换)
|
||
*/
|
||
|
||
import { API_BASE } from "./config"
|
||
|
||
export interface RequestOptions {
|
||
url: string
|
||
method?: "GET" | "POST" | "PUT" | "DELETE"
|
||
data?: any
|
||
header?: Record<string, string>
|
||
/** 是否需要认证,默认 true */
|
||
needAuth?: boolean
|
||
}
|
||
|
||
/** 是否正在刷新 token,防止并发刷新 */
|
||
let isRefreshing = false
|
||
/** 刷新期间排队的请求(等刷新完成后统一重试) */
|
||
let pendingQueue: Array<{
|
||
resolve: (value: any) => void
|
||
reject: (reason: any) => void
|
||
options: RequestOptions
|
||
}> = []
|
||
|
||
/**
|
||
* 从 globalData 或 Storage 获取 access_token
|
||
*/
|
||
function getAccessToken(): string | undefined {
|
||
const app = getApp<IAppOption>()
|
||
return app.globalData.token || wx.getStorageSync("token") || undefined
|
||
}
|
||
|
||
/**
|
||
* 从 globalData 或 Storage 获取 refresh_token
|
||
*/
|
||
function getRefreshToken(): string | undefined {
|
||
const app = getApp<IAppOption>()
|
||
return app.globalData.refreshToken || wx.getStorageSync("refreshToken") || undefined
|
||
}
|
||
|
||
/**
|
||
* 清除本地存储的所有 token
|
||
*/
|
||
function clearTokens(): void {
|
||
const app = getApp<IAppOption>()
|
||
app.globalData.token = undefined
|
||
app.globalData.refreshToken = undefined
|
||
wx.removeStorageSync("token")
|
||
wx.removeStorageSync("refreshToken")
|
||
}
|
||
|
||
/**
|
||
* 保存 token 到 globalData 和 Storage
|
||
*/
|
||
function saveTokens(accessToken: string, refreshToken: string): void {
|
||
const app = getApp<IAppOption>()
|
||
app.globalData.token = accessToken
|
||
app.globalData.refreshToken = refreshToken
|
||
wx.setStorageSync("token", accessToken)
|
||
wx.setStorageSync("refreshToken", refreshToken)
|
||
}
|
||
|
||
/**
|
||
* 跳转到登录页(使用 reLaunch 清空页面栈)
|
||
*/
|
||
function redirectToLogin(): void {
|
||
wx.reLaunch({ url: "/pages/login/login" })
|
||
}
|
||
|
||
/**
|
||
* 执行底层 wx.request,返回 Promise
|
||
*/
|
||
function wxRequest(options: RequestOptions): Promise<any> {
|
||
return new Promise((resolve, reject) => {
|
||
wx.request({
|
||
url: `${API_BASE}${options.url}`,
|
||
method: options.method || "GET",
|
||
data: options.data,
|
||
header: options.header || {},
|
||
success(res) {
|
||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||
resolve(res.data)
|
||
} else {
|
||
reject({ statusCode: res.statusCode, data: res.data })
|
||
}
|
||
},
|
||
fail(err) {
|
||
reject({ statusCode: 0, data: err })
|
||
},
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 尝试用 refresh_token 刷新令牌
|
||
*
|
||
* 成功 → 保存新 token 并返回 true
|
||
* 失败 → 清除 token、跳转登录页、返回 false
|
||
*/
|
||
async function tryRefreshToken(): Promise<boolean> {
|
||
const rt = getRefreshToken()
|
||
if (!rt) {
|
||
clearTokens()
|
||
redirectToLogin()
|
||
return false
|
||
}
|
||
|
||
try {
|
||
const data = await wxRequest({
|
||
url: "/api/xcx/refresh",
|
||
method: "POST",
|
||
data: { refresh_token: rt },
|
||
header: { "Content-Type": "application/json" },
|
||
needAuth: false,
|
||
})
|
||
if (data.access_token && data.refresh_token) {
|
||
saveTokens(data.access_token, data.refresh_token)
|
||
return true
|
||
}
|
||
// 响应格式异常,视为刷新失败
|
||
clearTokens()
|
||
redirectToLogin()
|
||
return false
|
||
} catch {
|
||
clearTokens()
|
||
redirectToLogin()
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理排队中的请求:刷新成功后全部重试,失败则全部拒绝
|
||
*/
|
||
function flushPendingQueue(success: boolean): void {
|
||
const queue = [...pendingQueue]
|
||
pendingQueue = []
|
||
for (const item of queue) {
|
||
if (success) {
|
||
// 重试时会自动附加新 token
|
||
request(item.options).then(item.resolve, item.reject)
|
||
} else {
|
||
item.reject({ statusCode: 401, data: { detail: "刷新令牌失败" } })
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 统一请求入口
|
||
*
|
||
* @param options 请求配置
|
||
* @returns 响应数据(已解析的 JSON)
|
||
*/
|
||
export function request(options: RequestOptions): Promise<any> {
|
||
const needAuth = options.needAuth !== false
|
||
const headers: Record<string, string> = {
|
||
"Content-Type": "application/json",
|
||
...options.header,
|
||
}
|
||
|
||
// 自动附加 Authorization header
|
||
if (needAuth) {
|
||
const token = getAccessToken()
|
||
if (token) {
|
||
headers["Authorization"] = `Bearer ${token}`
|
||
}
|
||
}
|
||
|
||
const finalOptions: RequestOptions = { ...options, header: headers }
|
||
|
||
return wxRequest(finalOptions).catch(async (err) => {
|
||
// 非 401 或不需要认证的请求,直接抛出
|
||
if (err.statusCode !== 401 || !needAuth) {
|
||
throw err
|
||
}
|
||
|
||
// 401 → 尝试刷新 token
|
||
if (isRefreshing) {
|
||
// 已有刷新请求在进行中,排队等待
|
||
return new Promise((resolve, reject) => {
|
||
pendingQueue.push({ resolve, reject, options })
|
||
})
|
||
}
|
||
|
||
isRefreshing = true
|
||
try {
|
||
const ok = await tryRefreshToken()
|
||
flushPendingQueue(ok)
|
||
if (!ok) {
|
||
throw { statusCode: 401, data: { detail: "刷新令牌失败" } }
|
||
}
|
||
// 刷新成功,用新 token 重试原请求
|
||
return request(options)
|
||
} finally {
|
||
isRefreshing = false
|
||
}
|
||
})
|
||
}
|
||
|
||
export default request
|