2026-04-10 16:10:33 +08:00

256 lines
8.7 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.

const { getBaseUrl } = require('./constants');
// Token刷新状态和请求队列
let isRefreshing = false;
let requestQueue = [];
function withAuthHeader(needAuth, header, useRefreshTokenOnly = false) {
const app = getApp();
if (!needAuth) return header;
const accessToken = app && app.globalData ? app.globalData.accessToken : '';
const refreshToken = app && app.globalData ? app.globalData.refreshToken : '';
header = header || {};
if (useRefreshTokenOnly && refreshToken) {
// 只使用 refresh token 模式(用于刷新)
header['X-Authorization'] = `Bearer ${refreshToken}`;
} else if (accessToken) {
// 正常模式:使用 access token
header.Authorization = `Bearer ${accessToken}`;
// 同时带上 refresh token,供后端在需要时自动刷新
if (refreshToken) {
header['X-Authorization'] = `Bearer ${refreshToken}`;
}
}
return header;
}
function normalizeError(payload) {
if (!payload) return new Error('Empty response');
// RESTfulResult usually has `errors` and/or `message`
if (typeof payload === 'string') return new Error(payload);
if (payload.errors) return new Error(payload.errors.toString());
if (payload.message) return new Error(payload.message);
return new Error(JSON.stringify(payload));
}
function getHeaderValue(headers, headerName) {
if (!headers || typeof headers !== 'object') return '';
const target = String(headerName || '').toLowerCase();
for (const k in headers) {
if (Object.prototype.hasOwnProperty.call(headers, k) && String(k).toLowerCase() === target) {
const v = headers[k];
return typeof v === 'string' ? v : '';
}
}
return '';
}
function tryUpdateAccessTokenFromResponseHeaders(res) {
// wx.request 的返回头在 res.header(部分环境也可能是 res.headers)
const headers = (res && res.header) || (res && res.headers) || null;
// 从响应头获取新的 token
const newAccessToken = getHeaderValue(headers, 'access-token') || getHeaderValue(headers, 'x-access-token');
const newRefreshToken = getHeaderValue(headers, 'x-access-token') || getHeaderValue(headers, 'x-refresh-token');
if (!newAccessToken && !newRefreshToken) return false;
const app = getApp();
const authData = {};
let hasUpdate = false;
if (newAccessToken && newAccessToken !== 'invalid_token') {
authData.accessToken = newAccessToken;
hasUpdate = true;
}
if (newRefreshToken && newRefreshToken !== 'invalid_token') {
authData.refreshToken = newRefreshToken;
hasUpdate = true;
}
if (hasUpdate) {
if (app && typeof app.setAuth === 'function') {
app.setAuth(authData);
} else {
if (authData.accessToken) {
wx.setStorageSync('accessToken', authData.accessToken);
if (app && app.globalData) app.globalData.accessToken = authData.accessToken;
}
if (authData.refreshToken) {
wx.setStorageSync('refreshToken', authData.refreshToken);
if (app && app.globalData) app.globalData.refreshToken = authData.refreshToken;
}
}
}
// 如果正在刷新Token且获取到了新Token,执行队列中的请求
if (isRefreshing && newAccessToken && newAccessToken !== 'invalid_token') {
isRefreshing = false;
requestQueue.forEach((callback) => callback(newAccessToken));
requestQueue = [];
return true;
}
return hasUpdate;
}
function clearAllTokensAndRedirect() {
wx.clearStorageSync();
const app = getApp();
if (app && app.globalData) {
app.globalData.accessToken = '';
app.globalData.refreshToken = '';
}
wx.showModal({
title: '登录失效',
content: '您的登录已失效,请重新登录。',
showCancel: false,
complete() {
wx.navigateTo({ url: '/subpackages/mine/login/login' });
}
});
}
function request({
path,
method = 'GET',
data = {},
needAuth = false,
contentType = 'application/json',
// Internal flag: prevent infinite retry when refresh token也失败
__retry = false,
// Internal flag: 标记这是刷新token的重试请求
__refreshRetry = false
}) {
const baseUrl = getBaseUrl();
const url = `${baseUrl}${path}`;
// 正常请求:同时带上 access token 和 refresh token
// 刷新重试请求:只使用 refresh token
const header = withAuthHeader(needAuth, {
'content-type': contentType
}, __refreshRetry);
return new Promise((resolve, reject) => {
wx.request({
url,
method: method.toUpperCase(),
data: method.toUpperCase() === 'GET' ? data : data,
header: header,
success: async (res) => {
// 如果后端在 Response Headers 下发新的 token,就替换本地 token
const tokenUpdated = tryUpdateAccessTokenFromResponseHeaders(res);
const statusCode = res && res.data && res.data.statusCode ? res.data.statusCode : 0;
const errors = res && res.data && res.data.errors ? res.data.errors : null;
const payload = res && res.data !== undefined ? res.data : null;
// ── 401 未授权:尝试刷新Token ──
if (statusCode === 401 && needAuth && !__retry) {
// 如果已经在刷新Token,将请求加入队列
if (isRefreshing) {
requestQueue.push((newToken) => {
resolve(request({ path, method, data, needAuth, contentType, __retry: true }));
});
return;
}
// 开始刷新Token
// 重发原请求
isRefreshing = true;
try {
// 检查是否有 refresh token
const app = getApp();
const refreshToken = app && app.globalData ? app.globalData.refreshToken : '';
if (!refreshToken) {
throw new Error('No refresh token available');
}
// 重发请求
await request({
path,
method,
data,
needAuth,
contentType,
__retry: false,
__refreshRetry: true
});
// 刷新成功,现在重试原请求
isRefreshing = false;
resolve(request({ path, method, data, needAuth, contentType, __retry: true }));
return;
} catch (refreshErr) {
// 刷新失败,清除Token并跳转登录
isRefreshing = false;
requestQueue = [];
clearAllTokensAndRedirect();
reject(new Error(errors));
return;
}
}
// ── 401 未授权(重试后仍然失败) ──
if (statusCode === 401) {
clearAllTokensAndRedirect();
reject(new Error(errors));
return;
}
// ── 403 禁止访问 ──
if ( statusCode === 400 || statusCode === 403 ) {
wx.showToast({ title: errors || '请求失败', icon: 'none' });
reject(new Error(errors));
return;
}
// ── 404 资源不存在 ───────────────────────────────────────────
if (statusCode === 404) {
wx.showToast({ title: errors || '请求资源不存在', icon: 'none' });
reject(new Error(errors));
return;
}
// ── 5xx 服务端错误 ───────────────────────────────────────────
if (statusCode >= 500) {
wx.showToast({ title: errors || '服务器异常,请稍后重试', icon: 'none' });
reject(new Error(errors));
return;
}
// ── 其他非 2xx ───────────────────────────────────────────────
if (statusCode !== 0 && (statusCode < 200 || statusCode >= 300)) {
wx.showToast({ title: errors || `请求失败(${statusCode})`, icon: 'none' });
reject(new Error(errors));
return;
}
// ── 2xx 成功:解析业务层 RESTfulResult ────────────────────────
if (payload && typeof payload === 'object' && typeof payload.succeeded === 'boolean') {
if (payload.succeeded) {
resolve(payload.data);
} else {
reject(normalizeError(payload));
}
return;
}
// ── Fallback:直接返回原始 data ───────────────────────────────
resolve(payload);
},
fail(err) {
// 网络请求失败处理
reject(err);
}
});
});
}
module.exports = {
request
};