request.js 7.45 KB
/*
 * @Date: 2022-09-19 14:11:06
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2026-01-13 21:29:43
 * @FilePath: /xyxBooking-weapp/src/utils/request.js
 * @Description: 简单axios封装,后续按实际处理
 */
// import axios from 'axios'
import axios from 'axios-miniprogram';
import Taro from '@tarojs/taro'
// import qs from 'qs'
// import { strExist } from './tools'
import { refreshSession, saveCurrentPagePath, navigateToAuth } from './authRedirect'
import { has_offline_booking_cache } from '@/composables/useOfflineBookingCache'
import { get_weak_network_modal_no_cache_options } from '@/utils/uiText'

// import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress';
// import store from '@/store'
// import { getToken } from '@/utils/auth'
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config';

/**
 * 获取sessionid的工具函数
 * @returns {string|null} sessionid或null
 */
const getSessionId = () => {
  try {
    return Taro.getStorageSync("sessionid") || null;
  } catch (error) {
    console.error('获取sessionid失败:', error);
    return null;
  }
};

// const isPlainObject = (value) => {
//   if (value === null || typeof value !== 'object') return false
//   return Object.prototype.toString.call(value) === '[object Object]'
// }

// create an axios instance
const service = axios.create({
  baseURL: BASE_URL, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000, // request timeout
})

service.defaults.params = {
  ...REQUEST_DEFAULT_PARAMS,
};

let has_shown_timeout_modal = false

/**
 * 判断是否为超时错误
 * @param {Error} error - 请求错误对象
 * @returns {boolean} 是否为超时错误
 */

const is_timeout_error = (error) => {
  const msg = String(error?.message || error?.errMsg || '')
  if (error?.code === 'ECONNABORTED') return true
  return msg.toLowerCase().includes('timeout')
}

/**
 * 判断是否为网络错误(断网/弱网/请求失败等)
 * @param {Error} error - 请求错误对象
 * @returns {boolean} 是否为网络错误
 */
const is_network_error = (error) => {
  const msg = String(error?.message || error?.errMsg || '')
  const raw = (() => {
    try {
      return JSON.stringify(error) || ''
    } catch (e) {
      return ''
    }
  })()
  const lower = (msg + ' ' + raw).toLowerCase()
  if (lower.includes('request:fail')) return true
  if (lower.includes('request fail')) return true
  if (lower.includes('network error')) return true
  if (lower.includes('failed to fetch')) return true
  if (lower.includes('the internet connection appears to be offline')) return true
  if (lower.includes('err_blocked_by_client')) return true
  if (lower.includes('blocked_by_client')) return true
  return false
}

/**
 * 是否需要触发弱网/断网降级逻辑
 * - 超时:直接触发
 * - 网络错误:直接触发(避免 wifi 但无网场景漏判)
 * @param {Error} error - 请求错误对象
 * @returns {Promise<boolean>} 是否需要触发降级
 */
const should_handle_bad_network = async (error) => {
  if (is_timeout_error(error)) return true
  return is_network_error(error)
}

/**
 * 处理请求超时错误
 * - 显示超时提示 modal
 * - 若有离线预约记录缓存,则跳转至离线预约列表页
 * - 否则提示用户检查网络连接
 */
const handle_request_timeout = async () => {
  if (has_shown_timeout_modal) return
  has_shown_timeout_modal = true

  const pages = Taro.getCurrentPages ? Taro.getCurrentPages() : []
  const current_page = pages && pages.length ? pages[pages.length - 1] : null
  const current_route = current_page?.route || ''
  if (String(current_route).includes('pages/offlineBookingList/index')) return

  // 若有离线预约记录缓存,则跳转至离线预约列表页
  if (has_offline_booking_cache()) {
    try {
      await Taro.reLaunch({ url: '/pages/offlineBookingList/index' })
    } catch (e) {
      console.error('reLaunch offlineBookingList failed:', e)
    }
    return
  }

  // 否则提示用户检查网络连接
  try {
    await Taro.showModal(get_weak_network_modal_no_cache_options())
  } catch (e) {
    console.error('show weak network modal failed:', e)
  }
}

// request interceptor
service.interceptors.request.use(
  config => {
    // console.warn(config)
    // console.warn(store)

    /**
     * 动态获取sessionid并设置到请求头
     * 确保每个请求都带上最新的sessionid
     */
    const sessionid = getSessionId();
    if (sessionid) {
      config.headers.cookie = sessionid;
    }

    // 增加时间戳
    if (config.method === 'get') {
        config.params = { ...config.params, timestamp: (new Date()).valueOf() }
    }

    // if ((config.method || '').toLowerCase() === 'post') {
    //   const url = config.url || ''
    //   const headers = config.headers || {}
    //   const contentType = headers['content-type'] || headers['Content-Type']
    //   const shouldUrlEncode =
    //     !contentType || String(contentType).includes('application/x-www-form-urlencoded')

    //   if (shouldUrlEncode && !strExist(['upload.qiniup.com'], url) && isPlainObject(config.data)) {
    //     config.headers = {
    //       ...headers,
    //       'content-type': 'application/x-www-form-urlencoded'
    //     }
    //     config.data = qs.stringify(config.data)
    //   }
    // }

    return config
  },
  error => {
    // do something with request error
    console.error(error, 'err') // for debug
    return Promise.reject(error)
  }
)

// response interceptor
service.interceptors.response.use(
  /**
   * 响应拦截器说明
   * - 这里统一处理后端自定义 code(例如 401 未授权)
   * - 如需拿到 headers/status 等原始信息,直接返回 response 即可
   */
  async response => {
    const res = response.data

    // 401 未授权处理
    if (res.code === 401) {
        const config = response?.config || {}
        /**
         * 避免死循环/重复重试:
         * - __is_retry:本次请求是 401 后的重试请求,如果仍 401,不再继续重试
         */
        if (config.__is_retry) {
          return response
        }

        /**
         * 记录来源页:用于授权成功后回跳
         * - 避免死循环:如果已经在 auth 页则不重复记录/跳转
         */
        const pages = Taro.getCurrentPages();
        const currentPage = pages[pages.length - 1];
        if (currentPage && currentPage.route !== 'pages/auth/index') {
            saveCurrentPagePath()
        }

        try {
          // 优先走静默续期:成功后重放原请求
          await refreshSession()
          const retry_config = { ...config, __is_retry: true }
          return await service(retry_config)
        } catch (error) {
          // 静默续期失败:降级跳转到授权页(由授权页完成授权并回跳)
          const pages_retry = Taro.getCurrentPages();
          const current_page_retry = pages_retry[pages_retry.length - 1];
          if (current_page_retry && current_page_retry.route !== 'pages/auth/index') {
            navigateToAuth()
          }
          return response
        }
    }

    if (['预约ID不存在'].includes(res.msg)) {
      res.show = false;
    }

    return response
  },
  async error => {
    // Taro.showToast({
    //   title: error.message,
    //   icon: 'none',
    //   duration: 2000
    // })
    if (await should_handle_bad_network(error)) {
      handle_request_timeout()
    }
    return Promise.reject(error)
  }
)

export default service