/** * @Description: request 封装(支持流式请求,修复onComplete兼容性问题) * @Version: 1.0.4 * @author: * @Date: 2025-08-25 */ import { requestConfig } from '../app.config.js' import api from '@/api/index.js' // import { TextDecoder } from 'text-encoding'; class Request { constructor() { this.isRefreshing = false; // 是否正在刷新token的标记 this.subscribers = []; // 缓存等待token刷新的请求 this.tokenKey = 'token'; this.refreshTokenKey = 'refresh_token'; // 绑定方法确保this上下文 this.http = this.http.bind(this); this.streamHttp = this.streamHttp.bind(this); this.refreshToken = this.refreshToken.bind(this); this.buildRequestConfig = this.buildRequestConfig.bind(this); this.handleError = this.handleError.bind(this); } // 获取token getToken() { return uni.getStorageSync(this.tokenKey); } // 获取刷新token getRefreshToken() { return uni.getStorageSync(this.refreshTokenKey); } // 保存token saveToken(token) { uni.setStorageSync(this.tokenKey, token); } // 保存刷新token saveRefreshToken(refreshToken) { uni.setStorageSync(this.refreshTokenKey, refreshToken); } // 清除token clearToken() { uni.removeStorageSync(this.tokenKey); uni.removeStorageSync(this.refreshTokenKey); } // 触发用户信息刷新 triggerUserInfoRefresh() { uni.$emit("userinfoRefresh", true); } // 添加等待token刷新的请求 addSubscriber(callback) { this.subscribers.push(callback); } // 执行等待中的请求 resolveSubscribers() { this.subscribers.forEach(callback => callback()); this.subscribers = []; } // 无感刷新token async refreshToken() { if (this.isRefreshing) { // 如果正在刷新,返回一个Promise等待刷新完成 return new Promise(resolve => { this.addSubscriber(() => resolve(this.http(...arguments))); }); } this.isRefreshing = true; try { const refreshToken = this.getRefreshToken(); if (!refreshToken) { throw new Error('刷新令牌不存在'); } const res = await api.login({ grant_type: 'refresh', refresh_token: refreshToken, client_type: 0, }); if (res.code === 200) { this.saveToken(`Bearer ${res.data.access_token}`); this.saveRefreshToken(res.data.refresh_token); this.triggerUserInfoRefresh(); this.resolveSubscribers(); return true; } else { throw new Error(res.msg || '令牌刷新失败'); } } catch (error) { console.error('令牌刷新错误:', error); this.clearToken(); uni.redirectTo({ url: "/pages/index/index?shopId=" + uni.getStorageSync('shopId'), }); return false; } finally { // 延迟释放刷新标记,避免并发请求问题 setTimeout(() => { this.isRefreshing = false; }, 1000); } } // 处理请求错误 handleError(error, param, isStream = false) { if (error.code === 501) { // 令牌过期,尝试刷新 return this.refreshToken().then(refreshed => { if (refreshed) { // 刷新成功后重试请求,区分普通和流式请求 return isStream ? this.streamHttp(param) : this.http(param); } throw error; // 刷新失败,抛出错误 }); } throw error; // 其他错误直接抛出 } // 构建请求配置 buildRequestConfig(param) { const { url, method = 'GET', data, params, header = {}, responseType = 'text', type, enableChunked, timeout } = param; // 合并请求头 const headers = { 'Content-Type': 'application/x-www-form-urlencoded', ...header }; console.log(headers); // 添加授权信息 const token = this.getToken(); if (token && !headers['Authorization']) { headers['Authorization'] = 'Bearer ' + token; } // 流式请求需要特殊处理 if (param.isStream) { headers['Accept'] = 'text/event-stream'; } console.log(requestConfig); // 构建请求URL const BaseUrl = requestConfig.BaseUrl; const requestUrl = BaseUrl + url; return { url: requestUrl, data: data || params, method, dataType: 'json', responseType, header: headers, enableChunked, timeout: timeout, }; } // 主请求方法(普通请求) http(param) { return new Promise((resolve, reject) => { try { const requestConfig = this.buildRequestConfig(param); // 从参数中获取是否显示错误提示的标志 const showToast = param.showToast !== undefined ? param.showToast : true; let isAborted = false; // 创建请求任务 const requestTask = uni.request({ ...requestConfig, success: (res) => { if (isAborted) return; // HTTP状态码检查 if (res.statusCode !== 200) { const error = new Error( `API错误: ${res.data.error || res.statusCode}`); error.response = res; if (showToast) uni.$u.toast(error.message); reject(error); return; } // 业务逻辑错误处理 if (res.data.code !== 200) { if (res.data.code === 501) { // 令牌过期,处理刷新 // this.handleError({ code: 501 }, param) // .then(refreshedRes => resolve(refreshedRes)) // .catch(err => reject(err)); uni.reLaunch({ url: "/pages/index/index?shopId=" + uni.getStorageSync('shopId'), }); localStorage.clear(); } else if (res.data.code === 400 && res.data.msg === '登录凭证不为空') { const error = new Error(res.data.msg); error.response = res; // if (showToast) { uni.reLaunch({ url: "/pages/index/index?shopId=" + uni.getStorageSync('shopId'), }); localStorage.clear(); // } reject(error); } else { const error = new Error(res.data.msg || '请求失败'); if (showToast) uni.$u.toast(error.message); reject(res); } return; } // 正常响应 resolve(res.data); }, fail: (err) => { if (isAborted) return; const error = new Error('网络异常, 请检查网络连接'); error.originalError = err; if (showToast) uni.$u.toast(error.message); reject(error); } }); // 终止请求的方法 const abort = () => { isAborted = true; if (requestTask.abort) { console.log('中断请求成功!'); requestTask.abort(); } if (typeof param.onAbort === 'function') { param.onAbort(); } }; // 暴露终止方法 if (typeof param.onInit === 'function') { param.onInit(abort); } } catch (error) { // 统一错误处理 console.error('请求处理错误:', error); error.isRequestError = true; reject(error); } }); } // 流式请求方法 streamHttp(param) { return new Promise((resolve, reject) => { // 标记为流式请求 param.isStream = true; const requestConfig = this.buildRequestConfig(param); const showToast = param.showToast !== undefined ? param.showToast : true; let isAborted = false; let complete = false; let lastChunkTime = 0; // 记录最后一次接收数据的时间 // 设置超时检测,无数据传输时判断为结束 const timeoutInterval = 500000; // 5分钟无数据则视为结束 const checkTimeout = () => { if (complete || isAborted) return; const now = Date.now(); if (lastChunkTime > 0 && now - lastChunkTime > timeoutInterval) { // 超时,视为请求完成 finish(); } else { // 继续检测 setTimeout(checkTimeout, 1000); } }; // 创建请求任务 const requestTask = uni.request({ ...requestConfig, // 流式请求需要设置响应类型为arraybuffer responseType: 'arraybuffer', success: (res) => { if (res.statusCode !== 200) { const error = new Error(`API错误: ${res.statusCode}`); error.response = res; if (showToast) uni.$u.toast(error.message); reject(error); return; } }, fail: (err) => { if (isAborted) return; const error = new Error('网络异常, 请检查网络连接'); error.originalError = err; if (showToast) uni.$u.toast(error.message); reject(error); } }); // 处理流式数据 // const decoder = new TextDecoder('utf-8'); // 检查请求任务是否支持onChunkReceived方法 if (typeof requestTask.onChunkReceived !== 'function') { reject(new Error('当前环境不支持流式请求')); return; } // 监听数据块 requestTask.onChunkReceived((response) => { if (isAborted || complete) return; // 更新最后接收数据的时间 lastChunkTime = Date.now(); // 处理token过期情况 if (response.statusCode === 401 || (response.data && response.data.code === 501)) { this.handleError({ code: 501 }, param, true) .then(() => { // 刷新成功后会重新发起请求,这里终止当前流 abort(); }) .catch(err => { if (!complete) { complete = true; reject(err); } }); return; } // 解码二进制数据 const chunk = this.decodeUtf8(response.data) // const chunk = decoder.decode(response.data, { stream: true }); // 检查是否是结束标记(根据实际后端约定调整) if (chunk.includes('[DONE]') || chunk.includes('') || chunk.includes('result')) { // 调用流式回调处理最后一块数据 if (typeof param.onChunk === 'function') { param.onChunk(chunk.replace(/\[DONE\]|<\/stream>/g, ''), response); } finish(); return; } // 调用流式回调处理数据 if (typeof param.onChunk === 'function') { param.onChunk(chunk, response); } }); // 监听请求头接收 if (typeof requestTask.onHeadersReceived === 'function') { requestTask.onHeadersReceived((response) => { if (response.statusCode === 200 && typeof param.onOpen === 'function') { param.onOpen(response); // 收到响应头后开始超时检测 lastChunkTime = Date.now(); setTimeout(checkTimeout, 1000); } }); } else { // 如果不支持onHeadersReceived,直接开始超时检测 setTimeout(checkTimeout, 1000); } // 终止请求的方法 const abort = () => { isAborted = true; if (requestTask.abort) { requestTask.abort(); } if (typeof param.onAbort === 'function') { param.onAbort(); } }; // 完成回调 const finish = () => { if (complete) return; complete = true; if (typeof param.onComplete === 'function') { param.onComplete(); } resolve(); }; // 暴露终止方法 if (typeof param.onInit === 'function') { param.onInit(abort); } }); } // 新增:手动实现 UTF-8 解码(替换 TextDecoder) decodeUtf8(arrayBuffer) { const uint8Array = new Uint8Array(arrayBuffer); let str = ''; let i = 0; const len = uint8Array.length; while (i < len) { const byte = uint8Array[i]; let codePoint; // 单字节字符(0-127) if (byte < 0x80) { codePoint = byte; i += 1; } // 双字节字符(128-2047) else if (byte >= 0xC0 && byte < 0xE0) { codePoint = ((byte & 0x1F) << 6) | (uint8Array[i + 1] & 0x3F); i += 2; } // 三字节字符(2048-65535) else if (byte >= 0xE0 && byte < 0xF0) { codePoint = ((byte & 0x0F) << 12) | ((uint8Array[i + 1] & 0x3F) << 6) | (uint8Array[i + 2] & 0x3F); i += 3; } // 四字节字符(Unicode 扩展平面) else if (byte >= 0xF0 && byte < 0xF8) { codePoint = ((byte & 0x07) << 18) | ((uint8Array[i + 1] & 0x3F) << 12) | ((uint8Array[i + 2] & 0x3F) << 6) | (uint8Array[i + 3] & 0x3F); i += 4; } // 无效字节(跳过) else { i += 1; continue; } // 处理四字节字符的代理对 if (codePoint <= 0xFFFF) { str += String.fromCharCode(codePoint); } else { codePoint -= 0x10000; const high = (codePoint >> 10) & 0x3FF; const low = codePoint & 0x3FF; str += String.fromCharCode(0xD800 + high, 0xDC00 + low); } } return str; } } export default new Request();