feat: 增强情绪博物馆项目功能 - 新增用户评论和帖子功能,优化前端架构和WebSocket通信 - 更新文档和部署配置
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* HTTP请求工具
|
||||
* 基于Axios封装,支持请求拦截、响应拦截、错误处理等
|
||||
*/
|
||||
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { envConfig } from '@/config/env'
|
||||
import { STORAGE_KEYS, ERROR_CODES } from '@/config/constants'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import router from '@/router'
|
||||
|
||||
// 请求配置接口
|
||||
interface RequestConfig extends AxiosRequestConfig {
|
||||
skipAuth?: boolean
|
||||
skipErrorHandler?: boolean
|
||||
showLoading?: boolean
|
||||
loadingText?: string
|
||||
}
|
||||
|
||||
// 响应数据接口
|
||||
interface ResponseData<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
success: boolean
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
class RequestService {
|
||||
private instance: AxiosInstance
|
||||
private pendingRequests = new Map<string, AbortController>()
|
||||
|
||||
constructor() {
|
||||
// 创建axios实例
|
||||
this.instance = axios.create({
|
||||
baseURL: envConfig.apiBaseUrl,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
})
|
||||
|
||||
// 设置请求拦截器
|
||||
this.setupRequestInterceptor()
|
||||
|
||||
// 设置响应拦截器
|
||||
this.setupResponseInterceptor()
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求拦截器
|
||||
*/
|
||||
private setupRequestInterceptor() {
|
||||
this.instance.interceptors.request.use(
|
||||
(config: any) => {
|
||||
const requestConfig = config as RequestConfig
|
||||
|
||||
// 生成请求ID用于追踪
|
||||
const requestId = this.generateRequestId(config)
|
||||
config.metadata = { requestId }
|
||||
|
||||
// 处理重复请求
|
||||
this.handleDuplicateRequest(config, requestId)
|
||||
|
||||
// 添加认证头
|
||||
if (!requestConfig.skipAuth) {
|
||||
const token = this.getToken()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
if (requestConfig.showLoading) {
|
||||
const appStore = useAppStore()
|
||||
appStore.setLoading(true, requestConfig.loadingText)
|
||||
}
|
||||
|
||||
// 调试模式下打印请求信息
|
||||
if (envConfig.debug) {
|
||||
console.log('🚀 发送请求:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
params: config.params,
|
||||
data: config.data,
|
||||
headers: config.headers
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.error('❌ 请求拦截器错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置响应拦截器
|
||||
*/
|
||||
private setupResponseInterceptor() {
|
||||
this.instance.interceptors.response.use(
|
||||
(response: AxiosResponse<ResponseData>) => {
|
||||
const config = response.config as RequestConfig
|
||||
const requestId = config.metadata?.requestId
|
||||
|
||||
// 移除pending请求
|
||||
if (requestId) {
|
||||
this.pendingRequests.delete(requestId)
|
||||
}
|
||||
|
||||
// 隐藏加载状态
|
||||
if (config.showLoading) {
|
||||
const appStore = useAppStore()
|
||||
appStore.setLoading(false)
|
||||
}
|
||||
|
||||
// 调试模式下打印响应信息
|
||||
if (envConfig.debug) {
|
||||
console.log('✅ 收到响应:', {
|
||||
url: response.config.url,
|
||||
status: response.status,
|
||||
data: response.data
|
||||
})
|
||||
}
|
||||
|
||||
const { data } = response
|
||||
|
||||
// 处理业务状态码
|
||||
if (data.code === 200 || data.success) {
|
||||
return data.data
|
||||
} else {
|
||||
return this.handleBusinessError(data, config)
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
const config = error.config as RequestConfig
|
||||
const requestId = config?.metadata?.requestId
|
||||
|
||||
// 移除pending请求
|
||||
if (requestId) {
|
||||
this.pendingRequests.delete(requestId)
|
||||
}
|
||||
|
||||
// 隐藏加载状态
|
||||
if (config?.showLoading) {
|
||||
const appStore = useAppStore()
|
||||
appStore.setLoading(false)
|
||||
}
|
||||
|
||||
return this.handleRequestError(error, config)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求ID
|
||||
*/
|
||||
private generateRequestId(config: AxiosRequestConfig): string {
|
||||
const { method, url, params, data } = config
|
||||
return `${method}_${url}_${JSON.stringify(params)}_${JSON.stringify(data)}_${Date.now()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理重复请求
|
||||
*/
|
||||
private handleDuplicateRequest(config: AxiosRequestConfig, requestId: string) {
|
||||
const duplicateKey = `${config.method}_${config.url}`
|
||||
|
||||
// 取消之前的相同请求
|
||||
if (this.pendingRequests.has(duplicateKey)) {
|
||||
const controller = this.pendingRequests.get(duplicateKey)
|
||||
controller?.abort('请求被取消:发起了新的相同请求')
|
||||
}
|
||||
|
||||
// 创建新的AbortController
|
||||
const controller = new AbortController()
|
||||
config.signal = controller.signal
|
||||
this.pendingRequests.set(duplicateKey, controller)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token
|
||||
*/
|
||||
private getToken(): string | null {
|
||||
return localStorage.getItem(STORAGE_KEYS.TOKEN)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务错误
|
||||
*/
|
||||
private handleBusinessError(data: ResponseData, config: RequestConfig) {
|
||||
if (config.skipErrorHandler) {
|
||||
return Promise.reject(data)
|
||||
}
|
||||
|
||||
// 特殊错误码处理
|
||||
switch (data.code) {
|
||||
case ERROR_CODES.UNAUTHORIZED:
|
||||
this.handleUnauthorized()
|
||||
break
|
||||
case ERROR_CODES.FORBIDDEN:
|
||||
ElMessage.error('权限不足,无法访问该资源')
|
||||
break
|
||||
case ERROR_CODES.NOT_FOUND:
|
||||
ElMessage.error('请求的资源不存在')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(data.message || '请求失败')
|
||||
}
|
||||
|
||||
return Promise.reject(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求错误
|
||||
*/
|
||||
private handleRequestError(error: any, config: RequestConfig) {
|
||||
if (config?.skipErrorHandler) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
let message = '网络错误,请稍后重试'
|
||||
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
message = '请求超时,请稍后重试'
|
||||
} else if (error.response) {
|
||||
const { status } = error.response
|
||||
switch (status) {
|
||||
case ERROR_CODES.UNAUTHORIZED:
|
||||
this.handleUnauthorized()
|
||||
return Promise.reject(error)
|
||||
case ERROR_CODES.FORBIDDEN:
|
||||
message = '权限不足,无法访问该资源'
|
||||
break
|
||||
case ERROR_CODES.NOT_FOUND:
|
||||
message = '请求的资源不存在'
|
||||
break
|
||||
case ERROR_CODES.INTERNAL_SERVER_ERROR:
|
||||
message = '服务器内部错误'
|
||||
break
|
||||
default:
|
||||
message = `请求失败 (${status})`
|
||||
}
|
||||
} else if (error.request) {
|
||||
message = '网络连接失败,请检查网络设置'
|
||||
}
|
||||
|
||||
ElMessage.error(message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理未授权错误
|
||||
*/
|
||||
private async handleUnauthorized() {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
try {
|
||||
// 尝试刷新Token
|
||||
await authStore.refreshToken()
|
||||
} catch {
|
||||
// 刷新失败,跳转到登录页
|
||||
ElMessageBox.alert('登录已过期,请重新登录', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
*/
|
||||
get<T = any>(url: string, params?: any, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.get(url, { params, ...config })
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.post(url, data, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
*/
|
||||
put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.put(url, data, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
*/
|
||||
delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.delete(url, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
upload<T = any>(url: string, file: File, config?: RequestConfig): Promise<T> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return this.instance.post(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有请求
|
||||
*/
|
||||
cancelAllRequests() {
|
||||
this.pendingRequests.forEach((controller) => {
|
||||
controller.abort('用户取消请求')
|
||||
})
|
||||
this.pendingRequests.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消指定请求
|
||||
*/
|
||||
cancelRequest(requestId: string) {
|
||||
const controller = this.pendingRequests.get(requestId)
|
||||
if (controller) {
|
||||
controller.abort('用户取消请求')
|
||||
this.pendingRequests.delete(requestId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建请求实例
|
||||
const request = new RequestService()
|
||||
|
||||
export default request
|
||||
export { type RequestConfig }
|
||||
Reference in New Issue
Block a user