import config from './config.js' /** * 网络请求封装 */ class Request { constructor() { // 安全地获取配置 // 内网环境:缩短超时时间,加快失败反馈 this.timeout = (config && config.REQUEST_TIMEOUT) ? config.REQUEST_TIMEOUT : 10000 // 改为10秒 // 重试配置 this.maxRetries = 1 // 内网环境减少重试次数,加快响应 this.retryDelay = 500 // 重试延迟减少到500ms // 调试信息(生产环境关闭) // if (process.env.NODE_ENV === 'development') { // console.log('Request initialized') // } } /** * 获取当前baseURL(动态获取,支持运行时切换) */ getBaseURL() { try { // 每次请求时动态获取服务器配置 let serverHost = 'localhost' let serverPort = 8080 // 安全地获取服务器配置 if (config && typeof config.getServerConfig === 'function') { const serverConfig = config.getServerConfig() if (serverConfig) { serverHost = serverConfig.serverHost || serverHost serverPort = serverConfig.serverPort || serverPort } } else { // 如果config.getServerConfig不可用,尝试直接从存储读取 try { const storedHost = uni.getStorageSync('server_host') const storedPort = uni.getStorageSync('server_port') if (storedHost) serverHost = storedHost if (storedPort) serverPort = parseInt(storedPort) || serverPort } catch (e) { console.warn('读取服务器配置失败,使用默认值:', e) } } let baseURL = `http://${serverHost}:${serverPort}` // #ifdef H5 // H5环境:如果访问的是localhost,使用localhost(通过Vite代理) if (typeof window !== 'undefined' && window.location) { const hostname = window.location.hostname if (hostname === 'localhost' || hostname === '127.0.0.1') { // H5开发环境:通过 Vite 代理 /api -> 本地后端 return '/api' } } // #endif // #ifdef APP-PLUS // App环境:如果使用localhost,给出提示 if (serverHost === 'localhost' || serverHost === '127.0.0.1') { console.error('❌ App环境无法使用localhost,请配置电脑的局域网IP地址!') console.error('❌ 配置方法:uni.setStorageSync("server_host", "你的电脑IP")') console.error('❌ 例如:uni.setStorageSync("server_host", "192.168.1.155")') } // #endif return baseURL } catch (error) { console.error('获取baseURL失败:', error) // 返回默认值 return 'http://192.168.1.8:30091' } } /** * 请求拦截器 */ interceptRequest(requestConfig) { // 检查是否需要token(注册接口等不需要token) const isToken = requestConfig.header && requestConfig.header.isToken === false // 添加token(RuoYi使用Authorization Bearer格式) const token = uni.getStorageSync('token') if (token && !isToken) { requestConfig.header = requestConfig.header || {} // RuoYi使用Authorization Bearer格式 requestConfig.header['Authorization'] = `Bearer ${token}` } // 设置请求头 requestConfig.header = requestConfig.header || {} requestConfig.header['Content-Type'] = requestConfig.header['Content-Type'] || 'application/json' // 删除isToken标记(不发送到服务器) if (requestConfig.header.isToken !== undefined) { delete requestConfig.header.isToken } return requestConfig } /** * 响应拦截器 */ interceptResponse(response) { return new Promise((resolve, reject) => { const { statusCode, data } = response // HTTP状态码检查 if (statusCode !== 200) { reject(new Error(`请求失败: ${statusCode}`)) return } // 处理响应数据(可能是字符串需要解析) let responseData = data if (typeof data === 'string') { try { responseData = JSON.parse(data) } catch (e) { reject(new Error('响应数据解析失败')) return } } // 业务状态码检查(RuoYi使用code字段) if (responseData.code === 401) { // Token过期,清除登录信息并跳转到登录页 uni.removeStorageSync('token') uni.removeStorageSync('userInfo') uni.reLaunch({ url: '/pages/login/login' }) reject(new Error('登录已过期,请重新登录')) return } // RuoYi响应格式:{ code: 200, msg: "操作成功", data: {...} } if (responseData.code !== 200) { reject(new Error(responseData.msg || responseData.message || '请求失败')) return } // 返回整个响应对象(包含code、msg、data等) resolve(responseData) }) } /** * 判断错误是否可重试 */ isRetryableError(error) { if (!error || !error.errMsg) return false const errMsg = error.errMsg.toLowerCase() // 可重试的错误类型 const retryableErrors = [ 'unexpected end of stream', 'abort', 'timeout', 'network', 'connection', 'fail' ] return retryableErrors.some(keyword => errMsg.includes(keyword)) } /** * 通用请求方法(带重试机制) */ request(options, retryCount = 0) { return new Promise((resolve, reject) => { try { // 动态获取baseURL(支持运行时切换服务器地址) const baseURL = this.getBaseURL() // 处理URL:如果options.url已经是完整URL,则不拼接baseURL let requestUrl = options.url if (!requestUrl || typeof requestUrl !== 'string') { reject(new Error('请求URL无效')) return } if (!requestUrl.startsWith('http://') && !requestUrl.startsWith('https://')) { requestUrl = baseURL + (requestUrl.startsWith('/') ? requestUrl : '/' + requestUrl) } // 请求配置 const requestConfig = { url: requestUrl, method: options.method || 'GET', data: options.data || {}, header: options.header || {}, timeout: this.timeout, ...options } // 覆盖url(确保使用处理后的URL) requestConfig.url = requestUrl // 请求拦截 const interceptedConfig = this.interceptRequest(requestConfig) // 调试日志(生产环境关闭) // if (process.env.NODE_ENV === 'development') { // if (retryCount > 0) { // console.log(`🔄 重试请求 (${retryCount}/${this.maxRetries}):`, requestUrl) // } else { // console.log('发送请求:', { // url: requestUrl, // method: requestConfig.method, // data: requestConfig.data, // header: interceptedConfig.header // }) // } // } // 发送请求 uni.request({ ...interceptedConfig, success: (response) => { // 调试日志(生产环境关闭) // if (process.env.NODE_ENV === 'development') { // console.log('请求成功:', response) // } this.interceptResponse(response) .then(resolve) .catch(reject) }, fail: (error) => { // 调试日志 console.error('请求失败:', error) // 判断是否可重试 if (retryCount < this.maxRetries && this.isRetryableError(error)) { console.log(`⚠️ 请求失败,将在 ${this.retryDelay / 1000} 秒后重试...`) setTimeout(() => { // 递归重试 this.request(options, retryCount + 1) .then(resolve) .catch(reject) }, this.retryDelay) } else { // 不可重试或已达到最大重试次数 let errorMessage = '网络请求失败' if (error.errMsg) { if (error.errMsg.includes('unexpected end of stream')) { errorMessage = '连接中断,请检查网络或服务器状态' } else if (error.errMsg.includes('timeout')) { errorMessage = '请求超时,请检查网络连接' } else if (error.errMsg.includes('abort')) { errorMessage = '请求被中断,请重试' } else { errorMessage = error.errMsg } } uni.showToast({ title: errorMessage, icon: 'none', duration: 3000 }) reject(error) } } }) } catch (error) { console.error('request() 方法执行失败:', error) reject(error) } }) } /** * GET请求 */ get(url, params = {}, options = {}) { try { return this.request({ url, method: 'GET', data: params, ...options }) } catch (error) { console.error('request.get() 调用失败:', error) return Promise.reject(error) } } /** * POST请求 */ post(url, data = {}, options = {}) { // 调试日志(生产环境关闭) // if (process.env.NODE_ENV === 'development') { // console.log('POST请求:', url, '数据:', data) // } return this.request({ url, method: 'POST', data: data, ...options }) } /** * PUT请求 */ put(url, data = {}, options = {}) { return this.request({ url, method: 'PUT', data, ...options }) } /** * DELETE请求 */ delete(url, data = {}, options = {}) { return this.request({ url, method: 'DELETE', data, ...options }) } /** * 文件上传 */ upload(url, filePath, formData = {}, options = {}) { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token') // 动态获取baseURL(支持运行时切换服务器地址) const baseURL = this.getBaseURL() // 处理URL let uploadUrl = url if (!uploadUrl.startsWith('http://') && !uploadUrl.startsWith('https://')) { uploadUrl = baseURL + (uploadUrl.startsWith('/') ? uploadUrl : '/' + uploadUrl) } uni.uploadFile({ url: uploadUrl, filePath: filePath, name: options.name || 'file', formData: formData, header: { 'Authorization': token ? `Bearer ${token}` : '' // RuoYi使用Authorization Bearer格式 }, success: (response) => { try { const data = JSON.parse(response.data) if (data.code === 200) { resolve(data) } else { reject(new Error(data.message || '上传失败')) } } catch (e) { reject(new Error('响应解析失败')) } }, fail: (error) => { uni.showToast({ title: '上传失败', icon: 'none' }) reject(error) } }) }) } } // 创建实例并导出 const request = new Request() // 确保所有方法都正确定义(防止编译优化导致的问题) if (typeof request.get !== 'function') { console.error('❌ request.get 方法不可用!') } if (typeof request.post !== 'function') { console.error('❌ request.post 方法不可用!') } if (typeof request.request !== 'function') { console.error('❌ request.request 方法不可用!') } export default request