ci: Prettier纳入两层防御体系 (#520)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 7m4s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 7m54s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Successful in 3m28s

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #520.
This commit is contained in:
2026-07-18 18:08:19 +08:00
committed by auto-approve-bot
parent 1ee779a566
commit 0fcb77b991
145 changed files with 10310 additions and 12869 deletions
+78 -89
View File
@@ -2,10 +2,10 @@
* API 客户端配置
* 封装 Axios 实例,配置拦截器和 Token 管理
*/
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
import { message } from "antd";
import { useAuthStore } from "@/store/authStore";
import { refreshAccessToken } from "./auth";
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios"
import { message } from "antd"
import { useAuthStore } from "@/store/authStore"
import { refreshAccessToken } from "./auth"
// 创建 Axios 实例
const apiClient = axios.create({
@@ -14,161 +14,150 @@ const apiClient = axios.create({
headers: {
"Content-Type": "application/json",
},
});
})
// ── Token 刷新队列 ─────────────────────────────────────────
let isRefreshing = false;
let isRefreshing = false
let failedQueue: Array<{
resolve: (value?: unknown) => void;
reject: (reason?: unknown) => void;
}> = [];
resolve: (value?: unknown) => void
reject: (reason?: unknown) => void
}> = []
const processQueue = (error: unknown | null, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
prom.reject(error)
} else {
prom.resolve(token);
prom.resolve(token)
}
});
failedQueue = [];
};
})
failedQueue = []
}
// 请求拦截器:添加 Token
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem("access_token");
const token = localStorage.getItem("access_token")
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
config.headers.Authorization = `Bearer ${token}`
}
return config;
return config
},
(error: AxiosError) => {
return Promise.reject(error);
return Promise.reject(error)
},
);
)
// 响应拦截器:统一错误提示 + 401 自动刷新 Token
apiClient.interceptors.response.use(
(response) => response,
async (
error: AxiosError<{ detail?: string; message?: string; msg?: string }>,
) => {
async (error: AxiosError<{ detail?: string; message?: string; msg?: string }>) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
_retry?: boolean
}
// 401 → 尝试刷新 Token
if (
error.response?.status === 401 &&
originalRequest &&
!originalRequest._retry
) {
const refreshToken = useAuthStore.getState().refreshToken;
if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
const refreshToken = useAuthStore.getState().refreshToken
// 无 refresh_token → 直接登出
if (!refreshToken) {
useAuthStore.getState().clearAuth();
window.location.href = "/";
return Promise.reject(error);
useAuthStore.getState().clearAuth()
window.location.href = "/"
return Promise.reject(error)
}
// 已在刷新中 → 排队等待
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
failedQueue.push({ resolve, reject })
}).then((token) => {
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${token}`;
originalRequest.headers.Authorization = `Bearer ${token}`
}
return apiClient(originalRequest);
});
return apiClient(originalRequest)
})
}
originalRequest._retry = true;
isRefreshing = true;
originalRequest._retry = true
isRefreshing = true
try {
const data = await refreshAccessToken(refreshToken);
const newAccessToken = data.access_token;
const newRefreshToken = data.refresh_token ?? refreshToken;
const data = await refreshAccessToken(refreshToken)
const newAccessToken = data.access_token
const newRefreshToken = data.refresh_token ?? refreshToken
// 更新 Zustand + localStorage
useAuthStore
.getState()
.setAuth(
useAuthStore.getState().user!,
newAccessToken,
newRefreshToken,
);
.setAuth(useAuthStore.getState().user!, newAccessToken, newRefreshToken)
// 处理排队的请求
processQueue(null, newAccessToken);
processQueue(null, newAccessToken)
// 重试原始请求
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`
}
return apiClient(originalRequest);
return apiClient(originalRequest)
} catch (refreshError) {
// 刷新失败 → 登出
processQueue(refreshError, null);
useAuthStore.getState().clearAuth();
window.location.href = "/";
return Promise.reject(refreshError);
processQueue(refreshError, null)
useAuthStore.getState().clearAuth()
window.location.href = "/"
return Promise.reject(refreshError)
} finally {
isRefreshing = false;
isRefreshing = false
}
}
// 提取后端返回的错误信息(detail / message / msg
// 注意:后端返回的字段可能是对象 {code, message} 而非字符串,需要安全提取
const data = error.response?.data;
const rawServerMsg = data?.detail || data?.message || data?.msg;
const data = error.response?.data
const rawServerMsg = data?.detail || data?.message || data?.msg
// 安全提取字符串:递归处理嵌套对象(后端可能返回 {code, message: {code, message}} 等)
const safeExtractString = (val: unknown): string => {
if (typeof val === "string") return val;
if (typeof val === "string") return val
if (typeof val === "object" && val !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定
const obj = val as Record<string, any>;
if (typeof obj.message === "string") return obj.message;
if (typeof obj.msg === "string") return obj.msg;
if (typeof obj.detail === "string") return obj.detail;
const obj = val as Record<string, any>
if (typeof obj.message === "string") return obj.message
if (typeof obj.msg === "string") return obj.msg
if (typeof obj.detail === "string") return obj.detail
// 嵌套对象:递归提取
if (typeof obj.message === "object" && obj.message !== null)
return safeExtractString(obj.message);
if (typeof obj.msg === "object" && obj.msg !== null)
return safeExtractString(obj.msg);
return JSON.stringify(val);
return safeExtractString(obj.message)
if (typeof obj.msg === "object" && obj.msg !== null) return safeExtractString(obj.msg)
return JSON.stringify(val)
}
return "";
};
const serverMsg = safeExtractString(rawServerMsg);
let handled = false;
return ""
}
const serverMsg = safeExtractString(rawServerMsg)
let handled = false
if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
message.error("请求超时,请检查网络后重试");
handled = true;
message.error("请求超时,请检查网络后重试")
handled = true
} else if (!error.response) {
message.error("网络连接异常,请检查网络设置");
handled = true;
message.error("网络连接异常,请检查网络设置")
handled = true
} else if (serverMsg) {
message.error(serverMsg);
handled = true;
message.error(serverMsg)
handled = true
} else {
const status = error.response?.status;
const status = error.response?.status
if (status === 413) {
message.error("文件过大,请缩小后重试");
handled = true;
message.error("文件过大,请缩小后重试")
handled = true
} else if (status === 415) {
message.error("不支持的文件格式");
handled = true;
message.error("不支持的文件格式")
handled = true
} else if (status === 503) {
message.error("服务暂不可用,请稍后再试");
handled = true;
message.error("服务暂不可用,请稍后再试")
handled = true
} else if (status && status >= 500) {
message.error("服务器繁忙,请稍后再试");
handled = true;
message.error("服务器繁忙,请稍后再试")
handled = true
}
// 其他 4xx 且无具体信息时不弹通用提示,由各组件自行处理
}
@@ -176,11 +165,11 @@ apiClient.interceptors.response.use(
// 标记已展示过提示,组件 onError 可据此跳过重复 toast
if (handled) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error as any).__msgShown = true;
;(error as any).__msgShown = true
}
return Promise.reject(error);
return Promise.reject(error)
},
);
)
export default apiClient;
export default apiClient