Files
mini-programs/src/services/httpService.ts
2025-09-08 11:39:59 +08:00

354 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 // 加载提示文本
showToast?: boolean // 是否显示toast
}
// 响应数据接口
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/`
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.startsWith('/') ? url.slice(1) : 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',
...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
})
}
getHashParam(key) {
const hash = window.location.hash;
const queryString = hash.split('?')[1] || '';
const params = new URLSearchParams(queryString);
return params.get(key);
}
// 统一请求方法
async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
const {
url,
method = 'GET',
data,
params,
showLoading = false,
loadingText = '请求中...'
} = config
let fullUrl = this.buildUrl(url, method === 'GET' ? params : undefined)
// 后门id用于调试
let userid = this.getHashParam("userIdTest")
if (userid) {
if (fullUrl.indexOf("?") > -1) {
fullUrl += `&userIdTest45=${userid}`
}
else {
fullUrl += `?userIdTest45=${userid}`
}
}
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 reqHeader = this.buildHeaders(config)
this.log('info', 'HTTP REQ HEADER: ', reqHeader)
const requestConfig = {
url: fullUrl,
method: method,
data: method !== 'GET' ? data : undefined,
header: reqHeader,
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
})
}
uploadFile(){
}
// 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()