// src/contexts/AuthContext.js - Session版本 import React, { createContext, useContext, useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useToast } from '@chakra-ui/react'; import { logger } from '../utils/logger'; import { useNotification } from '../contexts/NotificationContext'; // 创建认证上下文 const AuthContext = createContext(); // 自定义Hook export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }; // 认证提供者组件 export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); // ⚡ 串行执行,阻塞渲染直到 Session 检查完成 const [isAuthenticated, setIsAuthenticated] = useState(false); const navigate = useNavigate(); const toast = useToast(); const { showWelcomeGuide } = useNotification(); // ⚡ 使用 ref 保存最新的 isAuthenticated 值,避免事件监听器重复注册 const isAuthenticatedRef = React.useRef(isAuthenticated); // ⚡ 请求节流:记录上次请求时间,防止短时间内重复请求 const lastCheckTimeRef = React.useRef(0); const MIN_CHECK_INTERVAL = 1000; // 最少间隔1秒 // 检查Session状态 const checkSession = async () => { // 节流检查 const now = Date.now(); const timeSinceLastCheck = now - lastCheckTimeRef.current; if (timeSinceLastCheck < MIN_CHECK_INTERVAL) { logger.warn('AuthContext', 'checkSession 请求被节流(防止频繁请求)', { timeSinceLastCheck: `${timeSinceLastCheck}ms`, minInterval: `${MIN_CHECK_INTERVAL}ms`, reason: '距离上次请求间隔太短' }); return; } lastCheckTimeRef.current = now; try { logger.debug('AuthContext', '开始检查Session状态', { timestamp: new Date().toISOString(), timeSinceLastCheck: timeSinceLastCheck > 0 ? `${timeSinceLastCheck}ms` : '首次请求' }); // 创建超时控制器 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时 const response = await fetch(`/api/auth/session`, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json', }, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error('Session检查失败'); } const data = await response.json(); logger.debug('AuthContext', 'Session数据', { isAuthenticated: data.isAuthenticated, userId: data.user?.id }); if (data.isAuthenticated && data.user) { // ⚡ 只在 user 数据真正变化时才更新状态,避免无限循环 setUser((prevUser) => { // 比较用户 ID,如果相同则不更新 if (prevUser && prevUser.id === data.user.id) { return prevUser; } return data.user; }); setIsAuthenticated((prev) => prev === true ? prev : true); } else { setUser((prev) => prev === null ? prev : null); setIsAuthenticated((prev) => prev === false ? prev : false); } } catch (error) { logger.error('AuthContext', 'checkSession', error); // 网络错误或超时,设置为未登录状态 setUser((prev) => prev === null ? prev : null); setIsAuthenticated((prev) => prev === false ? prev : false); } finally { // ⚡ 只在 isLoading 为 true 时才设置为 false,避免不必要的状态更新 setIsLoading((prev) => prev === false ? prev : false); } }; // ⚡ 初始化时检查Session - 并行执行,不阻塞页面渲染 useEffect(() => { checkSession(); // 直接调用,与页面渲染并行 // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // ⚡ 同步 isAuthenticated 到 ref useEffect(() => { isAuthenticatedRef.current = isAuthenticated; }, [isAuthenticated]); // 监听路由变化,检查session(处理微信登录回调) // ⚡ 移除 isAuthenticated 依赖,使用 ref 避免重复注册事件监听器 useEffect(() => { const handleRouteChange = () => { // 使用 ref 获取最新的认证状态 if (window.location.pathname === '/home' && !isAuthenticatedRef.current) { checkSession(); } }; window.addEventListener('popstate', handleRouteChange); return () => window.removeEventListener('popstate', handleRouteChange); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // ✅ 空依赖数组,只注册一次事件监听器 // 更新本地用户的便捷方法 const updateUser = (partial) => { setUser((prev) => ({ ...(prev || {}), ...partial })); }; // 传统登录方法 const login = async (credential, password, loginType = 'email') => { try { setIsLoading(true); logger.debug('AuthContext', '开始登录流程', { credential: credential.substring(0, 3) + '***', loginType }); const formData = new URLSearchParams(); formData.append('password', password); if (loginType === 'username') { formData.append('username', credential); } else if (loginType === 'email') { formData.append('email', credential); } else if (loginType === 'phone') { formData.append('username', credential); } logger.api.request('POST', '/api/auth/login', { credential: credential.substring(0, 3) + '***', loginType }); const response = await fetch(`/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, credentials: 'include', body: formData }); // 获取响应文本,然后尝试解析JSON const responseText = await response.text(); let data; try { data = JSON.parse(responseText); logger.api.response('POST', '/api/auth/login', response.status, data); } catch (parseError) { logger.error('AuthContext', 'login', parseError, { responseText: responseText.substring(0, 100) }); throw new Error(`服务器响应格式错误: ${responseText.substring(0, 100)}...`); } if (!response.ok || !data.success) { throw new Error(data.error || '登录失败'); } // 更新状态 setUser(data.user); setIsAuthenticated(true); // ⚡ 移除toast,让调用者处理UI反馈,避免并发更新冲突 // toast({ // title: "登录成功", // description: "欢迎回来!", // status: "success", // duration: 3000, // isClosable: true, // }); // ⚡ 登录成功后显示欢迎引导(延迟2秒,避免与登录Toast冲突) setTimeout(() => { showWelcomeGuide(); }, 2000); return { success: true }; } catch (error) { logger.error('AuthContext', 'login', error, { loginType }); return { success: false, error: error.message }; } finally { setIsLoading(false); } }; // 手机号注册 const registerWithPhone = async (phone, code, username, password) => { try { setIsLoading(true); const response = await fetch(`/api/auth/register/phone`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ phone, code, username, password }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || '注册失败'); } // 注册成功后自动登录 setUser(data.user); setIsAuthenticated(true); toast({ title: "注册成功", description: "欢迎加入价值前沿!", status: "success", duration: 3000, isClosable: true, }); // ⚡ 注册成功后显示欢迎引导(延迟2秒) setTimeout(() => { showWelcomeGuide(); }, 2000); return { success: true }; } catch (error) { logger.error('AuthContext', 'registerWithPhone', error, { phone: phone.substring(0, 3) + '****' }); return { success: false, error: error.message }; } finally { setIsLoading(false); } }; // 邮箱注册 const registerWithEmail = async (email, code, username, password) => { try { setIsLoading(true); const response = await fetch(`/api/auth/register/email`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ email, code, username, password }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || '注册失败'); } // 注册成功后自动登录 setUser(data.user); setIsAuthenticated(true); toast({ title: "注册成功", description: "欢迎加入价值前沿!", status: "success", duration: 3000, isClosable: true, }); // ⚡ 注册成功后显示欢迎引导(延迟2秒) setTimeout(() => { showWelcomeGuide(); }, 2000); return { success: true }; } catch (error) { logger.error('AuthContext', 'registerWithEmail', error); return { success: false, error: error.message }; } finally { setIsLoading(false); } }; // 发送手机验证码 const sendSmsCode = async (phone) => { try { const response = await fetch(`/api/auth/send-sms-code`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ phone }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '发送失败'); } // ❌ 移除成功 toast logger.info('AuthContext', '验证码已发送', { phone: phone.substring(0, 3) + '****' }); return { success: true }; } catch (error) { // ❌ 移除错误 toast logger.error('AuthContext', 'sendSmsCode', error, { phone: phone.substring(0, 3) + '****' }); return { success: false, error: error.message }; } }; // 发送邮箱验证码 const sendEmailCode = async (email) => { try { const response = await fetch(`/api/auth/send-email-code`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ email }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '发送失败'); } // ❌ 移除成功 toast logger.info('AuthContext', '邮箱验证码已发送', { email: email.substring(0, 3) + '***@***' }); return { success: true }; } catch (error) { // ❌ 移除错误 toast logger.error('AuthContext', 'sendEmailCode', error); return { success: false, error: error.message }; } }; // 登出方法 const logout = async () => { try { // 调用后端登出API await fetch(`/api/auth/logout`, { method: 'POST', credentials: 'include' }); // 清除本地状态 setUser(null); setIsAuthenticated(false); // ✅ 保留登出成功 toast(关键操作提示) toast({ title: "已登出", description: "您已成功退出登录", status: "info", duration: 2000, isClosable: true, }); } catch (error) { logger.error('AuthContext', 'logout', error); // 即使API调用失败也清除本地状态 setUser(null); setIsAuthenticated(false); } }; // 检查用户是否有特定权限 const hasRole = (role) => { return user && user.role === role; }; // 刷新session(可选) const refreshSession = async () => { await checkSession(); }; // 提供给子组件的值 const value = { user, isAuthenticated, isLoading, updateUser, login, registerWithPhone, registerWithEmail, sendSmsCode, sendEmailCode, logout, hasRole, refreshSession, checkSession }; return ( {children} ); };