122 lines
3.4 KiB
TypeScript
122 lines
3.4 KiB
TypeScript
import axios, {
|
||
AxiosInstance,
|
||
AxiosRequestConfig,
|
||
Method,
|
||
AxiosResponse,
|
||
} from "axios";
|
||
import { Toast } from "antd-mobile";
|
||
|
||
const DEFAULT_DEBOUNCE_GAP = 1000;
|
||
const debounceMap = new Map<string, number>();
|
||
|
||
// 死循环请求拦截配置
|
||
const FAIL_LIMIT = 3;
|
||
const BLOCK_TIME = 30 * 1000; // 30秒
|
||
const failMap = new Map<
|
||
string,
|
||
{ count: number; lastFail: number; blockedUntil?: number }
|
||
>();
|
||
|
||
const instance: AxiosInstance = axios.create({
|
||
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api",
|
||
timeout: 10000,
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
});
|
||
|
||
instance.interceptors.request.use((config) => {
|
||
const token = localStorage.getItem("token");
|
||
if (token) {
|
||
config.headers = config.headers || {};
|
||
config.headers["Authorization"] = `Bearer ${token}`;
|
||
}
|
||
return config;
|
||
});
|
||
|
||
instance.interceptors.response.use(
|
||
(res: AxiosResponse) => {
|
||
const { code, success, msg } = res.data || {};
|
||
if (code === 200 || success) {
|
||
return res.data.data ?? res.data;
|
||
}
|
||
Toast.show({ content: msg || "接口错误", position: "top" });
|
||
if (code === 401) {
|
||
localStorage.removeItem("token");
|
||
const currentPath = window.location.pathname + window.location.search;
|
||
if (currentPath === "/login") {
|
||
window.location.href = "/login";
|
||
} else {
|
||
window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
|
||
}
|
||
}
|
||
return Promise.reject(msg || "接口错误");
|
||
},
|
||
(err) => {
|
||
Toast.show({ content: err.message || "网络异常", position: "top" });
|
||
return Promise.reject(err);
|
||
}
|
||
);
|
||
|
||
export function request(
|
||
url: string,
|
||
data?: any,
|
||
method: Method = "GET",
|
||
config?: AxiosRequestConfig,
|
||
debounceGap?: number
|
||
): Promise<any> {
|
||
const gap =
|
||
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||
const now = Date.now();
|
||
const last = debounceMap.get(key) || 0;
|
||
|
||
// 死循环拦截:如果被block,直接拒绝
|
||
const failInfo = failMap.get(key);
|
||
if (failInfo && failInfo.blockedUntil && now < failInfo.blockedUntil) {
|
||
Toast.show({ content: "请求失败过多,请稍后再试", position: "top" });
|
||
return Promise.reject("请求失败过多,请稍后再试");
|
||
}
|
||
|
||
if (gap > 0 && now - last < gap) {
|
||
Toast.show({ content: "请求过于频繁,请稍后再试", position: "top" });
|
||
return Promise.reject("请求过于频繁,请稍后再试");
|
||
}
|
||
debounceMap.set(key, now);
|
||
|
||
const axiosConfig: AxiosRequestConfig = {
|
||
url,
|
||
method,
|
||
...config,
|
||
};
|
||
if (method.toUpperCase() === "GET") {
|
||
axiosConfig.params = data;
|
||
} else {
|
||
axiosConfig.data = data;
|
||
}
|
||
return instance(axiosConfig)
|
||
.then((res) => {
|
||
// 成功则清除失败计数
|
||
failMap.delete(key);
|
||
return res;
|
||
})
|
||
.catch((err) => {
|
||
debounceMap.delete(key);
|
||
// 失败计数
|
||
const fail = failMap.get(key) || { count: 0, lastFail: 0 };
|
||
const newCount = now - fail.lastFail < BLOCK_TIME ? fail.count + 1 : 1;
|
||
if (newCount >= FAIL_LIMIT) {
|
||
failMap.set(key, {
|
||
count: newCount,
|
||
lastFail: now,
|
||
blockedUntil: now + BLOCK_TIME,
|
||
});
|
||
} else {
|
||
failMap.set(key, { count: newCount, lastFail: now });
|
||
}
|
||
throw err;
|
||
});
|
||
}
|
||
|
||
export default request;
|