feat: 优化
This commit is contained in:
@@ -24,8 +24,7 @@ const WHITE_LIST = [
|
||||
* 检查 URL 是否在白名单中
|
||||
*/
|
||||
function isInWhiteList(url) {
|
||||
if (!url) return false
|
||||
return WHITE_LIST.some((path) => url.includes(path))
|
||||
return !!(url && WHITE_LIST.some(path => url.includes(path)))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,23 +58,34 @@ function onRefreshed() {
|
||||
* 注意:只做清理工作,不处理重定向(重定向由上层回调处理)
|
||||
*/
|
||||
let isHandling401 = false
|
||||
let handling401Timeout = null
|
||||
|
||||
function handle401Error(error) {
|
||||
if (isHandling401) return
|
||||
|
||||
isHandling401 = true
|
||||
|
||||
try {
|
||||
// 清空token
|
||||
tokenManager.clearTokens()
|
||||
console.warn('Token已清空,因401错误')
|
||||
} catch (e) {
|
||||
console.error('清空 token 失败:', e)
|
||||
// 防止重复处理
|
||||
if (isHandling401 || error?._handled) {
|
||||
return
|
||||
}
|
||||
|
||||
// 延迟重置标志
|
||||
setTimeout(() => {
|
||||
isHandling401 = true
|
||||
error && (error._handled = true)
|
||||
|
||||
try {
|
||||
tokenManager.clearTokens()
|
||||
const message = error?.needRelogin
|
||||
? '[HTTP] Token已清空,refreshToken无效需要重新登录'
|
||||
: '[HTTP] Token已清空,因401错误'
|
||||
console.warn(message)
|
||||
} catch (e) {
|
||||
console.error('[HTTP] 清空 token 失败:', e)
|
||||
}
|
||||
|
||||
if (handling401Timeout) {
|
||||
clearTimeout(handling401Timeout)
|
||||
}
|
||||
|
||||
handling401Timeout = setTimeout(() => {
|
||||
isHandling401 = false
|
||||
}, 2000)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,66 +127,71 @@ export function createClientAxios(options = {}) {
|
||||
// 检查是否需要认证
|
||||
const needToken = config.headers?.isToken !== false && !isInWhiteList(config.url || '')
|
||||
|
||||
if (needToken) {
|
||||
// 检查 token 是否即将过期(30秒缓冲)
|
||||
const BUFFER_TIME = 30 * 1000
|
||||
const currentToken = tokenManager.getAccessToken()
|
||||
if (!needToken) {
|
||||
return config
|
||||
}
|
||||
|
||||
if (!currentToken) {
|
||||
console.warn('[Token] 没有可用的 accessToken')
|
||||
// 检查 token 是否即将过期(30秒缓冲)
|
||||
const BUFFER_TIME = 30 * 1000
|
||||
const currentToken = tokenManager.getAccessToken()
|
||||
|
||||
if (!currentToken) {
|
||||
console.warn('[Token] 没有可用的 accessToken')
|
||||
return config
|
||||
}
|
||||
|
||||
const isTokenExpired = tokenManager.isExpired(BUFFER_TIME)
|
||||
|
||||
if (!isTokenExpired) {
|
||||
// Token 未过期,直接添加 Authorization 头
|
||||
const authHeader = tokenManager.getAuthHeader()
|
||||
if (authHeader) {
|
||||
config.headers.Authorization = authHeader
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// Token 即将过期,需要刷新
|
||||
console.info('[Token] Token刷新')
|
||||
|
||||
// 如果不在刷新过程中,启动刷新
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true
|
||||
|
||||
if (!refreshTokenFn || typeof refreshTokenFn !== 'function') {
|
||||
console.warn('[Token] 未提供刷新函数,跳过刷新')
|
||||
isRefreshing = false
|
||||
onRefreshed()
|
||||
return config
|
||||
}
|
||||
|
||||
const isTokenExpired = tokenManager.isExpired(BUFFER_TIME)
|
||||
|
||||
if (isTokenExpired) {
|
||||
console.info('[Token] Token刷新')
|
||||
|
||||
// 如果不在刷新过程中,启动刷新
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true
|
||||
|
||||
// 执行刷新(使用上层传入的刷新函数)
|
||||
if (refreshTokenFn && typeof refreshTokenFn === 'function') {
|
||||
refreshTokenFn()
|
||||
.then(() => {
|
||||
console.info('[Token] 刷新成功')
|
||||
isRefreshing = false
|
||||
onRefreshed()
|
||||
})
|
||||
.catch((error) => {
|
||||
isRefreshing = false
|
||||
onRefreshed()
|
||||
console.error('[Token] 刷新失败:', error.message)
|
||||
})
|
||||
} else {
|
||||
console.warn('[Token] 未提供刷新函数,跳过刷新')
|
||||
isRefreshing = false
|
||||
onRefreshed()
|
||||
}
|
||||
}
|
||||
|
||||
// 等待刷新完成
|
||||
return new Promise((resolve) => {
|
||||
subscribeTokenRefresh(() => {
|
||||
// 刷新完成后,重新获取 token 并添加到请求头
|
||||
const authHeader = tokenManager.getAuthHeader()
|
||||
if (authHeader) {
|
||||
config.headers.Authorization = authHeader
|
||||
}
|
||||
resolve(config)
|
||||
})
|
||||
refreshTokenFn()
|
||||
.then(() => {
|
||||
console.info('[Token] 刷新成功')
|
||||
isRefreshing = false
|
||||
onRefreshed()
|
||||
})
|
||||
} else {
|
||||
// Token 未过期,直接添加 Authorization 头
|
||||
.catch((error) => {
|
||||
isRefreshing = false
|
||||
onRefreshed()
|
||||
console.error('[Token] 刷新失败:', error.message)
|
||||
|
||||
if (error.code === 401 && error.needRelogin) {
|
||||
console.info('[Token] refreshToken无效,需要重新登录')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 等待刷新完成
|
||||
return new Promise((resolve) => {
|
||||
subscribeTokenRefresh(() => {
|
||||
const authHeader = tokenManager.getAuthHeader()
|
||||
if (authHeader) {
|
||||
config.headers.Authorization = authHeader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
resolve(config)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
@@ -195,31 +210,24 @@ export function createClientAxios(options = {}) {
|
||||
error.code = data?.code
|
||||
error.data = data
|
||||
|
||||
// 业务码 401/403 只在响应拦截器处理,避免重复处理
|
||||
if (data.code === 401 && typeof on401 === 'function') {
|
||||
on401(error)
|
||||
}
|
||||
|
||||
// 业务码 403 只在响应拦截器处理,避免重复处理
|
||||
if (data.code === 403 && typeof on403 === 'function') {
|
||||
// 使用通用的权限不足错误,不使用后端返回的 message
|
||||
const forbiddenError = new Error('权限不足,无法访问该资源')
|
||||
forbiddenError.code = 403
|
||||
on403(forbiddenError)
|
||||
}
|
||||
|
||||
// 抛出错误,业务代码可以捕获
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
(error) => {
|
||||
// HTTP 状态码 401/403 只在错误拦截器处理
|
||||
if (error.response?.status === 401 && typeof on401 === 'function') {
|
||||
on401(error)
|
||||
}
|
||||
const status = error.response?.status
|
||||
|
||||
if (error.response?.status === 403 && typeof on403 === 'function') {
|
||||
if (status === 401 && typeof on401 === 'function') {
|
||||
on401(error)
|
||||
} else if (status === 403 && typeof on403 === 'function') {
|
||||
const forbiddenError = new Error('权限不足,无法访问该资源')
|
||||
forbiddenError.code = 403
|
||||
on403(forbiddenError)
|
||||
|
||||
@@ -12,26 +12,29 @@ const SERVER_BASE = API_BASE.APP_MEMBER
|
||||
* @param {Object} info - 包含 accessToken 和 refreshToken 的对象
|
||||
*/
|
||||
function saveTokens(info) {
|
||||
if (info?.accessToken || info?.refreshToken) {
|
||||
tokenManager.setTokens({
|
||||
accessToken: info.accessToken || '',
|
||||
refreshToken: info.refreshToken || '',
|
||||
expiresTime: info.expiresTime || 0, // 直接传递,由 token-manager 处理格式转换
|
||||
tokenType: info.tokenType || 'Bearer'
|
||||
})
|
||||
if (!info?.accessToken && !info?.refreshToken) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tokenManager.setTokens({
|
||||
accessToken: info.accessToken || '',
|
||||
refreshToken: info.refreshToken || '',
|
||||
expiresTime: info.expiresTime || 0,
|
||||
tokenType: info.tokenType || 'Bearer'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应拦截(可选):统一错误处理
|
||||
* 清除用户信息缓存
|
||||
*/
|
||||
// api.interceptors.response.use(
|
||||
// (resp) => resp,
|
||||
// (err) => {
|
||||
// // 统一错误日志/提示
|
||||
// return Promise.reject(err);
|
||||
// }
|
||||
// );
|
||||
async function clearUserCache() {
|
||||
try {
|
||||
const { clearUserInfoCache } = await import('@gold/hooks/web/useUserInfo')
|
||||
clearUserInfoCache()
|
||||
} catch (e) {
|
||||
// 清除缓存失败不影响登录流程
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信场景枚举(请与后端配置保持一致)
|
||||
@@ -50,8 +53,8 @@ export const SMS_SCENE = {
|
||||
* 短信模板编码常量
|
||||
*/
|
||||
export const SMS_TEMPLATE_CODE = {
|
||||
USER_REGISTER: 'muye-user-code', // 用户注册模板编码
|
||||
};
|
||||
USER_REGISTER: 'muye-user-code',
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号密码登录
|
||||
@@ -62,19 +65,11 @@ export const SMS_TEMPLATE_CODE = {
|
||||
* @returns {Promise<Object>} data.data: { accessToken, refreshToken, expiresTime, userInfo }
|
||||
*/
|
||||
export async function loginByPassword(mobile, password) {
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/login`, { mobile, password });
|
||||
const info = data || {};
|
||||
saveTokens(info);
|
||||
|
||||
// 清除用户信息缓存,确保登录后获取最新信息
|
||||
try {
|
||||
const { clearUserInfoCache } = await import('@gold/hooks/web/useUserInfo')
|
||||
clearUserInfoCache()
|
||||
} catch (e) {
|
||||
// 清除缓存失败不影响登录流程
|
||||
}
|
||||
|
||||
return info;
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/login`, { mobile, password })
|
||||
const info = data || {}
|
||||
saveTokens(info)
|
||||
await clearUserCache()
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,11 +82,12 @@ export async function loginByPassword(mobile, password) {
|
||||
* @returns {Promise<Object>} 后端通用响应结构
|
||||
*/
|
||||
export async function sendSmsCode(mobile, scene, templateCode) {
|
||||
const body = { scene };
|
||||
if (mobile) body.mobile = mobile; // 修改密码场景后端会根据当前登录用户自动填充手机号
|
||||
if (templateCode) body.templateCode = templateCode; // 添加模板编码参数
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/send-sms-code`, body);
|
||||
return data;
|
||||
const body = { scene }
|
||||
if (mobile) body.mobile = mobile
|
||||
if (templateCode) body.templateCode = templateCode
|
||||
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/send-sms-code`, body)
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,8 +100,8 @@ export async function sendSmsCode(mobile, scene, templateCode) {
|
||||
* @returns {Promise<Object>} 后端通用响应结构
|
||||
*/
|
||||
export async function validateSmsCode(mobile, code, scene) {
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/validate-sms-code`, { mobile, code, scene });
|
||||
return data;
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/validate-sms-code`, { mobile, code, scene })
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,19 +113,11 @@ export async function validateSmsCode(mobile, code, scene) {
|
||||
* @returns {Promise<Object>} data.data: { accessToken, refreshToken, expiresTime, userInfo }
|
||||
*/
|
||||
export async function loginBySms(mobile, code) {
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/sms-login`, { mobile, code });
|
||||
const info = data || {};
|
||||
saveTokens(info);
|
||||
|
||||
// 清除用户信息缓存,确保登录后获取最新信息
|
||||
try {
|
||||
const { clearUserInfoCache } = await import('@gold/hooks/web/useUserInfo')
|
||||
clearUserInfoCache()
|
||||
} catch (e) {
|
||||
// 清除缓存失败不影响登录流程
|
||||
}
|
||||
|
||||
return info;
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/sms-login`, { mobile, code })
|
||||
const info = data || {}
|
||||
saveTokens(info)
|
||||
await clearUserCache()
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,26 +127,36 @@ export async function loginBySms(mobile, code) {
|
||||
* @returns {Promise<Object>} data.data: { accessToken, refreshToken, expiresTime, userInfo }
|
||||
*/
|
||||
export async function refreshToken() {
|
||||
const rt = tokenManager.getRefreshToken();
|
||||
if (!rt) throw new Error('缺少 refresh_token');
|
||||
// refreshToken 作为 URL 查询参数传递
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/refresh-token`, null, { params: { refreshToken: rt } });
|
||||
const info = data || {};
|
||||
saveTokens(info);
|
||||
const rt = tokenManager.getRefreshToken()
|
||||
if (!rt) throw new Error('缺少 refresh_token')
|
||||
|
||||
// 清除用户信息缓存,因为 token 刷新后用户信息可能已更新
|
||||
try {
|
||||
const { clearUserInfoCache } = await import('@gold/hooks/web/useUserInfo')
|
||||
clearUserInfoCache()
|
||||
} catch (e) {
|
||||
// 清除缓存失败不影响登录流程
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/refresh-token`, null, { params: { refreshToken: rt } })
|
||||
|
||||
// 检查业务状态码
|
||||
if (data?.code === 401) {
|
||||
// 401: refreshToken 无效或过期
|
||||
tokenManager.clearTokens()
|
||||
await clearUserCache()
|
||||
router.push('/login')
|
||||
|
||||
const authError = new Error('登录已过期,请重新登录')
|
||||
authError.code = 401
|
||||
authError.needRelogin = true
|
||||
throw authError
|
||||
}
|
||||
|
||||
return info;
|
||||
if (data?.code !== 0 && data?.code !== 200) {
|
||||
throw new Error(data?.message || data?.msg || '刷新token失败')
|
||||
}
|
||||
|
||||
const info = data || {}
|
||||
saveTokens(info)
|
||||
await clearUserCache()
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录态下:发送“修改密码”验证码
|
||||
* 登录态下:发送"修改密码"验证码
|
||||
* - 场景:SMS_SCENE.MEMBER_UPDATE_PASSWORD
|
||||
* - 特性:后端会以当前登录用户的手机号为准,无需传 mobile
|
||||
* POST /member/auth/send-sms-code
|
||||
@@ -168,8 +166,8 @@ export async function refreshToken() {
|
||||
export async function sendUpdatePasswordCode() {
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/send-sms-code`, {
|
||||
scene: SMS_SCENE.MEMBER_UPDATE_PASSWORD,
|
||||
});
|
||||
return data;
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,12 +182,12 @@ export async function updatePasswordBySmsCode(newPassword, smsCode) {
|
||||
const { data } = await api.put(`${SERVER_BASE}/user/update-password`, {
|
||||
password: newPassword,
|
||||
code: smsCode,
|
||||
});
|
||||
return data;
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 未登录:发送“忘记密码”验证码
|
||||
* 未登录:发送"忘记密码"验证码
|
||||
* - 场景:SMS_SCENE.MEMBER_RESET_PASSWORD
|
||||
* POST /member/auth/send-sms-code
|
||||
*
|
||||
@@ -200,8 +198,8 @@ export async function sendResetPasswordCode(mobile) {
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/send-sms-code`, {
|
||||
mobile,
|
||||
scene: SMS_SCENE.MEMBER_RESET_PASSWORD,
|
||||
});
|
||||
return data;
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,8 +216,8 @@ export async function resetPasswordBySms(mobile, newPassword, smsCode) {
|
||||
mobile,
|
||||
password: newPassword,
|
||||
code: smsCode,
|
||||
});
|
||||
return data;
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,11 +232,11 @@ export async function getUserInfo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* “手机+验证码+密码注册”组合流程(基于短信登录即注册 + 设置密码)
|
||||
* "手机+验证码+密码注册"组合流程(基于短信登录即注册 + 设置密码)
|
||||
* 说明:
|
||||
* - 1) 发送登录场景验证码(可选:若已拿到 code 可跳过)
|
||||
* - 2) 短信登录:首次会自动注册并返回 token
|
||||
* - 3) 登录态下发送“修改密码”验证码
|
||||
* - 3) 登录态下发送"修改密码"验证码
|
||||
* - 4) 用短信验证码设置密码
|
||||
*
|
||||
* @param {string} mobile - 手机号(必填)
|
||||
@@ -248,17 +246,11 @@ export async function getUserInfo() {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function registerWithMobileCodePassword(mobile, loginCode, newPassword, updatePwdCode) {
|
||||
// 1) 可选:发送登录场景验证码(若已拿到 loginCode 可跳过)
|
||||
// await sendSmsCode(mobile, SMS_SCENE.MEMBER_LOGIN);
|
||||
|
||||
// 2) 短信登录(首次即注册)
|
||||
await loginBySms(mobile, loginCode);
|
||||
|
||||
// 3) 登录态下发送“修改密码”验证码(短信会发到当前账号绑定的手机号)
|
||||
// await sendUpdatePasswordCode();
|
||||
await loginBySms(mobile, loginCode)
|
||||
|
||||
// 4) 用短信验证码设置密码
|
||||
await updatePasswordBySmsCode(newPassword, updatePwdCode);
|
||||
await updatePasswordBySmsCode(newPassword, updatePwdCode)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,29 +22,16 @@ export function createHttpClient(options = {}) {
|
||||
const httpClient = createClientAxios({
|
||||
baseURL: '/',
|
||||
timeout: 180000,
|
||||
refreshTokenFn: refreshToken, // 传递刷新函数给拦截器
|
||||
refreshTokenFn: refreshToken,
|
||||
on401: async (error) => {
|
||||
// 401:优先使用上层自定义处理
|
||||
if (on401) {
|
||||
await on401(error)
|
||||
return
|
||||
}
|
||||
|
||||
// 默认处理:尝试刷新token
|
||||
try {
|
||||
await refreshToken()
|
||||
error._handled = true
|
||||
error._tokenRefreshed = true
|
||||
} catch (refreshError) {
|
||||
router.push('/login')
|
||||
}
|
||||
router.push('/login')
|
||||
},
|
||||
on403: (error) => {
|
||||
if (on403) {
|
||||
on403(error)
|
||||
} else {
|
||||
router.push('/login')
|
||||
}
|
||||
on403 ? on403(error) : router.push('/login')
|
||||
},
|
||||
})
|
||||
|
||||
@@ -74,38 +61,16 @@ export default http
|
||||
*
|
||||
* const myHttp = createHttpClient()
|
||||
* myHttp.interceptors.request.use((config) => {
|
||||
* // 添加自定义请求头
|
||||
* config.headers['X-Custom-Header'] = 'value'
|
||||
* return config
|
||||
* })
|
||||
* await myHttp.post('/api/data')
|
||||
*
|
||||
* 3. 自定义 401 处理
|
||||
* import { createHttpClient } from '@/api/http'
|
||||
*
|
||||
* const myHttp = createHttpClient({
|
||||
* on401: (error) => {
|
||||
* // 自定义 401 处理逻辑
|
||||
* console.log('自定义 401 处理')
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* 4. 高级用法:自动刷新 token 并重试请求
|
||||
* import { createHttpClient } from '@/api/http'
|
||||
*
|
||||
* const myHttp = createHttpClient()
|
||||
*
|
||||
* try {
|
||||
* const response = await myHttp.post('/api/data')
|
||||
* console.log(response.data)
|
||||
* } catch (error) {
|
||||
* // 如果是 401 且刷新失败,会自动调用 onAuthFailed 回调
|
||||
* console.error('请求失败:', error.message)
|
||||
* }
|
||||
*
|
||||
* 注意:当 401 发生时,系统会自动尝试刷新 token。
|
||||
* 如果刷新成功,会使用相同的 http 实例重新发起请求,
|
||||
* 确保自定义拦截器被正确应用。
|
||||
*/
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user