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,
|
}
|