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; } /** 保留 statusCode,便于调用方按 401/403 等状态做差异化提示 */ function httpError(message, statusCode) { const error = new Error(message || `请求失败(${statusCode})`); error.statusCode = statusCode; return error; } 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' }); } }); } /** * 清理请求参数,避免微信请求层把 undefined/null 转成 URL 中的字符串。 * GET 会移除无法被后端模型绑定的空参数;POST 仅移除 undefined,保留合法 null。 */ function sanitizeRequestData(value, isGet) { if (value === undefined) return undefined; if (isGet && (value === null || value === 'undefined' || value === 'null')) return undefined; if (typeof value === 'number' && !Number.isFinite(value)) return undefined; if (Array.isArray(value)) { return value .map((item) => sanitizeRequestData(item, isGet)) .filter((item) => item !== undefined); } if (value && typeof value === 'object') { const result = {}; Object.keys(value).forEach((key) => { const cleaned = sanitizeRequestData(value[key], isGet); if (cleaned !== undefined) result[key] = cleaned; }); return result; } return value; } 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}`; const upperMethod = method.toUpperCase(); const requestData = sanitizeRequestData(data, upperMethod === 'GET'); // 正常请求:同时带上 access token 和 refresh token // 刷新重试请求:只使用 refresh token const header = withAuthHeader(needAuth, { 'content-type': contentType }, __refreshRetry); return new Promise((resolve, reject) => { wx.request({ url, method: upperMethod, data: requestData, 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(httpError(errors, statusCode)); return; } } // ── 401 未授权(重试后仍然失败) ── if (statusCode === 401) { clearAllTokensAndRedirect(); reject(httpError(errors, statusCode)); return; } // ── 403 禁止访问 ── if ( statusCode == 400 || statusCode == 403 ) { wx.showToast({ title: errors || '请求失败', icon: 'none' }); reject(httpError(errors, statusCode)); return; } // ── 404 资源不存在 ─────────────────────────────────────────── if (statusCode == 404) { wx.showToast({ title: errors || '请求资源不存在', icon: 'none' }); reject(httpError(errors || '请求资源不存在', statusCode)); return; } // ── 5xx 服务端错误 ─────────────────────────────────────────── if (statusCode >= 500) { wx.showToast({ title: errors || '服务器异常,请稍后重试', icon: 'none' }); reject(httpError(errors || '服务器异常,请稍后重试', statusCode)); return; } // ── 其他非 2xx ─────────────────────────────────────────────── if (statusCode !== 0 && (statusCode < 200 || statusCode >= 300)) { wx.showToast({ title: errors || `请求失败(${statusCode})`, icon: 'none' }); reject(httpError(errors, statusCode)); 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 };