request.ts 7.43 KB
import axios, {
  type AxiosInstance,
  type AxiosResponse,
  type AxiosRequestConfig,
} from "axios";
import { ElMessage } from "element-plus";

// 多环境API配置
const API_BASE_URL = (() => {
  if (import.meta.env.MODE === "production") {
    // 从 vite.config.ts 中注入的 VITE_APP_BASE_URL 获取地址
    const baseUrl = import.meta.env.VITE_APP_BASE_URL;
    return `${baseUrl}/api`; // 生产环境使用子路径
  } else if (import.meta.env.MODE === "test") {
    return "http://192.168.0.193:8383/api"; // 测试环境
  } else {
    return "/api"; // 开发环境使用代理(相对路径)
  }
})();

// 创建 axios 实例
const service: AxiosInstance = axios.create({
  baseURL: API_BASE_URL,
  timeout: 60000,
});

// 刷新令牌相关状态
let isRefreshing = false;
let refreshSubscribers: Array<(token: string) => void> = [];

const TOKEN_KEY = "auth_token";
const REFRESH_TOKEN_KEY = "refresh_token";

function onRefreshed(token: string) {
  refreshSubscribers.forEach((cb) => cb(token));
  refreshSubscribers = [];
}

function addRefreshSubscriber(callback: (token: string) => void) {
  refreshSubscribers.push(callback);
}

function isAuthUrl(url?: string): boolean {
  if (!url) return false;
  const authPaths = [
    "/users/login",
    "/users/register",
    "/users/verify",
    "/users/resend-activation",
    "/users/password/forgot",
    "/users/password/reset",
    "/users/logout",
    "/users/refresh",
  ];
  return authPaths.some((p) => url.endsWith(p));
}

// 直接使用 axios(无拦截器)调用刷新接口,避免递归
async function refreshAccessTokenDirect(): Promise<string> {
  const refreshToken =
    localStorage.getItem(REFRESH_TOKEN_KEY) ||
    localStorage.getItem("refresh_token");
  if (!refreshToken) {
    return Promise.reject(new Error("No refresh token"));
  }
  const { data } = await axios.post(`${API_BASE_URL}/users/refresh`, {
    refreshToken,
  });
  const newToken = data?.token;
  const newRefreshToken = data?.refreshToken;
  if (!newToken) {
    throw new Error("No token in refresh response");
  }

  // 更新本地存储与默认头
  localStorage.setItem(TOKEN_KEY, newToken);
  if (newRefreshToken) {
    localStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken);
  }
  if (service.defaults && service.defaults.headers) {
    service.defaults.headers.common["Authorization"] = `Bearer ${newToken}`;
  }
  return newToken;
}

// 请求拦截器
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // 处理 GET 请求的 Content-Type 问题
    if (config.method?.toUpperCase() === "GET") {
      if (config.headers) {
        delete config.headers["Content-Type"];
        delete config.headers["content-type"];
      }
    } else {
      // 非 GET 请求默认加上 Content-Type
      if (config.headers && !config.headers["Content-Type"] && !config.headers["content-type"]) {
        config.headers["Content-Type"] = "application/json";
      }
    }

    // 从 localStorage 获取 token
    const token =
      localStorage.getItem("auth_token") || localStorage.getItem("token");
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`;
    }

    // 从 localStorage 获取用户信息并设置 X-User-ID 头
    const userInfoStr = localStorage.getItem("userInfo");
    if (userInfoStr) {
      try {
        const userInfo = JSON.parse(userInfoStr);
        if (userInfo && userInfo.id && config.headers) {
          config.headers["X-User-ID"] = String(userInfo.id);
        }
      } catch (e) {
        console.error("Failed to parse userInfo:", e);
      }
    }

    return config;
  },
  (error: any) => {
    console.error("Request error:", error);
    return Promise.reject(error);
  },
);

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const res = response.data;
    const config = response.config as any;

    // 如果返回的状态码不是 2xx,且没有开启静默模式,则判定为错误
    // 例如 POST 常见返回 201 Created,这里也应视为成功
    if ((response.status < 200 || response.status >= 300) && !config?._silent) {
      ElMessage.error(res.message || "Error");
      return Promise.reject(new Error(res.message || "Error"));
    }

    return response;
  },
  (error: any) => {
    const originalRequest: any = error?.config || {};

    // 如果是静默模式,直接 reject 不弹窗
    if (originalRequest._silent) {
      return Promise.reject(error);
    }

    console.error("Response error:", error);

    if (error.response) {
      const { status, data } = error.response;
      console.log(status, data);
      // 统一处理 401/403:优先尝试使用 refreshToken 刷新,然后重放请求
      if (
        (status === 401 || status === 403) &&
        !isAuthUrl(originalRequest.url)
      ) {
        const hasRefresh = !!(
          localStorage.getItem(REFRESH_TOKEN_KEY) ||
          localStorage.getItem("refresh_token")
        );

        if (!hasRefresh) {
          localStorage.removeItem(TOKEN_KEY);
          localStorage.removeItem(REFRESH_TOKEN_KEY);
          ElMessage.error(data?.message || "登录已过期,请重新登录");
          window.location.href = "/auth";
          return Promise.reject(error);
        }

        if ((originalRequest as any)._retry) {
          // 已重试过一次,避免循环
          localStorage.removeItem(TOKEN_KEY);
          localStorage.removeItem(REFRESH_TOKEN_KEY);
          ElMessage.error(data?.message || "登录已过期,请重新登录");
          window.location.href = "/auth";
          return Promise.reject(error);
        }

        if (isRefreshing) {
          return new Promise((resolve: (value: unknown) => void) => {
            addRefreshSubscriber((newToken: string) => {
              if (!originalRequest.headers) originalRequest.headers = {};
              originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
              (originalRequest as any)._retry = true;
              resolve(service(originalRequest));
            });
          });
        }

        (originalRequest as any)._retry = true;
        isRefreshing = true;
        return refreshAccessTokenDirect()
          .then((newToken) => {
            isRefreshing = false;
            onRefreshed(newToken);
            if (!originalRequest.headers) originalRequest.headers = {};
            originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
            return service(originalRequest);
          })
          .catch((e) => {
            isRefreshing = false;
            // 清空订阅,避免内存泄漏
            refreshSubscribers = [];
            localStorage.removeItem(TOKEN_KEY);
            localStorage.removeItem(REFRESH_TOKEN_KEY);
            ElMessage.error("登录已过期,请重新登录");
            window.location.href = "/auth";
            return Promise.reject(e);
          });
      }

      // 其他状态码处理
      switch (status) {
        case 404:
          ElMessage.error(data?.message || "请求的资源不存在");
          break;
        case 500:
          ElMessage.error(data?.message || "服务器错误");
          break;
        default:
          ElMessage.error(data?.message || `请求失败 (${status})`);
      }
    } else if (error.request) {
      ElMessage.error("网络错误,请检查您的网络连接");
    } else {
      ElMessage.error(error.message || "请求失败");
    }

    return Promise.reject(error);
  },
);

export { service, API_BASE_URL };
export default service;