request.js 9.22 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 { parseQueryString } from './tools'

// 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'

/**
 * @description 获取 sessionid 的工具函数
 * - sessionid 由 authRedirect.refreshSession 写入
 * - 每次请求前动态读取,避免旧会话导致的 401
 * @returns {string|null} sessionid或null
 */
export const getSessionId = () => {
  try {
    return Taro.getStorageSync('sessionid') || null
  } catch (error) {
    console.error('获取sessionid失败:', error)
    return null
  }
}

/**
 * @description 设置 sessionid(一般不需要手动调用)
 * - 正常情况下由 authRedirect.refreshSession 写入
 * - 保留该方法用于极端场景的手动修复/兼容旧逻辑
 * @param {string} sessionid cookie 字符串
 * @returns {void} 无返回值
 */
export const setSessionId = sessionid => {
  try {
    if (!sessionid) {
      return
    }
    Taro.setStorageSync('sessionid', sessionid)
  } catch (error) {
    console.error('设置sessionid失败:', error)
  }
}

/**
 * @description 清空 sessionid(一般不需要手动调用)
 * @returns {void} 无返回值
 */
export const clearSessionId = () => {
  try {
    Taro.removeStorageSync('sessionid')
  } catch (error) {
    console.error('清空sessionid失败:', error)
  }
}

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

/**
 * @description axios 实例(axios-miniprogram)
 * - 统一 baseURL / timeout
 * - 通过拦截器处理:默认参数、cookie 注入、401 自动续期、弱网降级
 */
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

/**
 * @description 判断是否为超时错误
 * @param {Error} error 请求错误对象
 * @returns {boolean} true=超时,false=非超时
 */

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

/**
 * @description 判断是否为网络错误(断网/弱网/请求失败等)
 * @param {Error} error 请求错误对象
 * @returns {boolean} true=网络错误,false=非网络错误
 */
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
}

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

/**
 * @description 处理请求超时/弱网错误
 * - 优先:若存在离线预约记录缓存,直接跳转离线预约列表页
 * - 否则:弹出弱网提示(统一文案由 uiText 管理)
 * @returns {Promise<void>} 无返回值
 */
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)
  }
}

// 请求拦截器:合并默认参数 / 注入 cookie
service.interceptors.request.use(
  config => {
    // console.warn(config)
    // console.warn(store)

    // 解析 URL 参数并合并
    const url = config.url || ''
    let url_params = {}
    if (url.includes('?')) {
      url_params = parseQueryString(url)
      config.url = url.split('?')[0]
    }

    // 优先级:调用传参 > URL参数 > 默认参数
    config.params = {
      ...REQUEST_DEFAULT_PARAMS,
      ...url_params,
      ...(config.params || {})
    }

    /**
     * 动态获取 sessionid 并设置到请求头
     * - 确保每个请求都带上最新的 sessionid
     * - 注意:axios-miniprogram 的 headers 可能不存在,需要先兜底
     */
    const sessionid = getSessionId()
    if (sessionid) {
      config.headers = config.headers || {}
      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 => {
    console.error('请求拦截器异常:', error)
    return Promise.reject(error)
  }
)

// 响应拦截器:401 自动续期 / 弱网降级
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