326 lines
8.1 KiB
TypeScript
326 lines
8.1 KiB
TypeScript
import Taro from '@tarojs/taro'
|
||
import tokenManager from '../utils/tokenManager'
|
||
import envConfig, { isDevelopment, getEnvInfo } from '../config/env'
|
||
|
||
// 请求方法类型
|
||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||
|
||
// 请求配置接口
|
||
export interface RequestConfig {
|
||
url: string
|
||
method?: HttpMethod
|
||
data?: any
|
||
params?: Record<string, any>
|
||
headers?: Record<string, string>
|
||
needAuth?: boolean // 是否需要token认证
|
||
showLoading?: boolean // 是否显示加载提示
|
||
loadingText?: string // 加载提示文本
|
||
}
|
||
|
||
// 响应数据接口
|
||
export interface ApiResponse<T = any> {
|
||
code: number
|
||
data: T
|
||
message: string
|
||
success: boolean
|
||
}
|
||
|
||
// HTTP状态码常量
|
||
export const HTTP_STATUS = {
|
||
SUCCESS: 200,
|
||
UNAUTHORIZED: 401,
|
||
FORBIDDEN: 403,
|
||
NOT_FOUND: 404,
|
||
SERVER_ERROR: 500
|
||
}
|
||
|
||
class HttpService {
|
||
private baseURL: string
|
||
private timeout: number
|
||
private enableLog: boolean
|
||
|
||
constructor() {
|
||
// 使用环境配置
|
||
this.baseURL = `${envConfig.apiBaseURL}/api/${envConfig.apiVersion}`
|
||
this.timeout = envConfig.timeout
|
||
this.enableLog = envConfig.enableLog
|
||
|
||
// 在开发环境下输出配置信息
|
||
if (isDevelopment()) {
|
||
console.log('🌍 HTTP服务初始化:', {
|
||
baseURL: this.baseURL,
|
||
timeout: this.timeout,
|
||
envInfo: getEnvInfo()
|
||
})
|
||
}
|
||
}
|
||
|
||
// 构建完整URL
|
||
private buildUrl(url: string, params?: Record<string, any>): string {
|
||
const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`
|
||
|
||
if (params) {
|
||
const searchParams = new URLSearchParams()
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null) {
|
||
searchParams.append(key, String(value))
|
||
}
|
||
})
|
||
const queryString = searchParams.toString()
|
||
return queryString ? `${fullUrl}?${queryString}` : fullUrl
|
||
}
|
||
|
||
return fullUrl
|
||
}
|
||
|
||
// 构建请求头
|
||
private buildHeaders(config: RequestConfig): Record<string, string> {
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json',
|
||
'X-Environment': envConfig.name, // 添加环境标识
|
||
...config.headers
|
||
}
|
||
|
||
// 如果需要认证,添加token
|
||
if (config.needAuth !== false) {
|
||
const authHeader = tokenManager.getAuthHeader()
|
||
Object.assign(headers, authHeader)
|
||
}
|
||
|
||
return headers
|
||
}
|
||
|
||
// 日志输出
|
||
private log(level: 'info' | 'warn' | 'error', message: string, data?: any) {
|
||
if (!this.enableLog) return
|
||
|
||
const logMethod = console[level] || console.log
|
||
const timestamp = new Date().toLocaleTimeString()
|
||
|
||
if (data) {
|
||
logMethod(`[${timestamp}] HTTP ${level.toUpperCase()}: ${message}`, data)
|
||
} else {
|
||
logMethod(`[${timestamp}] HTTP ${level.toUpperCase()}: ${message}`)
|
||
}
|
||
}
|
||
|
||
// 处理响应
|
||
private handleResponse<T>(response: any): Promise<ApiResponse<T>> {
|
||
return new Promise((resolve, reject) => {
|
||
const { statusCode, data } = response
|
||
|
||
this.log('info', `响应状态码: ${statusCode}`, { data })
|
||
|
||
// HTTP状态码检查
|
||
if (statusCode !== HTTP_STATUS.SUCCESS) {
|
||
this.handleHttpError(statusCode)
|
||
reject(new Error(`HTTP Error: ${statusCode}`))
|
||
return
|
||
}
|
||
|
||
// 业务状态码检查
|
||
if (data && typeof data === 'object') {
|
||
if (data.success === false || (data.code && data.code !== 0 && data.code !== 200)) {
|
||
this.handleBusinessError(data)
|
||
reject(new Error(data.message || '请求失败'))
|
||
return
|
||
}
|
||
}
|
||
|
||
resolve(data)
|
||
})
|
||
}
|
||
|
||
// 处理HTTP错误
|
||
private handleHttpError(statusCode: number): void {
|
||
let message = '网络请求失败'
|
||
|
||
switch (statusCode) {
|
||
case HTTP_STATUS.UNAUTHORIZED:
|
||
message = '登录已过期,请重新登录'
|
||
tokenManager.clearTokens()
|
||
// 可以在这里跳转到登录页面
|
||
break
|
||
case HTTP_STATUS.FORBIDDEN:
|
||
message = '没有权限访问该资源'
|
||
break
|
||
case HTTP_STATUS.NOT_FOUND:
|
||
message = '请求的资源不存在'
|
||
break
|
||
case HTTP_STATUS.SERVER_ERROR:
|
||
message = '服务器内部错误'
|
||
break
|
||
default:
|
||
message = `请求失败 (${statusCode})`
|
||
}
|
||
|
||
this.log('error', `HTTP错误 ${statusCode}: ${message}`)
|
||
|
||
Taro.showToast({
|
||
title: message,
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
}
|
||
|
||
// 处理业务错误
|
||
private handleBusinessError(data: any): void {
|
||
const message = data.message || '操作失败'
|
||
|
||
this.log('error', `业务错误: ${message}`, data)
|
||
|
||
Taro.showToast({
|
||
title: message,
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
}
|
||
|
||
// 统一请求方法
|
||
async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
|
||
const {
|
||
url,
|
||
method = 'GET',
|
||
data,
|
||
params,
|
||
showLoading = false,
|
||
loadingText = '请求中...'
|
||
} = config
|
||
|
||
const fullUrl = this.buildUrl(url, method === 'GET' ? params : undefined)
|
||
|
||
this.log('info', `发起请求: ${method} ${fullUrl}`, {
|
||
data: method !== 'GET' ? data : undefined,
|
||
params: method === 'GET' ? params : undefined
|
||
})
|
||
// 检查token(如果需要认证)
|
||
if (config.needAuth === true && !tokenManager.hasValidToken()) {
|
||
Taro.showToast({
|
||
title: '请先登录',
|
||
icon: 'none'
|
||
})
|
||
throw new Error('Token无效或已过期')
|
||
}
|
||
|
||
// 显示加载提示
|
||
if (showLoading) {
|
||
Taro.showLoading({
|
||
title: loadingText,
|
||
mask: true
|
||
})
|
||
}
|
||
|
||
try {
|
||
const requestConfig = {
|
||
url: fullUrl,
|
||
method: method,
|
||
data: method !== 'GET' ? data : undefined,
|
||
header: this.buildHeaders(config),
|
||
timeout: this.timeout
|
||
}
|
||
|
||
const response = await Taro.request(requestConfig)
|
||
return this.handleResponse<T>(response)
|
||
} catch (error) {
|
||
this.log('error', '请求失败', error)
|
||
|
||
// 在模拟模式下返回模拟数据
|
||
if (envConfig.enableMock && isDevelopment()) {
|
||
this.log('info', '使用模拟数据')
|
||
return this.getMockResponse<T>(url, method)
|
||
}
|
||
|
||
Taro.showToast({
|
||
title: '网络连接失败',
|
||
icon: 'none'
|
||
})
|
||
|
||
throw error
|
||
} finally {
|
||
// 隐藏加载提示
|
||
if (showLoading) {
|
||
Taro.hideLoading()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取模拟数据
|
||
private getMockResponse<T>(url: string, method: string): ApiResponse<T> {
|
||
this.log('info', `返回模拟数据: ${method} ${url}`)
|
||
|
||
return {
|
||
code: 200,
|
||
success: true,
|
||
message: '模拟请求成功',
|
||
data: {
|
||
mockData: true,
|
||
url,
|
||
method,
|
||
timestamp: new Date().toISOString()
|
||
} as T
|
||
}
|
||
}
|
||
|
||
// GET请求
|
||
get<T = any>(url: string, params?: Record<string, any>, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||
return this.request<T>({
|
||
url,
|
||
method: 'GET',
|
||
params,
|
||
...config
|
||
})
|
||
}
|
||
|
||
// POST请求
|
||
post<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||
return this.request<T>({
|
||
url,
|
||
method: 'POST',
|
||
data,
|
||
...config
|
||
})
|
||
}
|
||
|
||
// PUT请求
|
||
put<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||
return this.request<T>({
|
||
url,
|
||
method: 'PUT',
|
||
data,
|
||
...config
|
||
})
|
||
}
|
||
|
||
// DELETE请求
|
||
delete<T = any>(url: string, params?: Record<string, any>, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||
return this.request<T>({
|
||
url,
|
||
method: 'DELETE',
|
||
params,
|
||
...config
|
||
})
|
||
}
|
||
|
||
// PATCH请求
|
||
patch<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||
return this.request<T>({
|
||
url,
|
||
method: 'PATCH',
|
||
data,
|
||
...config
|
||
})
|
||
}
|
||
|
||
// 获取当前环境信息
|
||
getEnvInfo() {
|
||
return {
|
||
baseURL: this.baseURL,
|
||
timeout: this.timeout,
|
||
enableLog: this.enableLog,
|
||
...getEnvInfo()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出HTTP服务实例
|
||
export default new HttpService()
|