255 lines
7.9 KiB
TypeScript
255 lines
7.9 KiB
TypeScript
// src/utils/request.js
|
||
import axios from "axios";
|
||
import { generateRandomString } from "./index";
|
||
import i18n from '@/i18n'
|
||
import { getApiBaseUrl } from './api-base';
|
||
|
||
const t = (key: string) => i18n.global.t(key)
|
||
|
||
// API基础URL
|
||
const BASE_URL = getApiBaseUrl();
|
||
|
||
|
||
// 创建Axios实例
|
||
const instance = axios.create({
|
||
baseURL: BASE_URL, // 使用配置的API基础URL
|
||
timeout: 30000, // 请求超时时间
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"X-Request-ID": `${generateRandomString(12)}`,
|
||
},
|
||
});
|
||
|
||
// 获取当前用户语言(用于 Accept-Language header)
|
||
function getCurrentLanguage(): string {
|
||
return i18n.global.locale?.value || localStorage.getItem('locale') || 'zh-CN'
|
||
}
|
||
|
||
|
||
instance.interceptors.request.use(
|
||
(config) => {
|
||
// 添加JWT token认证
|
||
const token = localStorage.getItem('weknora_token');
|
||
if (token) {
|
||
config.headers["Authorization"] = `Bearer ${token}`;
|
||
}
|
||
|
||
// 添加用户语言偏好
|
||
config.headers["Accept-Language"] = getCurrentLanguage();
|
||
|
||
// 添加跨租户访问请求头(如果选择了其他租户)
|
||
const selectedTenantId = localStorage.getItem('weknora_selected_tenant_id');
|
||
const defaultTenantId = localStorage.getItem('weknora_tenant');
|
||
if (selectedTenantId) {
|
||
try {
|
||
const defaultTenant = defaultTenantId ? JSON.parse(defaultTenantId) : null;
|
||
const defaultId = defaultTenant?.id ? String(defaultTenant.id) : null;
|
||
// 如果选择的租户ID与默认租户ID不同,添加请求头
|
||
if (selectedTenantId !== defaultId) {
|
||
config.headers["X-Tenant-ID"] = selectedTenantId;
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to parse tenant info', e);
|
||
}
|
||
}
|
||
|
||
config.headers["X-Request-ID"] = `${generateRandomString(12)}`;
|
||
return config;
|
||
},
|
||
(error) => {
|
||
return Promise.reject(error);
|
||
}
|
||
);
|
||
|
||
// Token刷新标志,防止多个请求同时刷新token
|
||
let isRefreshing = false;
|
||
let failedQueue: Array<{ resolve: Function; reject: Function }> = [];
|
||
let hasRedirectedOn401 = false;
|
||
|
||
// 处理队列中的请求
|
||
const processQueue = (error: any, token: string | null = null) => {
|
||
failedQueue.forEach(({ resolve, reject }) => {
|
||
if (error) {
|
||
reject(error);
|
||
} else {
|
||
resolve(token);
|
||
}
|
||
});
|
||
|
||
failedQueue = [];
|
||
};
|
||
|
||
instance.interceptors.response.use(
|
||
(response) => {
|
||
// 根据业务状态码处理逻辑
|
||
const { status, data } = response;
|
||
if (status >= 200 && status < 300) {
|
||
return data;
|
||
} else {
|
||
return Promise.reject(data);
|
||
}
|
||
},
|
||
async (error: any) => {
|
||
const originalRequest = error.config;
|
||
|
||
if (!error.response) {
|
||
return Promise.reject({ message: t('error.networkError') });
|
||
}
|
||
|
||
// 如果是登录接口的401,直接返回错误以便页面展示toast,不做跳转
|
||
if (error.response.status === 401 && originalRequest?.url?.includes('/auth/login')) {
|
||
const { status, data } = error.response;
|
||
return Promise.reject({ status, message: (typeof data === 'object' ? data?.message : data) || t('error.invalidCredentials') });
|
||
}
|
||
|
||
// 如果是401错误且不是刷新token的请求,尝试刷新token
|
||
if (error.response.status === 401 && !originalRequest._retry && !originalRequest.url?.includes('/auth/refresh')) {
|
||
if (isRefreshing) {
|
||
// 如果正在刷新token,将请求加入队列
|
||
return new Promise((resolve, reject) => {
|
||
failedQueue.push({ resolve, reject });
|
||
}).then(token => {
|
||
originalRequest.headers['Authorization'] = 'Bearer ' + token;
|
||
return instance(originalRequest);
|
||
}).catch(err => {
|
||
return Promise.reject(err);
|
||
});
|
||
}
|
||
|
||
originalRequest._retry = true;
|
||
isRefreshing = true;
|
||
|
||
const refreshToken = localStorage.getItem('weknora_refresh_token');
|
||
|
||
if (refreshToken) {
|
||
try {
|
||
// 动态导入refresh token API
|
||
const { refreshToken: refreshTokenAPI } = await import('../api/auth/index');
|
||
const response = await refreshTokenAPI(refreshToken);
|
||
|
||
if (response.success && response.data) {
|
||
const { token, refreshToken: newRefreshToken } = response.data;
|
||
|
||
// 更新localStorage中的token
|
||
localStorage.setItem('weknora_token', token);
|
||
localStorage.setItem('weknora_refresh_token', newRefreshToken);
|
||
|
||
// 更新请求头
|
||
originalRequest.headers['Authorization'] = 'Bearer ' + token;
|
||
|
||
// 处理队列中的请求
|
||
processQueue(null, token);
|
||
|
||
return instance(originalRequest);
|
||
} else {
|
||
throw new Error(response.message || t('error.tokenRefreshFailed'));
|
||
}
|
||
} catch (refreshError) {
|
||
// 刷新失败,清除所有token并跳转到登录页
|
||
localStorage.removeItem('weknora_token');
|
||
localStorage.removeItem('weknora_refresh_token');
|
||
localStorage.removeItem('weknora_user');
|
||
localStorage.removeItem('weknora_tenant');
|
||
|
||
processQueue(refreshError, null);
|
||
|
||
// 跳转到登录页
|
||
if (!hasRedirectedOn401 && typeof window !== 'undefined') {
|
||
hasRedirectedOn401 = true;
|
||
window.location.href = '/login';
|
||
}
|
||
|
||
return Promise.reject(refreshError);
|
||
} finally {
|
||
isRefreshing = false;
|
||
}
|
||
} else {
|
||
// 没有refresh token,直接跳转到登录页
|
||
localStorage.removeItem('weknora_token');
|
||
localStorage.removeItem('weknora_user');
|
||
localStorage.removeItem('weknora_tenant');
|
||
|
||
if (!hasRedirectedOn401 && typeof window !== 'undefined') {
|
||
hasRedirectedOn401 = true;
|
||
window.location.href = '/login';
|
||
}
|
||
|
||
return Promise.reject({ message: t('error.pleaseRelogin') });
|
||
}
|
||
}
|
||
|
||
// 处理 Nginx 413 Request Entity Too Large
|
||
if (error.response.status === 413) {
|
||
return Promise.reject({
|
||
status: 413,
|
||
message: t('error.fileSizeExceeded'),
|
||
success: false
|
||
});
|
||
}
|
||
|
||
const { status, data } = error.response;
|
||
// 将HTTP状态码一并抛出,方便上层判断401等场景
|
||
// 后端返回格式: { success: false, error: { code, message, details } }
|
||
// 提取 error.message 作为顶层 message,方便前端使用 error?.message 获取
|
||
let errorMessage: string | undefined;
|
||
if (typeof data === 'object') {
|
||
if (typeof data?.error === 'string') {
|
||
errorMessage = data.error;
|
||
} else if (data?.error?.message) {
|
||
errorMessage = data.error.message;
|
||
} else {
|
||
errorMessage = data?.message;
|
||
}
|
||
} else if (typeof data === 'string') {
|
||
errorMessage = data;
|
||
}
|
||
return Promise.reject({
|
||
status,
|
||
message: errorMessage,
|
||
...(typeof data === 'object' ? data : {})
|
||
});
|
||
}
|
||
);
|
||
|
||
export function get(url: string) {
|
||
return instance.get(url);
|
||
}
|
||
|
||
export async function getDown(url: string) {
|
||
let res = await instance.get(url, {
|
||
responseType: "blob",
|
||
});
|
||
return res
|
||
}
|
||
|
||
export function postUpload(url: string, data = {}, onUploadProgress?: (progressEvent: any) => void) {
|
||
return instance.post(url, data, {
|
||
headers: {
|
||
"Content-Type": "multipart/form-data",
|
||
"X-Request-ID": `${generateRandomString(12)}`,
|
||
},
|
||
onUploadProgress,
|
||
});
|
||
}
|
||
|
||
export function postChat(url: string, data = {}) {
|
||
return instance.post(url, data, {
|
||
headers: {
|
||
"Content-Type": "text/event-stream;charset=utf-8",
|
||
"X-Request-ID": `${generateRandomString(12)}`,
|
||
},
|
||
});
|
||
}
|
||
|
||
export function post(url: string, data = {}, config?: any) {
|
||
return instance.post(url, data, config);
|
||
}
|
||
|
||
export function put(url: string, data = {}) {
|
||
return instance.put(url, data);
|
||
}
|
||
|
||
export function del(url: string, data?: any) {
|
||
return instance.delete(url, { data });
|
||
}
|