diff --git a/src/api/auth.ts b/src/api/auth/auth.ts similarity index 90% rename from src/api/auth.ts rename to src/api/auth/auth.ts index 960a825..11e3f38 100644 --- a/src/api/auth.ts +++ b/src/api/auth/auth.ts @@ -24,7 +24,8 @@ export interface LoginResponse { unionId: string | null; accessToken: string; refreshToken: string; - expire: number | null; + accessTokenExpireTime: number | null; + refreshTokenExpireTime: number | null; } // 刷新令牌响应数据 @@ -34,7 +35,8 @@ export interface RefreshTokenResponse { unionId: string | null; accessToken: string; refreshToken: string; - expire: number | null; + accessTokenExpireTime: number | null; + refreshTokenExpireTime: number | null; } /** diff --git a/src/api/index.ts b/src/api/auth/index.ts similarity index 72% rename from src/api/index.ts rename to src/api/auth/index.ts index 0870993..a24e3e9 100644 --- a/src/api/index.ts +++ b/src/api/auth/index.ts @@ -1,4 +1,4 @@ -export { chatAPI, type ChatMessage, type ChatRequest } from './aiChat' +export { chatAPI, type ChatMessage, type ChatRequest } from '../aiChat' // 认证 API export { diff --git a/src/plugins/axios.ts b/src/plugins/axios.ts index f95dc3c..8ee1d61 100644 --- a/src/plugins/axios.ts +++ b/src/plugins/axios.ts @@ -3,7 +3,18 @@ import axios, { type InternalAxiosRequestConfig, } from "axios"; import { ENV } from "../config"; -import { getAccessToken } from "@/utils/token"; +import { + getAccessToken, + getRefreshToken, + setTokens, + clearTokens, +} from "@/utils/token"; +import { Message } from "@arco-design/web-vue"; + +// 是否正在刷新 token +let isRefreshing = false; +// 存储因为 token 过期而挂起的请求 +let requests: ((token: string) => void)[] = []; const request: AxiosInstance = axios.create({ baseURL: ENV.API_BASE_URL, @@ -32,17 +43,83 @@ request.interceptors.request.use( // ======================= request.interceptors.response.use( (response) => { - console.log("响应: ", response); + // console.log("响应: ", response); const data = response.data; /**TODO: 增加响应码处理 */ return data; }, - (error) => { + async (error) => { + const originalRequest = error.config; + + // 处理 401 未授权情况 (Token 过期) + // 确保不是刷新 token 的请求本身 (避免死循环) + if ( + error.response?.status === 401 && + !originalRequest._retry && + !originalRequest.url.includes("/auth/refresh") + ) { + // 如果正在刷新,将当前请求加入队列等待 + if (isRefreshing) { + return new Promise((resolve) => { + requests.push((token: string) => { + originalRequest.headers.Authorization = `Bearer ${token}`; + resolve(request(originalRequest)); + }); + }); + } + + originalRequest._retry = true; + isRefreshing = true; + + try { + const refreshToken = getRefreshToken(); + if (!refreshToken) { + throw new Error("No refresh token available"); + } + + // 使用原生 axios 发送刷新请求,避免拦截器循环 + const response = await axios.post( + `${ENV.API_BASE_URL}/v1/auth/refresh`, + null, + { + params: { refreshToken }, + } + ); + + if (response.data?.success) { + const { accessToken, refreshToken: newRefreshToken } = + response.data.data; + // 更新本地存储 + setTokens(accessToken, newRefreshToken); + + // 执行队列中的请求 + requests.forEach((cb) => cb(accessToken)); + requests = []; + + // 重试当前请求 + originalRequest.headers.Authorization = `Bearer ${accessToken}`; + return request(originalRequest); + } else { + throw new Error("Refresh token failed"); + } + } catch (refreshError) { + console.error("Token 刷新失败:", refreshError); + // 清除过期 token + clearTokens(); + Message.error("登录已过期,请重新登录"); + // 这里可以选择跳转到登录页,例如 window.location.href = '/user/login' + // 建议让路由守卫或者页面自行处理未登录状态 + return Promise.reject(refreshError); + } finally { + isRefreshing = false; + } + } + /** * Promise.reject(error) 用来 返回一个状态为 rejected 的 Promise,相当于“主动抛出错误”, - * + * * 让调用者能够在 .catch() 或 try/catch 中捕获这个错误。 - 它在 异步流程、拦截器、错误处理 中非常常见。 + * 它在 异步流程、拦截器、错误处理 中非常常见。 */ console.error("❌ 网络错误:", error); return Promise.reject(error); diff --git a/src/views/ai/AiChatView.vue b/src/views/ai/AiChatView.vue index 7aae5b4..d483f4e 100644 --- a/src/views/ai/AiChatView.vue +++ b/src/views/ai/AiChatView.vue @@ -52,7 +52,7 @@ import { ref, nextTick, onMounted } from 'vue' import { Message } from '@arco-design/web-vue' import { IconRobot, IconUser, IconSend } from '@arco-design/web-vue/es/icon' -import { chatAPI, type ChatMessage as APIChatMessage } from '@/api' +import { chatAPI, type ChatMessage as APIChatMessage } from '@/api/auth' interface ChatMessage { role: 'user' | 'assistant' diff --git a/src/views/user/UserLoginView.vue b/src/views/user/UserLoginView.vue index e382300..cc4ec2e 100644 --- a/src/views/user/UserLoginView.vue +++ b/src/views/user/UserLoginView.vue @@ -74,7 +74,7 @@ import { reactive, ref } from "vue"; import { useRouter } from "vue-router"; import { Message } from "@arco-design/web-vue"; import { IconUser, IconLock } from "@arco-design/web-vue/es/icon"; -import { login } from "@/api/auth"; +import { login } from "@/api/auth/auth"; import { setTokens } from "@/utils/token"; import { useUserStore } from "@/store/user"; import ACCESS_ENUM from "@/access/accessEnum"; diff --git a/tsconfig.app.json b/tsconfig.app.json index dd2eed2..71992d0 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,6 +1,8 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], "declaration": true, "emitDeclarationOnly": false, "declarationDir": "./dist/types", @@ -26,4 +28,4 @@ "src/**/*.tsx", "src/**/*.vue" ] -} \ No newline at end of file +}