diff --git a/src/composables/sse/useDictSync.ts b/src/composables/sse/useDictSync.ts index d174f9c2..cc59185d 100644 --- a/src/composables/sse/useDictSync.ts +++ b/src/composables/sse/useDictSync.ts @@ -19,6 +19,7 @@ function createDictSyncComposable() { const callbacks: DictChangeCallback[] = []; let unsubscribe: (() => void) | null = null; + let initialized = false; // 防止重复初始化导致重复订阅 // 处理字典变更消息:清除指定字典缓存,并通知所有已注册回调 const handleDictChange = (data: DictChangeMessage) => { @@ -38,13 +39,16 @@ function createDictSyncComposable() { }); }; - // 订阅 SSE 字典变更事件 + // 订阅 SSE 字典变更事件(幂等:重复调用不会产生重复订阅) const initialize = () => { + if (initialized) return; + initialized = true; unsubscribe = sse.on(SseTopics.DICT, handleDictChange); }; // 取消 SSE 订阅并清空所有回调 const cleanup = () => { + initialized = false; unsubscribe?.(); unsubscribe = null; callbacks.length = 0; diff --git a/src/composables/sse/useSse.ts b/src/composables/sse/useSse.ts index 7f901a29..60c9492c 100644 --- a/src/composables/sse/useSse.ts +++ b/src/composables/sse/useSse.ts @@ -1,4 +1,5 @@ import { AuthStorage } from "@/utils/auth"; +import { useUserStoreHook } from "@/stores/user"; /** SSE 连接配置选项 */ export interface UseSseOptions { @@ -58,6 +59,7 @@ function createSseConnection(options: UseSseOptions = {}) { let reconnectTimer: ReturnType | null = null; let reconnectAttempts = 0; let currentReconnectInterval = config.reconnectInterval; + let tokenRefreshed = false; // 本轮拒绝是否已刷新过令牌,防止无限刷新 const eventHandlers = new Map>(); @@ -189,7 +191,8 @@ function createSseConnection(options: UseSseOptions = {}) { const token = AuthStorage.getAccessToken(); if (!token) { log("未检测到有效令牌,稍后重试"); - reconnectTimer = setTimeout(() => connect(), config.reconnectInterval); + // 走统一重连调度,受 maxReconnectAttempts 上限约束 + scheduleReconnect(); return; } @@ -213,9 +216,26 @@ function createSseConnection(options: UseSseOptions = {}) { }, signal: abortController.signal, }) - .then((response) => { + .then(async (response) => { if (!response.ok) { if (response.status === 401 || response.status === 403) { + // 令牌过期:刷新后用新令牌重连,刷新失败或令牌仍无效则停止重连 + if (!tokenRefreshed) { + tokenRefreshed = true; + connectionTimeoutTimer = clearTimer(connectionTimeoutTimer); + try { + const userStore = useUserStoreHook(); + await userStore.refreshTokenOnce(); + if (AuthStorage.getAccessToken()) { + connectionState.value = SseConnectionState.DISCONNECTED; + log(`SSE 连接被拒绝(HTTP ${response.status}),令牌已刷新,使用新令牌重连`); + connect(); + return null; + } + } catch (err) { + logError("SSE 令牌刷新失败:", err); + } + } isManualDisconnect = true; connectionState.value = SseConnectionState.DISCONNECTED; log(`SSE 连接被拒绝(HTTP ${response.status}),不再重连`); @@ -225,6 +245,7 @@ function createSseConnection(options: UseSseOptions = {}) { } connectionTimeoutTimer = clearTimer(connectionTimeoutTimer); connectionState.value = SseConnectionState.CONNECTED; + tokenRefreshed = false; resetReconnectState(); log("SSE 连接已建立"); return response.body?.getReader();