import { refresh as refreshRequest } from '@/api/auth.js' import { isAccessTokenExpiring, session, signOut, updateTokenPair, getSessionRevision } from '@/stores/session.js' import { requireOnline } from '@/utils/network.js' let refreshPromise = null let refreshRevision = null let redirectingToLogin = false function responseCode(response) { return String(response?.data?.code ?? response?.statusCode ?? '') } function isUnauthorized(responseOrError) { const statusCode = Number(responseOrError?.statusCode ?? responseOrError?.response?.statusCode ?? 0) const code = String(responseOrError?.code ?? responseCode(responseOrError)) return statusCode === 401 || code === '401' || code === 'A0401' } function sessionExpiredError(source) { const error = new Error('登录已过期,请重新登录') error.code = source?.code || 'A0401' error.statusCode = source?.statusCode || 401 return error } function redirectAfterSessionExpiry() { if (redirectingToLogin) return redirectingToLogin = true if (typeof uni !== 'undefined' && typeof uni.showToast === 'function') { uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 2200 }) } if (typeof uni !== 'undefined' && typeof uni.reLaunch === 'function') { uni.reLaunch({ url: '/pages/login/login', complete() { setTimeout(() => { redirectingToLogin = false }, 1000) } }) } } function expireSession(source, observedRefreshToken = session.refreshToken) { if (observedRefreshToken && session.refreshToken && observedRefreshToken !== session.refreshToken) return session.token const hadSession = Boolean(session.currentUser || session.token || session.refreshToken) signOut() if (hadSession) redirectAfterSessionExpiry() throw sessionExpiredError(source) } function networkError(error, fallback = '网络连接失败') { const next = new Error(error?.errMsg || fallback) next.network = true return next } function rawRequest(options, accessToken) { const baseUrl = session.apiBase || '/api/platform/v1' const headers = { ...(options.header || {}) } if (accessToken) headers.Authorization = `Bearer ${accessToken}` if (session.requestId) headers['X-Request-Id'] = session.requestId return new Promise((resolve, reject) => { uni.request({ ...options, url: `${baseUrl}${options.url}`, header: headers, success: resolve, fail: (error) => reject(networkError(error)) }) }) } function requireSameSession(revision) { if (getSessionRevision() !== revision) throw new Error('登录会话已变更,请重新操作') } async function refreshAccessToken() { const revision = getSessionRevision() const observedRefreshToken = session.refreshToken if (!observedRefreshToken) return expireSession(null, observedRefreshToken) if (!refreshPromise || refreshRevision !== revision) { refreshRevision = revision refreshPromise = (async () => { if (session.refreshToken !== observedRefreshToken && session.token) return session.token try { const tokens = await refreshRequest(observedRefreshToken) requireSameSession(revision) if (session.refreshToken !== observedRefreshToken) { if (session.token) return session.token throw sessionExpiredError() } updateTokenPair(tokens) redirectingToLogin = false return session.token } catch (error) { requireSameSession(revision) if (isUnauthorized(error)) return expireSession(error, observedRefreshToken) throw error } })() const pending = refreshPromise const release = () => { if (refreshPromise === pending) { refreshPromise = null; refreshRevision = null } } pending.then(release, release) // Original pending promise still propagates failures to callers. } return refreshPromise } async function validAccessToken() { if (!session.token || isAccessTokenExpiring(session.token)) return refreshAccessToken() return session.token } export function ensureAccessToken() { return validAccessToken() } async function runAuthenticated(execute) { requireOnline() const revision = getSessionRevision() let accessToken = await validAccessToken() requireSameSession(revision) let response = await execute(accessToken) requireSameSession(revision) if (!isUnauthorized(response)) return response accessToken = session.token && session.token !== accessToken ? session.token : await refreshAccessToken() requireSameSession(revision) const activeRefreshToken = session.refreshToken response = await execute(accessToken) requireSameSession(revision) if (isUnauthorized(response)) expireSession(response, activeRefreshToken) return response } export function apiRequest(options) { return runAuthenticated((accessToken) => rawRequest(options, accessToken)).then((response) => { const body = response.data || {} const code = responseCode(response) if (code !== '200') { const error = new Error(body.message || '服务端请求失败') error.code = body.code || response.statusCode error.statusCode = response.statusCode error.requestId = body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id'] error.traceId = body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id'] throw error } return body.data }) } export function apiDownload(options) { return runAuthenticated((accessToken) => new Promise((resolve, reject) => { const headers = { ...(options.header || {}) } if (accessToken) headers.Authorization = `Bearer ${accessToken}` if (session.requestId) headers['X-Request-Id'] = session.requestId uni.downloadFile({ ...options, header: headers, success: resolve, fail: (error) => reject(networkError(error, '文件下载失败')) }) })) } export function apiUpload(options, onProgress) { return runAuthenticated((accessToken) => new Promise((resolve, reject) => { const headers = { ...(options.header || {}) } if (accessToken) headers.Authorization = `Bearer ${accessToken}` if (session.requestId) headers['X-Request-Id'] = session.requestId const task = uni.uploadFile({ ...options, header: headers, success: resolve, fail: (error) => reject(networkError(error)) }) if (task && typeof task.onProgressUpdate === 'function' && typeof onProgress === 'function') task.onProgressUpdate(({ progress }) => onProgress(progress)) })) } export { isUnauthorized }