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)。三重兜底,应对「第一次报错、重试就好」的偶发: // 1) 上传前若还没 token(冷启动竞态),先续期拿到再传; // 2) token 过期(40100)→ 续期后重试; // 3) 网络 fail / 网关 5xx(响应非 JSON)等瞬时错误 → 自动重试一次。 // 后端按 MD5 去重,重复上传同一张图返回同一条记录,重试是安全的。 function uploadFile(filePath, _retried) { return new Promise((resolve, reject) => { // 瞬时错误:还没重试过就延后再传一次,否则直接失败 const softRetry = (err) => { if (_retried) return reject(err); setTimeout(() => uploadFile(filePath, true).then(resolve, reject), 500); }; const fire = () => { const token = getToken(); wx.uploadFile({ url: BASE_URL + '/api/upload', filePath, name: 'file', timeout: 60000, header: token ? { Authorization: 'Bearer ' + token } : {}, success(res) { let body; try { body = JSON.parse(res.data); } catch (e) { // 非 JSON 多为网关 5xx / 服务重启窗口,按瞬时错误重试 return softRetry(new Error('上传失败,请重试')); } 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 || '上传失败')); }, fail(err) { softRetry(new Error((err && err.errMsg) || '上传失败')); }, }); }; // 冷启动竞态:点得太快、登录还没完成就上传,先把 token 拿到再传 if (!getToken() && !_retried) { return recoverAuth().then(fire, fire); } fire(); }); } module.exports = { request, uploadFile, getToken, setToken, getRefreshToken, setRefreshToken, clearToken, setReAuthHandler, };