feat: 重构HTTP客户端架构和认证系统

核心改进:
- HTTP客户端:工厂函数模式,支持自定义拦截器和401/403处理
- 认证服务:函数式实现,消除this绑定问题,支持业务码+HTTP状态码双通道
- Token管理:简化为直接实例导出,移除bind()和箭头函数包装
- 路由守卫:优化逻辑,移除冗余代码,更简洁易维护

技术亮点:
- 统一401/403错误处理(业务code和HTTP status双检查)
- 自动刷新token并重试请求,保留自定义拦截器
- 分层清晰:clientAxios (Mono) -> http (应用) -> AuthService
- 支持扩展:业务代码可创建自定义HTTP实例并添加拦截器

文件变更:
- 新增 AuthService.js (函数式) 和 Login.vue
- 重构 http.js、token-manager.js、router/index.js
- 删除 TokenInput.vue、utils/auth.js 等冗余文件
- 更新所有API调用点使用直接实例导入

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-25 00:58:51 +08:00
parent cea43dd635
commit fb6d18b4f5
24 changed files with 1148 additions and 1198 deletions

View File

@@ -0,0 +1,232 @@
/**
* 认证服务 - 封装所有认证相关业务逻辑
* 提供登录、登出、刷新token等方法
* 函数式实现,无 this 绑定问题
*/
import { clientAxios } from '@gold/api/axios/client'
import tokenManager from '@gold/utils/token-manager'
// 刷新token的锁避免并发刷新
let isRefreshing = false
let refreshPromise = null
/**
* 手机号+验证码登录
*/
async function loginBySms(mobile, code) {
try {
const { data } = await clientAxios.post('/api/member/auth/sms-login', { mobile, code })
const tokenInfo = data || {}
// 保存token
tokenManager.setTokens({
accessToken: tokenInfo.accessToken || '',
refreshToken: tokenInfo.refreshToken || '',
expiresIn: tokenInfo.expiresTime ? Math.floor(tokenInfo.expiresTime / 1000) : 7200,
tokenType: 'Bearer'
})
return tokenInfo
} catch (error) {
console.error('短信登录失败:', error)
throw error
}
}
/**
* 账号密码登录
*/
async function loginByPassword(mobile, password) {
try {
const { data } = await clientAxios.post('/api/member/auth/login', { mobile, password })
const tokenInfo = data || {}
tokenManager.setTokens({
accessToken: tokenInfo.accessToken || '',
refreshToken: tokenInfo.refreshToken || '',
expiresIn: tokenInfo.expiresTime ? Math.floor(tokenInfo.expiresTime / 1000) : 7200,
tokenType: 'Bearer'
})
return tokenInfo
} catch (error) {
console.error('密码登录失败:', error)
throw error
}
}
/**
* 执行实际刷新token的逻辑
*/
async function doRefreshToken(refreshToken) {
const { data } = await clientAxios.post('/api/member/auth/refresh-token', null, {
params: { refreshToken }
})
const tokenInfo = data || {}
tokenManager.setTokens({
accessToken: tokenInfo.accessToken || '',
refreshToken: tokenInfo.refreshToken || '',
expiresIn: tokenInfo.expiresTime ? Math.floor(tokenInfo.expiresTime / 1000) : 7200,
tokenType: 'Bearer'
})
return tokenInfo
}
/**
* 刷新token
* 使用锁机制防止并发刷新
*/
async function refreshToken() {
// 如果正在刷新返回已有的刷新Promise
if (isRefreshing && refreshPromise) {
return refreshPromise
}
const refreshTokenValue = tokenManager.getRefreshToken()
if (!refreshTokenValue) {
throw new Error('缺少refreshToken无法刷新')
}
try {
isRefreshing = true
refreshPromise = doRefreshToken(refreshTokenValue)
const result = await refreshPromise
return result
} finally {
isRefreshing = false
refreshPromise = null
}
}
/**
* 发送短信验证码
*/
async function sendSmsCode(mobile, scene = 1) {
const { data } = await clientAxios.post('/api/member/auth/send-sms-code', {
mobile,
scene
})
return data
}
/**
* 登出
*/
async function logout() {
try {
// 清除token
tokenManager.clearTokens()
// 跳转到登录页
window.location.href = '/login'
} catch (error) {
console.error('登出失败:', error)
// 即使出错也要清除token
tokenManager.clearTokens()
window.location.href = '/login'
}
}
/**
* 检查是否已登录
*/
function isAuthenticated() {
return tokenManager.isLoggedIn()
}
/**
* 获取当前用户信息
*/
async function getCurrentUser() {
if (!isAuthenticated()) {
return null
}
try {
const { data } = await clientAxios.get('/api/member/user/get')
return data || null
} catch (error) {
console.error('获取用户信息失败:', error)
return null
}
}
/**
* 处理401/403错误
* @param {Error} error - axios错误对象
* @param {Function} onAuthFailed - 认证失败回调
* @param {Object} options - 额外选项
* @param {AxiosInstance} options.httpClient - 可选的HTTP客户端实例用于重新发起请求
*/
async function handleAuthError(error, onAuthFailed, options = {}) {
const status = error?.response?.status
const data = error?.response?.data
const code = data?.code
const originalRequest = error.config
const { httpClient } = options
// 统一检查是否为 401 (token无效或过期)
const is401 = (code === 401) || (status === 401)
// 统一检查是否为 403 (没有权限)
const is403 = (code === 403) || (status === 403)
// 处理 401 错误:尝试刷新 token
if (is401) {
try {
// 刷新 token
await refreshToken()
// 刷新成功,重新发起原请求(如果提供了 httpClient
if (originalRequest && httpClient) {
originalRequest.headers['Authorization'] = tokenManager.getAuthHeader()
return httpClient(originalRequest)
}
return null
} catch (refreshError) {
console.error('刷新token失败:', refreshError)
if (typeof onAuthFailed === 'function') {
onAuthFailed()
}
throw error
}
}
// 处理 403 错误:直接调用回调
if (is403) {
if (typeof onAuthFailed === 'function') {
onAuthFailed()
}
throw error
}
// 其他错误直接抛出
throw error
}
export default {
loginBySms,
loginByPassword,
refreshToken,
sendSmsCode,
logout,
isAuthenticated,
getCurrentUser,
handleAuthError
}
// 导出便捷方法(解构赋值,不会有 this 问题)
export {
loginBySms,
loginByPassword,
refreshToken,
sendSmsCode,
logout,
isAuthenticated,
getCurrentUser,
handleAuthError
}