| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- /**
- * @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;
- }
- 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);
- // alert(JSON.stringify(requestConfig));
- // 从参数中获取是否显示错误提示的标志
- 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') + '&storeId=' + uni.getStorageSync('storeId'),
- });
- 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') + '&storeId=' + uni.getStorageSync('storeId'),
- });
- 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('</stream>') || 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();
|