feat: 优化

This commit is contained in:
2026-01-18 02:55:02 +08:00
parent 85073b7960
commit a0378b5cbd
3 changed files with 165 additions and 200 deletions

View File

@@ -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)

View File

@@ -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)
}
/**

View File

@@ -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 实例重新发起请求,
* 确保自定义拦截器被正确应用。
*/