/** * 统一请求封装:baseURL、Bearer Token、业务 code 处理。 * 接口地址统一从 .env 的 VITE_API_BASE_URL 读取(见 api/config.js)。 */ import { getToken, logout, isSSOEnabled, getRefreshToken, setAuthTokens } from '@/utils/auth.js' import { resolveBaseURL, getBaseURL } from '@/api/config.js' export { getBaseURL } function buildFullUrl(url) { return resolveBaseURL().replace(/\/$/, '') + url } /** * 用 refresh_token 换取新的访问令牌(独立封装,避免与 request 递归调用)。 * 仅在统一认证模式(VITE_AUTH_MODE=sso)下使用。 */ function refreshAccessToken() { return new Promise((resolve, reject) => { const refresh = getRefreshToken() if (!refresh) { reject(new Error('无刷新令牌')) return } uni.request({ url: buildFullUrl('/auth/token'), method: 'POST', header: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refresh)}&client_id=yz-uniapp`, timeout: 10000, success(res) { const body = res.data || {} if (res.statusCode === 200 && body.access_token) { setAuthTokens(body) resolve(body.access_token) } else { reject(new Error((body && (body.error || body.msg)) || '刷新失败')) } }, fail: reject }) }) } /** 续期后重放原请求,返回与 request 一致的 data 解包结果 */ async function replayRequest(options) { const token = await refreshAccessToken() const res = await new Promise((resolve, reject) => { uni.request({ url: buildFullUrl(options.url), method: (options.method || 'GET').toUpperCase(), data: options.data || {}, header: Object.assign( { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, options.header || {} ), timeout: options.timeout || 30000, success: (r) => resolve(r.data || {}), fail: reject }) }) if (typeof res.code !== 'undefined' && res.code !== 200) { const err = new Error(res.msg || '操作失败') err.code = res.code err.response = res throw err } return typeof res.data !== 'undefined' ? res.data : res } /** * @param {{ url: string, method?: string, data?: object, header?: object, silent?: boolean }} options * @returns {Promise} 业务 data 字段(若无 data 则返回整包) */ export function request(options = {}) { const { url, method = 'GET', data, header = {}, silent = false, timeout = 30000 } = options const token = getToken() const headers = { 'Content-Type': 'application/json', ...header } if (token) { headers.Authorization = `Bearer ${token}` } const fullUrl = buildFullUrl(url) return new Promise((resolve, reject) => { uni.request({ url: fullUrl, method: method.toUpperCase(), data: data || {}, header: headers, timeout, async success(res) { const status = res.statusCode const body = res.data || {} if (status === 401 || body.code === 401) { // 统一认证模式:先用 refresh_token 静默续期并重放请求, // 续期失败(令牌过期/被吊销/被踢下线)才真正登出。 if (isSSOEnabled()) { try { resolve(await replayRequest(options)) return } catch (e) { // 续期失败,继续走下面的登出流程 } } logout() if (!silent) { uni.showToast({ title: body.msg || '请重新登录', icon: 'none' }) } setTimeout(() => { uni.reLaunch({ url: '/pages/login/login' }) }, 400) reject(new Error(body.msg || '未授权')) return } if (status < 200 || status >= 300) { const msg = body.msg || `请求失败(${status})` if (!silent) uni.showToast({ title: msg, icon: 'none' }) reject(new Error(msg)) return } // 统一后端 { code, msg, data } if (typeof body.code !== 'undefined' && body.code !== 200) { const msg = body.msg || '操作失败' if (!silent) uni.showToast({ title: msg, icon: 'none' }) const err = new Error(msg) err.code = body.code err.response = body reject(err) return } resolve(typeof body.data !== 'undefined' ? body.data : body) }, fail(err) { const detail = (err && err.errMsg) || '' console.error('[request fail]', fullUrl, detail) if (!silent) { uni.showToast({ title: detail.includes('timeout') ? '请求超时' : '网络异常,请检查接口地址', icon: 'none' }) } reject(new Error(detail || '网络异常')) } }) }) } /** 返回完整响应体(含 code/msg),用于需要自行判断 code 的场景 */ export function requestRaw(options = {}) { const { url, method = 'GET', data, header = {} } = options const token = getToken() const headers = { 'Content-Type': 'application/json', ...header } if (token) { headers.Authorization = `Bearer ${token}` } const fullUrl = buildFullUrl(url) return new Promise((resolve, reject) => { uni.request({ url: fullUrl, method: method.toUpperCase(), data: data || {}, header: headers, timeout: 30000, success(res) { resolve(res.data || {}) }, fail(err) { console.error('[requestRaw fail]', fullUrl, err) reject(err) } }) }) } export function generateId() { return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}` }