import config from '@/config/index.js' import { clearJPushAliasBinding } from '@/utils/push.js' import { clearAuthSession, getStoredToken, hasBoundPhone, hasPendingPhoneBindSession, saveAuthSession, willTokenExpireWithin, } from '@/utils/auth-session.js' const GET = 'GET' const POST = 'POST' const PUT = 'PUT' const FORM = 'FORM' const DELETE = 'DELETE' const baseURL = config.BASE_URL const TOKEN_REFRESH_BUFFER_MS = 30 * 60 * 1000 const SUCCESS_CODES = [0, 200] const UNAUTHORIZED_CODES = [401, 504, 505, 510] const SESSION_INVALID_MESSAGES = ['用户不存在', '用户已禁用'] const PENDING_BIND_ALLOWED_URLS = [ '/api/app/settings/phone/code', '/api/app/settings/phone/bind', ] const PUBLIC_APP_URL_PREFIXES = [ '/api/app/home/', '/api/v1/app/home/', ] const waitingList = [] const excutingList = [] let appAuthRedirecting = false let refreshTokenPromise = null /** * 判断当前响应码是否属于成功态。 */ function isSuccessCode(code) { return SUCCESS_CODES.includes(code) } /** * 从响应体中提取可展示的错误文案。 */ function resolveMessage(payload) { if (!payload) { return '服务器错误' } if (typeof payload.data === 'string' && payload.data) { return payload.data } return payload.msg || payload.message || '服务器错误' } /** * 后端部分已登录接口会以业务错误返回“用户不存在/已禁用”,这里统一视为登录态失效。 */ function isSessionInvalidPayload(payload) { if (!getStoredToken()) { return false } const message = resolveMessage(payload).trim() return SESSION_INVALID_MESSAGES.includes(message) } /** * 读取当前语言请求头值。 */ function resolveLangHeader() { return uni.getStorageSync('locale') === 'zh-Hans' ? 'zh_CN' : 'zh-tw' } /** * 构造当前请求头。 */ function buildHeaders(type, url) { const authorization = resolveAuthorizationHeader(url) const headers = { 'content-type': type ? 'application/x-www-form-urlencoded;charset=UTF-8' : 'application/json', lang: resolveLangHeader(), } if (authorization) { headers.Authorization = authorization } return headers } /** * 解析请求鉴权头,待绑定手机号临时 token 仅允许访问绑定手机号接口。 */ function resolveAuthorizationHeader(url) { if (isPublicAppUrl(url)) { return '' } if (!hasPendingPhoneBindSession()) { return getStoredToken() } return isPendingBindAllowedUrl(url) ? getStoredToken() : '' } /** * 首页公共接口允许游客访问,不能被本地旧登录态或 token 刷新失败影响。 */ function isPublicAppUrl(url) { return PUBLIC_APP_URL_PREFIXES.some((prefix) => url.startsWith(prefix)) } /** * 判断当前接口是否允许使用待绑定手机号临时 token。 */ function isPendingBindAllowedUrl(url) { return PENDING_BIND_ALLOWED_URLS.includes(url) } /** * 统一展示普通业务错误提示。 */ function showErrorToast(payload) { uni.showToast({ title: resolveMessage(payload), duration: 2000, icon: 'none', mask: true, }) } /** * 统一发起请求,并兼容当前仓库 `code=0` 的后端成功协议。 */ function request(method, url, data, type, lodingFlag = true) { return new Promise((resolve, reject) => { const executeRequest = () => { if (lodingFlag) { uni.showLoading({ mask: true, }) } const requestTask = uni.request({ url: `${baseURL}${url}`, method, data, header: buildHeaders(type, url), success: (res) => { if (lodingFlag) { uni.hideLoading() } const payload = res.data || {} if (isSuccessCode(payload.code)) { resolve(payload) return } if (payload.code === 502 || payload.code === 501) { showErrorToast(payload) resolve(payload) return } if (isSessionInvalidPayload(payload)) { handleUnauthorized(payload) resolve(payload) return } if (payload.code === 1 || payload.code === 500) { showErrorToast(payload) resolve(payload) return } if (UNAUTHORIZED_CODES.includes(payload.code)) { handleUnauthorized(payload) resolve(payload) return } if (payload.code === 506) { resolve(payload) return } showErrorToast(payload) resolve(payload) }, fail: (error) => { if (lodingFlag) { uni.hideLoading() } reject(error) }, complete: () => { const excutingIndex = excutingList.findIndex((item) => item === requestTask) if (excutingIndex >= 0) { excutingList.splice(excutingIndex, 1) } const [apiFn] = waitingList if (excutingList.length === 0 && typeof apiFn === 'function') { apiFn() waitingList.splice(0, 1) } }, }) excutingList.push(requestTask) } ensureSessionFresh(url) .then(() => { executeRequest() }) .catch((error) => { reject(error) }) }) } /** * 收到未登录响应时清理 APP 登录态并跳转登录页。 */ function handleUnauthorized(payload) { const routeStr = getCurrentRoute() clearLoginSession() clearAllRequest() if (routeStr === 'pages/login/login' || appAuthRedirecting) { return } appAuthRedirecting = true uni.showToast({ title: resolveMessage(payload) || '请先登录', duration: 1500, icon: 'none', mask: true, }) setTimeout(() => { uni.reLaunch({ url: '/pages/login/login', }) setTimeout(() => { appAuthRedirecting = false }, 300) }, 150) } /** * 清除 APP 登录态缓存。 */ function clearLoginSession() { clearAuthSession() clearJPushAliasBinding() } /** * 获取当前页面路由,便于避免在登录页重复跳转。 */ function getCurrentRoute() { const currentPages = getCurrentPages() if (!currentPages || currentPages.length === 0) { return '' } return currentPages[currentPages.length - 1].route || '' } /** * 清除请求队列,避免登录失效后旧请求继续回写界面状态。 */ function clearAllRequest() { excutingList.forEach((item) => { if (item && typeof item.abort === 'function') { item.abort() } }) excutingList.length = 0 waitingList.length = 0 } /** * 在请求发送前按阈值串行刷新 APP token,避免多个接口同时触发续期。 */ function ensureSessionFresh(url) { if (!shouldAttemptTokenRefresh(url)) { return Promise.resolve() } if (refreshTokenPromise) { return refreshTokenPromise } refreshTokenPromise = refreshAppToken().finally(() => { refreshTokenPromise = null }) return refreshTokenPromise } /** * 判断当前请求是否需要在发送前尝试刷新 token。 */ function shouldAttemptTokenRefresh(url) { if (isPublicAppUrl(url)) { return false } const token = getStoredToken() if (!token || !hasBoundPhone()) { return false } if (url === '/api/app/auth/refresh-token' || url === '/api/v1/app/auth/refresh-token') { return false } const routeStr = getCurrentRoute() if (routeStr === 'pages/login/login' || routeStr === 'pages/register/register' || routeStr === 'pages/bindPhone/bindPhone') { return false } return willTokenExpireWithin(TOKEN_REFRESH_BUFFER_MS) } /** * 调用后端续期接口并回写本地 token 与用户信息。 */ function refreshAppToken() { return new Promise((resolve, reject) => { uni.request({ url: `${baseURL}/api/app/auth/refresh-token`, method: POST, data: {}, header: { 'content-type': 'application/json', Authorization: getStoredToken(), lang: resolveLangHeader(), }, success: (res) => { const payload = res.data || {} if (isSuccessCode(payload.code) && payload.data) { saveAuthSession(payload.data) resolve(payload) return } if (UNAUTHORIZED_CODES.includes(payload.code) || isSessionInvalidPayload(payload)) { handleUnauthorized(payload) } reject(payload) }, fail: (error) => { reject(error) }, }) }) } /** * 发起 GET 请求。 */ function get(url, data, type = false, lodingFlag = true) { return request(GET, url, data, type, lodingFlag) } /** * 发起 POST 请求。 */ function post(url, data, type = false, lodingFlag = true) { return request(POST, url, data, type, lodingFlag) } /** * 发起 PUT 请求。 */ function put(url, data, type = false, lodingFlag = true) { return request(PUT, url, data, type, lodingFlag) } /** * 发起表单编码请求。 */ function form(url, data, type = false, lodingFlag = true) { return request(FORM, url, data, type, lodingFlag) } /** * 发起 DELETE 请求。 */ function remove(url, data, type = false, lodingFlag = true) { return request(DELETE, url, data, type, lodingFlag) } export default { get, post, put, FORM: form, delete: remove, }