const { BASE_URL } = require('./config.js'); const TOKEN_KEY = 'pets_token'; const REFRESH_KEY = 'pets_refresh_token'; function getToken() { return wx.getStorageSync(TOKEN_KEY) || ''; } function setToken(t) { wx.setStorageSync(TOKEN_KEY, t); } function getRefreshToken() { return wx.getStorageSync(REFRESH_KEY) || ''; } function setRefreshToken(t) { if (t) wx.setStorageSync(REFRESH_KEY, t); } function clearToken() { wx.removeStorageSync(TOKEN_KEY); wx.removeStorageSync(REFRESH_KEY); } // token 失效时的重新登录钩子,由 store 注册(放在这里避免 request ← api ← store 循环依赖) let reAuth = null; function setReAuthHandler(fn) { reAuth = fn; } const TIMEOUT = 15000; // access token 只有 2 小时,过期后先拿 refresh token 换新的(用户无感), // 换不到(刷新令牌也过期/被吊销)才退回 wx.login 重新登录。 // 单飞:并发请求同时 401 时只发一次刷新,否则刷新令牌会被并发轮换掉。 let refreshing = null; function refreshAccessToken() { if (!refreshing) { const rt = getRefreshToken(); refreshing = (rt ? request({ url: '/api/auth/refresh', method: 'POST', data: { refresh_token: rt }, noAuthRetry: true }) : Promise.reject(new Error('没有刷新令牌')) ).then((data) => { setToken(data.token); setRefreshToken(data.refresh_token); return data.token; }); const done = () => { refreshing = null; }; refreshing.then(done, done); } return refreshing; } // 恢复登录态:先续期,续不了就整套重新登录 function recoverAuth() { return refreshAccessToken().catch(() => { clearToken(); if (!reAuth) throw new Error('登录已过期'); return reAuth(); }); } // 统一请求:注入 token,解析 {code,message,data},成功返回 data,失败 reject(Error)。 // token 失效(40100)时自动续期/重新登录并重试一次,避免 JWT 过期后小程序静默白屏。 function request(options, _retried) { const { url, method = 'GET', data, header = {}, noAuthRetry } = options; return new Promise((resolve, reject) => { const token = getToken(); wx.request({ url: BASE_URL + url, method, data, timeout: TIMEOUT, header: Object.assign( { 'Content-Type': 'application/json' }, token ? { Authorization: 'Bearer ' + token } : {}, header, ), success(res) { const body = res.data; if (body && typeof body === 'object' && 'code' in body) { if (body.code === 0) return resolve(body.data); if (body.code === 40100) { // 只重试一次,防止刷新/登录接口本身出问题时无限循环 if (_retried || noAuthRetry) { clearToken(); return reject(new Error(body.message || '登录已过期,请重进小程序')); } return recoverAuth() .then(() => request(options, true).then(resolve, reject)) .catch(() => { clearToken(); reject(new Error(body.message || '登录已过期,请重进小程序')); }); } return reject(new Error(body.message || '请求失败')); } resolve(body); }, fail(err) { const msg = (err && err.errMsg) || '网络错误'; reject(new Error(/timeout/i.test(msg) ? '网络超时,请稍后重试' : msg)); }, }); }); } // 文件上传(multipart);token 过期同样先续期再重试一次 function uploadFile(filePath, _retried) { return new Promise((resolve, reject) => { const token = getToken(); wx.uploadFile({ url: BASE_URL + '/api/upload', filePath, name: 'file', timeout: 60000, header: token ? { Authorization: 'Bearer ' + token } : {}, success(res) { try { const body = JSON.parse(res.data); if (body.code === 0) return resolve(body.data); if (body.code === 40100 && !_retried) { return recoverAuth() .then(() => uploadFile(filePath, true).then(resolve, reject)) .catch(() => reject(new Error('登录已过期,请重进小程序'))); } reject(new Error(body.message || '上传失败')); } catch (e) { reject(new Error('上传响应解析失败')); } }, fail(err) { reject(new Error((err && err.errMsg) || '上传失败')); }, }); }); } module.exports = { request, uploadFile, getToken, setToken, getRefreshToken, setRefreshToken, clearToken, setReAuthHandler, };