=== 现象 === 打开 /login 页面一直加载、闪屏,无法登录。 线上访问日志:10 分钟内 /api/v1/system/license 被请求 **1566 次,全部 401**(约 2.6 次/秒)。 === 根因(三处叠加)=== 1) LicenseProvider 挂在 App 根部,包住**所有**路由(含 /login), 挂载即请求鉴权接口 /system/license;登录页没有 token → 401。 2) http.ts 的 redirectToLogin() 在已处于 /login 时仍然执行 window.location.href = '/login' —— **给 href 赋同值也会整页重载**。 3) 重载 → App 重新挂载 → LicenseProvider 再请求 → 再 401 → 再重载。 形成"请求→401→整页重载→请求"的死循环,用户看到的就是转圈+闪屏。 === 修复(三处,防御性叠加)=== 1) useLicense:没有 accessToken 时**不请求**;有 token 才拉。 并用 1 秒一次的低成本探测(只读 localStorage,不发请求)在登录成功 (token 出现)后补拉一次,避免登录后授权状态缺失。 2) licenseApi.status 使用 silent: true —— 401 不触发"清凭证+跳登录"的全局副作用。 3) redirectToLogin:已在 /login 时直接 return,不再重载页面。 === 验证 === tsc 通过;发布后 /login 不再产生 401 轮询(见发布后回归)
73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||
import { licenseApi, type LicenseInfo } from '@/api/license'
|
||
import { getAccessToken } from '@/lib/auth-storage'
|
||
|
||
interface LicenseState {
|
||
pro: boolean
|
||
info: LicenseInfo | null
|
||
reload: () => void
|
||
}
|
||
|
||
const LicenseContext = createContext<LicenseState>({ pro: false, info: null, reload: () => {} })
|
||
|
||
/**
|
||
* 授权状态 Provider。
|
||
*
|
||
* ⚠️ 必须只在**已登录**时请求:`/system/license` 是鉴权接口,
|
||
* 未登录访问会 401,而 401 会触发全局"跳登录"逻辑。
|
||
* 之前没有这道判断,登录页一挂载就请求 → 401 → 跳 /login(整页重载)
|
||
* → 重新挂载 → 再 401,形成死循环:登录页一直转圈+闪屏
|
||
* (线上实测 10 分钟 1566 次 401)。三处修复:
|
||
* 1) 这里没有 token 就不请求
|
||
* 2) 请求带 silent,401 不触发全局跳转副作用
|
||
* 3) http.ts 的 redirectToLogin 在已处于 /login 时不再重载
|
||
*
|
||
* 登录成功后 token 才出现,所以用一个轻量的定时探测(只读 localStorage,
|
||
* 不发请求)在 token 出现时补拉一次。
|
||
*/
|
||
export function LicenseProvider({ children }: { children: React.ReactNode }) {
|
||
const [state, setState] = useState<{ pro: boolean; info: LicenseInfo | null }>({ pro: false, info: null })
|
||
const loadedRef = useRef(false)
|
||
|
||
const load = useCallback(() => {
|
||
// 未登录(登录页)直接跳过,避免 401 → 跳登录 → 重载 的死循环
|
||
if (!getAccessToken()) return
|
||
licenseApi
|
||
.status()
|
||
.then(res => {
|
||
loadedRef.current = true
|
||
setState({ pro: Boolean(res.pro), info: res.license })
|
||
})
|
||
.catch(() => {
|
||
// 拿不到授权信息时按免费版处理,不打断页面
|
||
setState({ pro: false, info: null })
|
||
})
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
load()
|
||
// 登录后 token 才写入 localStorage,用 1 秒一次的低成本探测补拉(只读本地存储)
|
||
const timer = window.setInterval(() => {
|
||
if (loadedRef.current) {
|
||
window.clearInterval(timer)
|
||
return
|
||
}
|
||
if (getAccessToken()) {
|
||
window.clearInterval(timer)
|
||
load()
|
||
}
|
||
}, 1000)
|
||
return () => window.clearInterval(timer)
|
||
}, [load])
|
||
|
||
return (
|
||
<LicenseContext.Provider value={{ ...state, reload: load }}>
|
||
{children}
|
||
</LicenseContext.Provider>
|
||
)
|
||
}
|
||
|
||
export function useLicense() {
|
||
return useContext(LicenseContext)
|
||
}
|