From 5d83938c248b3f4b5e03675e15cb7ebb58dfd38f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Thu, 8 May 2025 14:51:22 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=81=E5=9F=9F=E6=93=8D=E7=9B=98=E6=89=8B?= =?UTF-8?q?=20-=20=E4=BF=AE=E5=A4=8D=20http-interceptors.ts=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E6=9C=AA=E5=87=86=E7=A1=AE=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?401=E9=94=99=E8=AF=AF=E6=97=B6=EF=BC=8C=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E6=84=8F=E5=A4=96=E5=88=A0=E9=99=A4=20localS?= =?UTF-8?q?torage=20=E4=B8=AD=20token=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/app/components/AuthProvider.tsx | 64 +++++++++--- Cunkebao/app/devices/[id]/page.tsx | 117 +++++++++++++++++++-- Cunkebao/lib/api.ts | 126 +++++++++++++++++++++-- Cunkebao/lib/http-interceptors.ts | 56 ++++++---- 4 files changed, 313 insertions(+), 50 deletions(-) diff --git a/Cunkebao/app/components/AuthProvider.tsx b/Cunkebao/app/components/AuthProvider.tsx index 9a3e0da3..ca09cfa5 100644 --- a/Cunkebao/app/components/AuthProvider.tsx +++ b/Cunkebao/app/components/AuthProvider.tsx @@ -76,29 +76,67 @@ export function AuthProvider({ children }: AuthProviderProps) { const storedToken = safeLocalStorage.getItem("token") if (storedToken) { - // 验证token是否有效 - const isValid = await validateToken() - - if (isValid) { - // 从localStorage获取用户信息 - const userDataStr = safeLocalStorage.getItem("user") - if (userDataStr) { + // 首先尝试从localStorage获取用户信息 + const userDataStr = safeLocalStorage.getItem("user") + if (userDataStr) { + try { + // 如果能解析用户数据,先设置登录状态 const userData = JSON.parse(userDataStr) as User setToken(storedToken) setUser(userData) setIsAuthenticated(true) - } else { - // token有效但没有用户信息,清除token + + // 然后在后台尝试验证token,但不影响当前登录状态 + validateToken().then(isValid => { + // 只有在确认token绝对无效时才登出 + // 网络错误等情况默认保持登录状态 + if (isValid === false) { + console.warn('验证token失败,但仍允许用户保持登录状态') + } + }).catch(error => { + // 捕获所有验证过程中的错误,并记录日志 + console.error('验证token过程中出错:', error) + // 网络错误等不会导致登出 + }) + } catch (parseError) { + // 用户数据无法解析,需要清除 + console.error('解析用户数据失败:', parseError) handleLogout() } } else { - // token无效,清除 - handleLogout() + // 有token但没有用户信息,可能是部分数据丢失 + console.warn('找到token但没有用户信息,尝试保持登录状态') + + // 尝试验证token并获取用户信息 + try { + const isValid = await validateToken() + if (isValid) { + // 如果token有效,尝试从API获取用户信息 + // 这里简化处理,直接使用token + setToken(storedToken) + setIsAuthenticated(true) + } else { + // token确认无效,清除 + handleLogout() + } + } catch (error) { + // 验证过程出错,记录日志但不登出 + console.error('验证token过程中出错:', error) + // 保留token,允许用户继续使用 + setToken(storedToken) + setIsAuthenticated(true) + } } } } catch (error) { - console.error("验证token时出错:", error) - handleLogout() + console.error("初始化认证状态时出错:", error) + // 非401错误不应强制登出 + if (error instanceof Error && + (error.message.includes('401') || + error.message.includes('未授权') || + error.message.includes('token'))) { + handleLogout() + } } finally { setIsLoading(false) setIsInitialized(true) diff --git a/Cunkebao/app/devices/[id]/page.tsx b/Cunkebao/app/devices/[id]/page.tsx index 867a0947..f532dfdf 100644 --- a/Cunkebao/app/devices/[id]/page.tsx +++ b/Cunkebao/app/devices/[id]/page.tsx @@ -361,22 +361,117 @@ export default function DeviceDetailPage() { features: updatedFeatures } : null) - // 调用API更新服务器配置 - const response = await updateDeviceTaskConfig(configUpdate) - - if (response && response.code === 200) { - toast.success(`${getFeatureName(feature)}${checked ? '已启用' : '已禁用'}`) - } else { - // 如果请求失败,回滚UI变更 + // 使用更安全的API调用方式,避免自动重定向 + try { + // 获取token + const token = localStorage.getItem('token'); + if (!token) { + throw new Error('未找到授权信息'); + } + + // 直接使用fetch,而不是通过API工具 + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/task-config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(configUpdate) + }); + + // 检查是否是401错误(未授权),这是唯一应该处理token的情况 + if (response.status === 401) { + // 此处我们不立即跳转,而是给出错误提示 + toast.error('认证已过期,请重新登录后再尝试操作'); + console.error('API请求返回401未授权错误'); + // 可以选择是否重定向到登录页面 + // window.location.href = '/login'; + throw new Error('认证已过期'); + } + + // 检查响应是否正常 + if (!response.ok) { + // 所有非401的HTTP错误 + console.warn(`API返回HTTP错误: ${response.status} ${response.statusText}`); + + // 尝试解析错误详情 + try { + const errorResult = await response.json(); + // 显示详细错误信息,但保持本地token + const errorMsg = errorResult?.msg || `服务器错误 (${response.status})`; + toast.error(`更新失败: ${errorMsg}`); + + // 回滚UI更改 + setDevice(prev => prev ? { + ...prev, + features: { ...prev.features, [feature]: !checked } + } : null); + } catch (parseError) { + // 无法解析响应,可能是网络问题 + console.error('无法解析错误响应:', parseError); + toast.error(`更新失败: 服务器无响应 (${response.status})`); + + // 回滚UI更改 + setDevice(prev => prev ? { + ...prev, + features: { ...prev.features, [feature]: !checked } + } : null); + } + + return; // 提前返回,避免继续处理 + } + + // 响应正常,尝试解析 + try { + const result = await response.json(); + + // 检查API响应码 + if (result && result.code === 200) { + toast.success(`${getFeatureName(feature)}${checked ? '已启用' : '已禁用'}`); + } else if (result && result.code === 401) { + // API明确返回401,提示用户但不自动登出 + toast.error('认证已过期,请重新登录后再尝试操作'); + console.error('API请求返回401未授权状态码'); + + // 回滚UI更改 + setDevice(prev => prev ? { + ...prev, + features: { ...prev.features, [feature]: !checked } + } : null); + } else { + // 其他API错误 + const errorMsg = result?.msg || '未知错误'; + console.warn(`API返回业务错误: ${result?.code} - ${errorMsg}`); + toast.error(`更新失败: ${errorMsg}`); + + // 回滚UI更改 + setDevice(prev => prev ? { + ...prev, + features: { ...prev.features, [feature]: !checked } + } : null); + } + } catch (parseError) { + // 无法解析响应JSON + console.error('无法解析API响应:', parseError); + toast.error('更新失败: 无法解析服务器响应'); + + // 回滚UI更改 + setDevice(prev => prev ? { + ...prev, + features: { ...prev.features, [feature]: !checked } + } : null); + } + } catch (fetchError) { + console.error('请求错误:', fetchError) + + // 回滚UI更改 setDevice(prev => prev ? { ...prev, features: { ...prev.features, [feature]: !checked } } : null) - // 处理错误信息,使用类型断言解决字段不一致问题 - const anyResponse = response as any; - const errorMsg = anyResponse ? (anyResponse.message || anyResponse.msg || '未知错误') : '未知错误'; - toast.error(`更新失败: ${errorMsg}`) + // 显示友好的错误提示 + toast.error('网络请求失败,请稍后重试') } } catch (error) { console.error(`更新${getFeatureName(feature)}失败:`, error) diff --git a/Cunkebao/lib/api.ts b/Cunkebao/lib/api.ts index a11e1a7b..71932b0b 100644 --- a/Cunkebao/lib/api.ts +++ b/Cunkebao/lib/api.ts @@ -51,13 +51,79 @@ export const request = async ( try { const response = await fetch(`${API_BASE_URL}${url}`, options); - const result = await response.json(); + + // 检查网络响应状态 + if (!response.ok) { + // 只有当响应状态码为401时才特殊处理为认证错误 + if (response.status === 401) { + if (typeof window !== 'undefined') { + // 直接调用handleTokenExpired而不是handleApiError,以处理401错误 + // 因为有响应体的情况下,我们应该让handleApiResponse处理401 + // 在这里只处理没有响应体的401网络错误 + const errorMessage = `Unauthorized: ${response.statusText}`; + console.error('授权错误:', errorMessage); + + // 尝试解析响应,如果无法解析才直接处理token过期 + try { + await response.json(); + // 如果能够解析,让后续代码处理 + } catch (e) { + // 如果无法解析,说明是纯网络层401错误,直接处理token过期 + if (typeof window !== 'undefined') { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + window.location.href = '/login'; + throw new Error(errorMessage); + } + } + } + } + // 其他HTTP错误正常返回,让上层组件自行处理 + } + + let result; + try { + result = await response.json(); + } catch (parseError) { + // 处理JSON解析错误 + console.error('无法解析响应JSON:', parseError); + throw new Error('服务器响应格式错误'); + } // 使用响应拦截器处理响应 - return handleApiResponse(response, result); + if (result && result.code === 401) { + if (typeof window !== 'undefined') { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + // 使用客户端导航而不是直接修改window.location + setTimeout(() => { + window.location.href = '/login'; + }, 0); + } + throw new Error(result.msg || '登录已过期,请重新登录'); + } + + // 返回结果而不调用可能会清除token的handleApiResponse + return result; } catch (error) { // 使用错误拦截器处理错误 - return handleApiError(error); + // 只有在确认是401错误时才清除token + if (error instanceof Error && + (error.message.includes('401') || + (error.message.toLowerCase().includes('unauthorized') && + error.message.toLowerCase().includes('token')))) { + if (typeof window !== 'undefined') { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + setTimeout(() => { + window.location.href = '/login'; + }, 0); + } + } + + // 其他错误直接抛出但不调用handleApiError + console.error('API请求错误:', error); + throw error; } }; @@ -103,10 +169,58 @@ export const validateToken = async (): Promise => { } try { - const response = await loginApi.getUserInfo(); - return response.code === 200; + // 直接使用fetch而不是通过api调用,以便捕获具体错误 + const token = getToken(); + if (!token) return false; + + const response = await fetch(`${API_BASE_URL}/v1/auth/info`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + + // 如果状态码是401,明确是认证问题 + if (response.status === 401) { + return false; + } + + // 如果是其他HTTP错误,我们不确定是否是认证问题 + if (!response.ok) { + // 对于非401错误,不要立即判定token无效 + console.warn(`验证token时收到HTTP错误: ${response.status} ${response.statusText}`); + + // 尝试读取响应内容 + try { + const result = await response.json(); + // 只有明确返回code为401才判断为token无效 + if (result && result.code === 401) { + return false; + } + // 其他错误代码视为服务端问题,不影响token有效性 + return true; + } catch (parseError) { + // 无法解析响应,视为网络或服务器问题,不影响token + console.error('无法解析验证token的响应:', parseError); + return true; + } + } + + // 正常情况下,尝试解析响应并检查code + try { + const result = await response.json(); + return result.code === 200; + } catch (parseError) { + // 无法解析响应,视为网络或服务器问题,不影响token + console.error('无法解析验证token的响应:', parseError); + return true; + } } catch (error) { - return false; + // 网络错误或其他异常不应该导致token被视为无效 + console.error('验证token时发生异常:', error); + // 对于网络连接问题,不直接判定为token无效 + return true; } }; diff --git a/Cunkebao/lib/http-interceptors.ts b/Cunkebao/lib/http-interceptors.ts index 439802a7..3965feab 100644 --- a/Cunkebao/lib/http-interceptors.ts +++ b/Cunkebao/lib/http-interceptors.ts @@ -1,4 +1,5 @@ import { useRouter } from "next/navigation"; +import { toast } from "@/components/ui/use-toast"; // Token过期处理 export const handleTokenExpired = () => { @@ -15,34 +16,49 @@ export const handleTokenExpired = () => { } }; -// 响应拦截器 -export const handleApiResponse = (response: Response, result: any): T => { - // 处理token过期情况 - if (result && (result.code === 401 || result.msg?.includes('token'))) { - // 仅在客户端处理token过期 - if (typeof window !== 'undefined') { - handleTokenExpired(); +// 显示API错误,但不会重定向 +export const showApiError = (error: any, defaultMessage: string = '请求失败') => { + if (typeof window === 'undefined') return; // 服务端不处理 + + let errorMessage = defaultMessage; + + // 尝试从各种可能的错误格式中获取消息 + if (error) { + if (typeof error === 'string') { + errorMessage = error; + } else if (error instanceof Error) { + errorMessage = error.message || defaultMessage; + } else if (typeof error === 'object') { + // 尝试从API响应中获取错误消息 + errorMessage = error.msg || error.message || error.error || defaultMessage; } - throw new Error(result.msg || '登录已过期,请重新登录'); } + // 显示错误消息 + if (typeof toast !== 'undefined') { + toast({ + title: "请求错误", + description: errorMessage, + variant: "destructive" + }); + } else { + console.error('API错误:', errorMessage); + } +}; + +// 响应拦截器 - 此函数已不再使用,保留仅用于兼容性 +export const handleApiResponse = (response: Response, result: any): T => { + // 不再处理401响应,而是直接返回结果 + // 401的处理已移至api.ts中直接处理 + console.warn('handleApiResponse已弃用,请直接在api.ts中处理响应'); + return result; }; -// 处理API错误 +// 处理API错误 - 此函数已不再使用,保留仅用于兼容性 export const handleApiError = (error: unknown): never => { console.error('API请求错误:', error); - - if (error instanceof Error) { - // 如果是未授权错误,可能是token过期 - if (error.message.includes('401') || error.message.includes('token') || error.message.includes('授权')) { - // 仅在客户端处理token过期 - if (typeof window !== 'undefined') { - handleTokenExpired(); - } - } - throw error; - } + console.warn('handleApiError已弃用,请直接在api.ts中处理错误'); throw new Error('未知错误,请稍后重试'); }; \ No newline at end of file