2026-03-30 18:08:27 +08:00
|
|
|
|
const { getBaseUrl } = require('./constants');
|
|
|
|
|
|
|
2026-04-07 23:50:41 +08:00
|
|
|
|
// Token刷新状态和请求队列
|
|
|
|
|
|
let isRefreshing = false;
|
|
|
|
|
|
let requestQueue = [];
|
|
|
|
|
|
|
|
|
|
|
|
function withAuthHeader(needAuth, header, useRefreshTokenOnly = false) {
|
2026-03-30 18:08:27 +08:00
|
|
|
|
const app = getApp();
|
|
|
|
|
|
if (!needAuth) return header;
|
2026-04-07 23:50:41 +08:00
|
|
|
|
const accessToken = app && app.globalData ? app.globalData.accessToken : '';
|
2026-04-03 17:57:08 +08:00
|
|
|
|
const refreshToken = app && app.globalData ? app.globalData.refreshToken : '';
|
2026-04-07 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
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}`;
|
|
|
|
|
|
}
|
2026-03-30 18:08:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:57:08 +08:00
|
|
|
|
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;
|
2026-04-07 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 从响应头获取新的 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;
|
2026-04-03 17:57:08 +08:00
|
|
|
|
|
|
|
|
|
|
const app = getApp();
|
2026-04-07 23:50:41 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-03 17:57:08 +08:00
|
|
|
|
}
|
2026-04-07 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果正在刷新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' });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-04-03 17:57:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-30 18:08:27 +08:00
|
|
|
|
function request({
|
|
|
|
|
|
path,
|
|
|
|
|
|
method = 'GET',
|
|
|
|
|
|
data = {},
|
|
|
|
|
|
needAuth = false,
|
2026-04-03 17:57:08 +08:00
|
|
|
|
contentType = 'application/json',
|
|
|
|
|
|
// Internal flag: prevent infinite retry when refresh token也失败
|
2026-04-07 23:50:41 +08:00
|
|
|
|
__retry = false,
|
|
|
|
|
|
// Internal flag: 标记这是刷新token的重试请求
|
|
|
|
|
|
__refreshRetry = false
|
2026-03-30 18:08:27 +08:00
|
|
|
|
}) {
|
|
|
|
|
|
const baseUrl = getBaseUrl();
|
|
|
|
|
|
const url = `${baseUrl}${path}`;
|
|
|
|
|
|
|
2026-04-07 23:50:41 +08:00
|
|
|
|
// 正常请求:同时带上 access token 和 refresh token
|
|
|
|
|
|
// 刷新重试请求:只使用 refresh token
|
2026-03-30 18:08:27 +08:00
|
|
|
|
const header = withAuthHeader(needAuth, {
|
|
|
|
|
|
'content-type': contentType
|
2026-04-07 23:50:41 +08:00
|
|
|
|
}, __refreshRetry);
|
2026-03-30 18:08:27 +08:00
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
wx.request({
|
|
|
|
|
|
url,
|
|
|
|
|
|
method: method.toUpperCase(),
|
|
|
|
|
|
data: method.toUpperCase() === 'GET' ? data : data,
|
|
|
|
|
|
header: header,
|
2026-04-04 18:57:50 +08:00
|
|
|
|
success: async (res) => {
|
2026-04-03 17:57:08 +08:00
|
|
|
|
// 如果后端在 Response Headers 下发新的 token,就替换本地 token
|
2026-04-07 23:50:41 +08:00
|
|
|
|
const tokenUpdated = tryUpdateAccessTokenFromResponseHeaders(res);
|
2026-04-04 18:57:50 +08:00
|
|
|
|
const statusCode = res && res.data && res.data.statusCode ? res.data.statusCode : 0;
|
2026-04-10 01:54:11 +08:00
|
|
|
|
const errors = res && res.data && res.data.errors ? res.data.errors : null;
|
2026-03-30 18:08:27 +08:00
|
|
|
|
const payload = res && res.data !== undefined ? res.data : null;
|
|
|
|
|
|
|
2026-04-07 23:50:41 +08:00
|
|
|
|
// ── 401 未授权:尝试刷新Token ──
|
|
|
|
|
|
if (statusCode === 401 && needAuth && !__retry) {
|
|
|
|
|
|
// 如果已经在刷新Token,将请求加入队列
|
|
|
|
|
|
if (isRefreshing) {
|
|
|
|
|
|
requestQueue.push((newToken) => {
|
|
|
|
|
|
resolve(request({ path, method, data, needAuth, contentType, __retry: true }));
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
2026-04-04 18:57:50 +08:00
|
|
|
|
}
|
2026-04-07 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 开始刷新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');
|
2026-04-04 18:57:50 +08:00
|
|
|
|
}
|
2026-04-07 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 重发请求
|
|
|
|
|
|
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();
|
2026-04-10 16:10:33 +08:00
|
|
|
|
reject(new Error(errors));
|
2026-04-07 23:50:41 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 401 未授权(重试后仍然失败) ──
|
|
|
|
|
|
if (statusCode === 401) {
|
|
|
|
|
|
clearAllTokensAndRedirect();
|
2026-04-10 16:10:33 +08:00
|
|
|
|
reject(new Error(errors));
|
2026-04-04 18:57:50 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 23:50:41 +08:00
|
|
|
|
// ── 403 禁止访问 ──
|
2026-04-13 23:20:50 +08:00
|
|
|
|
if ( statusCode == 400 || statusCode == 403 ) {
|
2026-04-10 16:10:33 +08:00
|
|
|
|
wx.showToast({ title: errors || '请求失败', icon: 'none' });
|
|
|
|
|
|
reject(new Error(errors));
|
2026-04-04 18:57:50 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 404 资源不存在 ───────────────────────────────────────────
|
2026-04-13 23:56:07 +08:00
|
|
|
|
if (statusCode == 404) {
|
2026-04-10 01:54:11 +08:00
|
|
|
|
wx.showToast({ title: errors || '请求资源不存在', icon: 'none' });
|
2026-04-10 16:10:33 +08:00
|
|
|
|
reject(new Error(errors));
|
2026-04-04 18:57:50 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 5xx 服务端错误 ───────────────────────────────────────────
|
|
|
|
|
|
if (statusCode >= 500) {
|
2026-04-10 01:54:11 +08:00
|
|
|
|
wx.showToast({ title: errors || '服务器异常,请稍后重试', icon: 'none' });
|
2026-04-10 16:10:33 +08:00
|
|
|
|
reject(new Error(errors));
|
2026-04-04 18:57:50 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 其他非 2xx ───────────────────────────────────────────────
|
|
|
|
|
|
if (statusCode !== 0 && (statusCode < 200 || statusCode >= 300)) {
|
2026-04-10 01:54:11 +08:00
|
|
|
|
wx.showToast({ title: errors || `请求失败(${statusCode})`, icon: 'none' });
|
2026-04-10 16:10:33 +08:00
|
|
|
|
reject(new Error(errors));
|
2026-04-04 18:57:50 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 2xx 成功:解析业务层 RESTfulResult ────────────────────────
|
2026-03-30 18:08:27 +08:00
|
|
|
|
if (payload && typeof payload === 'object' && typeof payload.succeeded === 'boolean') {
|
2026-04-04 18:57:50 +08:00
|
|
|
|
if (payload.succeeded) {
|
|
|
|
|
|
resolve(payload.data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
reject(normalizeError(payload));
|
|
|
|
|
|
}
|
2026-03-30 18:08:27 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 18:57:50 +08:00
|
|
|
|
// ── Fallback:直接返回原始 data ───────────────────────────────
|
2026-03-30 18:08:27 +08:00
|
|
|
|
resolve(payload);
|
|
|
|
|
|
},
|
|
|
|
|
|
fail(err) {
|
2026-04-07 23:50:41 +08:00
|
|
|
|
// 网络请求失败处理
|
2026-03-30 18:08:27 +08:00
|
|
|
|
reject(err);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
|
request
|
|
|
|
|
|
};
|
|
|
|
|
|
|