request.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. /**
  2. * @Description: request 封装(支持流式请求,修复onComplete兼容性问题)
  3. * @Version: 1.0.4
  4. * @author:
  5. * @Date: 2025-08-25
  6. */
  7. import { requestConfig } from '../app.config.js'
  8. import api from '@/api/index.js'
  9. // import { TextDecoder } from 'text-encoding';
  10. class Request {
  11. constructor() {
  12. this.isRefreshing = false; // 是否正在刷新token的标记
  13. this.subscribers = []; // 缓存等待token刷新的请求
  14. this.tokenKey = 'token';
  15. this.refreshTokenKey = 'refresh_token';
  16. // 绑定方法确保this上下文
  17. this.http = this.http.bind(this);
  18. this.streamHttp = this.streamHttp.bind(this);
  19. this.refreshToken = this.refreshToken.bind(this);
  20. this.buildRequestConfig = this.buildRequestConfig.bind(this);
  21. this.handleError = this.handleError.bind(this);
  22. }
  23. // 获取token
  24. getToken() {
  25. return uni.getStorageSync(this.tokenKey);
  26. }
  27. // 获取刷新token
  28. getRefreshToken() {
  29. return uni.getStorageSync(this.refreshTokenKey);
  30. }
  31. // 保存token
  32. saveToken(token) {
  33. uni.setStorageSync(this.tokenKey, token);
  34. }
  35. // 保存刷新token
  36. saveRefreshToken(refreshToken) {
  37. uni.setStorageSync(this.refreshTokenKey, refreshToken);
  38. }
  39. // 清除token
  40. clearToken() {
  41. uni.removeStorageSync(this.tokenKey);
  42. uni.removeStorageSync(this.refreshTokenKey);
  43. }
  44. // 触发用户信息刷新
  45. triggerUserInfoRefresh() {
  46. uni.$emit("userinfoRefresh", true);
  47. }
  48. // 添加等待token刷新的请求
  49. addSubscriber(callback) {
  50. this.subscribers.push(callback);
  51. }
  52. // 执行等待中的请求
  53. resolveSubscribers() {
  54. this.subscribers.forEach(callback => callback());
  55. this.subscribers = [];
  56. }
  57. // 无感刷新token
  58. async refreshToken() {
  59. if (this.isRefreshing) {
  60. // 如果正在刷新,返回一个Promise等待刷新完成
  61. return new Promise(resolve => {
  62. this.addSubscriber(() => resolve(this.http(...arguments)));
  63. });
  64. }
  65. this.isRefreshing = true;
  66. try {
  67. const refreshToken = this.getRefreshToken();
  68. if (!refreshToken) {
  69. throw new Error('刷新令牌不存在');
  70. }
  71. const res = await api.login({
  72. grant_type: 'refresh',
  73. refresh_token: refreshToken,
  74. client_type: 0,
  75. });
  76. if (res.code === 200) {
  77. this.saveToken(`Bearer ${res.data.access_token}`);
  78. this.saveRefreshToken(res.data.refresh_token);
  79. this.triggerUserInfoRefresh();
  80. this.resolveSubscribers();
  81. return true;
  82. } else {
  83. throw new Error(res.msg || '令牌刷新失败');
  84. }
  85. } catch (error) {
  86. console.error('令牌刷新错误:', error);
  87. this.clearToken();
  88. uni.redirectTo({
  89. url:
  90. "/pages/index/index?shopId=" + uni.getStorageSync('shopId'),
  91. });
  92. return false;
  93. } finally {
  94. // 延迟释放刷新标记,避免并发请求问题
  95. setTimeout(() => {
  96. this.isRefreshing = false;
  97. }, 1000);
  98. }
  99. }
  100. // 处理请求错误
  101. handleError(error, param, isStream = false) {
  102. if (error.code === 501) {
  103. // 令牌过期,尝试刷新
  104. return this.refreshToken().then(refreshed => {
  105. if (refreshed) {
  106. // 刷新成功后重试请求,区分普通和流式请求
  107. return isStream ? this.streamHttp(param) : this.http(param);
  108. }
  109. throw error; // 刷新失败,抛出错误
  110. });
  111. }
  112. throw error; // 其他错误直接抛出
  113. }
  114. // 构建请求配置
  115. buildRequestConfig(param) {
  116. const {
  117. url,
  118. method = 'GET',
  119. data,
  120. params,
  121. header = {},
  122. responseType = 'text',
  123. type,
  124. enableChunked,
  125. timeout
  126. } = param;
  127. // 合并请求头
  128. const headers = {
  129. 'Content-Type': 'application/x-www-form-urlencoded',
  130. ...header
  131. };
  132. console.log(headers);
  133. // 添加授权信息
  134. const token = this.getToken();
  135. if (token && !headers['Authorization']) {
  136. headers['Authorization'] = 'Bearer ' + token;
  137. }
  138. console.log(requestConfig);
  139. // 构建请求URL
  140. const BaseUrl = requestConfig.BaseUrl;
  141. const requestUrl = BaseUrl + url;
  142. return {
  143. url: requestUrl,
  144. data: data || params,
  145. method,
  146. dataType: 'json',
  147. responseType,
  148. header: headers,
  149. enableChunked,
  150. timeout: timeout,
  151. };
  152. }
  153. // 主请求方法(普通请求)
  154. http(param) {
  155. return new Promise((resolve, reject) => {
  156. try {
  157. const requestConfig = this.buildRequestConfig(param);
  158. // alert(JSON.stringify(requestConfig));
  159. // 从参数中获取是否显示错误提示的标志
  160. const showToast = param.showToast !== undefined ? param.showToast : true;
  161. let isAborted = false;
  162. // 创建请求任务
  163. const requestTask = uni.request({
  164. ...requestConfig,
  165. success: (res) => {
  166. if (isAborted) return;
  167. // HTTP状态码检查
  168. if (res.statusCode !== 200) {
  169. const error = new Error(
  170. `API错误: ${res.data.error || res.statusCode}`);
  171. error.response = res;
  172. if (showToast) uni.$u.toast(error.message);
  173. reject(error);
  174. return;
  175. }
  176. // 业务逻辑错误处理
  177. if (res.data.code !== 200) {
  178. if (res.data.code === 501) {
  179. // 令牌过期,处理刷新
  180. // this.handleError({ code: 501 }, param)
  181. // .then(refreshedRes => resolve(refreshedRes))
  182. // .catch(err => reject(err));
  183. uni.reLaunch({
  184. url:
  185. "/pages/index/index?shopId=" + uni.getStorageSync('shopId') + '&storeId=' + uni.getStorageSync('storeId'),
  186. });
  187. localStorage.clear();
  188. } else if (res.data.code === 400 && res.data.msg === '登录凭证不为空') {
  189. const error = new Error(res.data.msg);
  190. error.response = res;
  191. // if (showToast) {
  192. uni.reLaunch({
  193. url:
  194. "/pages/index/index?shopId=" + uni.getStorageSync('shopId') + '&storeId=' + uni.getStorageSync('storeId'),
  195. });
  196. localStorage.clear();
  197. // }
  198. reject(error);
  199. } else {
  200. const error = new Error(res.data.msg || '请求失败');
  201. if (showToast) uni.$u.toast(error.message);
  202. reject(res);
  203. }
  204. return;
  205. }
  206. // 正常响应
  207. resolve(res.data);
  208. },
  209. fail: (err) => {
  210. if (isAborted) return;
  211. const error = new Error('网络异常, 请检查网络连接');
  212. error.originalError = err;
  213. if (showToast) uni.$u.toast(error.message);
  214. reject(error);
  215. }
  216. });
  217. // 终止请求的方法
  218. const abort = () => {
  219. isAborted = true;
  220. if (requestTask.abort) {
  221. console.log('中断请求成功!');
  222. requestTask.abort();
  223. }
  224. if (typeof param.onAbort === 'function') {
  225. param.onAbort();
  226. }
  227. };
  228. // 暴露终止方法
  229. if (typeof param.onInit === 'function') {
  230. param.onInit(abort);
  231. }
  232. } catch (error) {
  233. // 统一错误处理
  234. console.error('请求处理错误:', error);
  235. error.isRequestError = true;
  236. reject(error);
  237. }
  238. });
  239. }
  240. // 流式请求方法
  241. streamHttp(param) {
  242. return new Promise((resolve, reject) => {
  243. // 标记为流式请求
  244. param.isStream = true;
  245. const requestConfig = this.buildRequestConfig(param);
  246. const showToast = param.showToast !== undefined ? param.showToast : true;
  247. let isAborted = false;
  248. let complete = false;
  249. let lastChunkTime = 0; // 记录最后一次接收数据的时间
  250. // 设置超时检测,无数据传输时判断为结束
  251. const timeoutInterval = 500000; // 5分钟无数据则视为结束
  252. const checkTimeout = () => {
  253. if (complete || isAborted) return;
  254. const now = Date.now();
  255. if (lastChunkTime > 0 && now - lastChunkTime > timeoutInterval) {
  256. // 超时,视为请求完成
  257. finish();
  258. } else {
  259. // 继续检测
  260. setTimeout(checkTimeout, 1000);
  261. }
  262. };
  263. // 创建请求任务
  264. const requestTask = uni.request({
  265. ...requestConfig,
  266. // 流式请求需要设置响应类型为arraybuffer
  267. responseType: 'arraybuffer',
  268. success: (res) => {
  269. if (res.statusCode !== 200) {
  270. const error = new Error(`API错误: ${res.statusCode}`);
  271. error.response = res;
  272. if (showToast) uni.$u.toast(error.message);
  273. reject(error);
  274. return;
  275. }
  276. },
  277. fail: (err) => {
  278. if (isAborted) return;
  279. const error = new Error('网络异常, 请检查网络连接');
  280. error.originalError = err;
  281. if (showToast) uni.$u.toast(error.message);
  282. reject(error);
  283. }
  284. });
  285. // 处理流式数据
  286. // const decoder = new TextDecoder('utf-8');
  287. // 检查请求任务是否支持onChunkReceived方法
  288. if (typeof requestTask.onChunkReceived !== 'function') {
  289. reject(new Error('当前环境不支持流式请求'));
  290. return;
  291. }
  292. // 监听数据块
  293. requestTask.onChunkReceived((response) => {
  294. if (isAborted || complete) return;
  295. // 更新最后接收数据的时间
  296. lastChunkTime = Date.now();
  297. // 处理token过期情况
  298. if (response.statusCode === 401 || (response.data && response.data.code === 501)) {
  299. this.handleError({ code: 501 }, param, true)
  300. .then(() => {
  301. // 刷新成功后会重新发起请求,这里终止当前流
  302. abort();
  303. })
  304. .catch(err => {
  305. if (!complete) {
  306. complete = true;
  307. reject(err);
  308. }
  309. });
  310. return;
  311. }
  312. // 解码二进制数据
  313. const chunk = this.decodeUtf8(response.data)
  314. // const chunk = decoder.decode(response.data, { stream: true });
  315. // 检查是否是结束标记(根据实际后端约定调整)
  316. if (chunk.includes('[DONE]') || chunk.includes('</stream>') || chunk.includes('result')) {
  317. // 调用流式回调处理最后一块数据
  318. if (typeof param.onChunk === 'function') {
  319. param.onChunk(chunk.replace(/\[DONE\]|<\/stream>/g, ''), response);
  320. }
  321. finish();
  322. return;
  323. }
  324. // 调用流式回调处理数据
  325. if (typeof param.onChunk === 'function') {
  326. param.onChunk(chunk, response);
  327. }
  328. });
  329. // 监听请求头接收
  330. if (typeof requestTask.onHeadersReceived === 'function') {
  331. requestTask.onHeadersReceived((response) => {
  332. if (response.statusCode === 200 && typeof param.onOpen === 'function') {
  333. param.onOpen(response);
  334. // 收到响应头后开始超时检测
  335. lastChunkTime = Date.now();
  336. setTimeout(checkTimeout, 1000);
  337. }
  338. });
  339. } else {
  340. // 如果不支持onHeadersReceived,直接开始超时检测
  341. setTimeout(checkTimeout, 1000);
  342. }
  343. // 终止请求的方法
  344. const abort = () => {
  345. isAborted = true;
  346. if (requestTask.abort) {
  347. requestTask.abort();
  348. }
  349. if (typeof param.onAbort === 'function') {
  350. param.onAbort();
  351. }
  352. };
  353. // 完成回调
  354. const finish = () => {
  355. if (complete) return;
  356. complete = true;
  357. if (typeof param.onComplete === 'function') {
  358. param.onComplete();
  359. }
  360. resolve();
  361. };
  362. // 暴露终止方法
  363. if (typeof param.onInit === 'function') {
  364. param.onInit(abort);
  365. }
  366. });
  367. }
  368. // 新增:手动实现 UTF-8 解码(替换 TextDecoder)
  369. decodeUtf8(arrayBuffer) {
  370. const uint8Array = new Uint8Array(arrayBuffer);
  371. let str = '';
  372. let i = 0;
  373. const len = uint8Array.length;
  374. while (i < len) {
  375. const byte = uint8Array[i];
  376. let codePoint;
  377. // 单字节字符(0-127)
  378. if (byte < 0x80) {
  379. codePoint = byte;
  380. i += 1;
  381. }
  382. // 双字节字符(128-2047)
  383. else if (byte >= 0xC0 && byte < 0xE0) {
  384. codePoint = ((byte & 0x1F) << 6) | (uint8Array[i + 1] & 0x3F);
  385. i += 2;
  386. }
  387. // 三字节字符(2048-65535)
  388. else if (byte >= 0xE0 && byte < 0xF0) {
  389. codePoint = ((byte & 0x0F) << 12) | ((uint8Array[i + 1] & 0x3F) << 6) | (uint8Array[i + 2] & 0x3F);
  390. i += 3;
  391. }
  392. // 四字节字符(Unicode 扩展平面)
  393. else if (byte >= 0xF0 && byte < 0xF8) {
  394. codePoint = ((byte & 0x07) << 18) | ((uint8Array[i + 1] & 0x3F) << 12) | ((uint8Array[i + 2] & 0x3F) << 6) | (uint8Array[i + 3] & 0x3F);
  395. i += 4;
  396. }
  397. // 无效字节(跳过)
  398. else {
  399. i += 1;
  400. continue;
  401. }
  402. // 处理四字节字符的代理对
  403. if (codePoint <= 0xFFFF) {
  404. str += String.fromCharCode(codePoint);
  405. } else {
  406. codePoint -= 0x10000;
  407. const high = (codePoint >> 10) & 0x3FF;
  408. const low = codePoint & 0x3FF;
  409. str += String.fromCharCode(0xD800 + high, 0xDC00 + low);
  410. }
  411. }
  412. return str;
  413. }
  414. }
  415. export default new Request();