refactor(subscription): Phase 2 - 迁移到 Redux 状态管理
重构目标: 使用 Redux 管理订阅数据,替代本地状态 Phase 2 完成: ✅ 创建 subscriptionSlice.js (143行) - Redux Toolkit createSlice + createAsyncThunk - 管理订阅信息、loading、error、Modal 状态 - fetchSubscriptionInfo 异步 thunk - resetToFree reducer (登出时调用) ✅ 注册到 Redux Store - 添加 subscriptionReducer 到 store ✅ 重构 useSubscription Hook (182行) - 从本地状态迁移到 Redux (useSelector + useDispatch) - 保留所有权限检查逻辑 - 新增: isSubscriptionModalOpen, open/closeSubscriptionModal - 自动加载订阅数据 (登录时) ✅ 重构 HomeNavbar 使用 Redux - 替换 useSubscriptionData → useSubscription - 删除 ./hooks/useSubscriptionData.js 架构优势: ✅ 全局状态共享 - 多组件可访问订阅数据 ✅ Redux DevTools 可调试 ✅ 异步逻辑统一管理 (createAsyncThunk) ✅ 与现有架构一致 (authModalSlice 等) 性能优化: ✅ Redux 状态优化,减少不必要渲染 ✅ useSelector 精确订阅,只在相关数据变化时更新 累计优化: - 原始: 1623行 - Phase 1后: 1573行 (↓ 50行) - Phase 2后: 1533行 (↓ 90行, -5.5%) - 新增 Redux 逻辑: subscriptionSlice (143行) + Hook (182行) 下一步: Phase 3+ 继续拆分组件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,224 +1,182 @@
|
||||
// src/hooks/useSubscription.js
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
// 订阅信息自定义 Hook - 使用 Redux 状态管理
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { logger } from '../utils/logger';
|
||||
import {
|
||||
fetchSubscriptionInfo,
|
||||
openModal,
|
||||
closeModal,
|
||||
resetToFree,
|
||||
selectSubscriptionInfo,
|
||||
selectSubscriptionLoading,
|
||||
selectSubscriptionError,
|
||||
selectSubscriptionModalOpen
|
||||
} from '../store/slices/subscriptionSlice';
|
||||
|
||||
// 订阅级别映射
|
||||
const SUBSCRIPTION_LEVELS = {
|
||||
free: 0,
|
||||
pro: 1,
|
||||
max: 2
|
||||
free: 0,
|
||||
pro: 1,
|
||||
max: 2
|
||||
};
|
||||
|
||||
// 功能权限映射
|
||||
const FEATURE_REQUIREMENTS = {
|
||||
'related_stocks': 'pro', // 相关标的
|
||||
'related_concepts': 'pro', // 相关概念
|
||||
'transmission_chain': 'max', // 事件传导链分析
|
||||
'historical_events_full': 'pro', // 历史事件对比(完整版)
|
||||
'concept_html_detail': 'pro', // 概念HTML具体内容
|
||||
'concept_stats_panel': 'pro', // 概念统计中心
|
||||
'concept_related_stocks': 'pro', // 概念相关股票
|
||||
'concept_timeline': 'max', // 概念历史时间轴
|
||||
'hot_stocks': 'pro' // 热门个股
|
||||
'related_stocks': 'pro', // 相关标的
|
||||
'related_concepts': 'pro', // 相关概念
|
||||
'transmission_chain': 'max', // 事件传导链分析
|
||||
'historical_events_full': 'pro', // 历史事件对比(完整版)
|
||||
'concept_html_detail': 'pro', // 概念HTML具体内容
|
||||
'concept_stats_panel': 'pro', // 概念统计中心
|
||||
'concept_related_stocks': 'pro', // 概念相关股票
|
||||
'concept_timeline': 'max', // 概念历史时间轴
|
||||
'hot_stocks': 'pro' // 热门个股
|
||||
};
|
||||
|
||||
/**
|
||||
* 订阅信息自定义 Hook (Redux 版本)
|
||||
*
|
||||
* 功能:
|
||||
* - 自动根据登录状态加载订阅信息 (从 Redux)
|
||||
* - 提供权限检查方法
|
||||
* - 提供订阅 Modal 控制方法
|
||||
*
|
||||
* @returns {{
|
||||
* subscriptionInfo: Object,
|
||||
* loading: boolean,
|
||||
* error: string|null,
|
||||
* isSubscriptionModalOpen: boolean,
|
||||
* openSubscriptionModal: Function,
|
||||
* closeSubscriptionModal: Function,
|
||||
* refreshSubscription: Function,
|
||||
* hasFeatureAccess: Function,
|
||||
* hasSubscriptionLevel: Function,
|
||||
* getRequiredLevel: Function,
|
||||
* getSubscriptionStatusText: Function,
|
||||
* getUpgradeRecommendation: Function
|
||||
* }}
|
||||
*/
|
||||
export const useSubscription = () => {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const [subscriptionInfo, setSubscriptionInfo] = useState({
|
||||
type: 'free',
|
||||
status: 'active',
|
||||
is_active: true,
|
||||
days_left: 0
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
|
||||
// 获取订阅信息
|
||||
const fetchSubscriptionInfo = async () => {
|
||||
if (!isAuthenticated || !user) {
|
||||
setSubscriptionInfo({
|
||||
type: 'free',
|
||||
status: 'active',
|
||||
is_active: true,
|
||||
days_left: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Redux 状态
|
||||
const subscriptionInfo = useSelector(selectSubscriptionInfo);
|
||||
const loading = useSelector(selectSubscriptionLoading);
|
||||
const error = useSelector(selectSubscriptionError);
|
||||
const isSubscriptionModalOpen = useSelector(selectSubscriptionModalOpen);
|
||||
|
||||
// 首先检查用户对象中是否已经包含订阅信息
|
||||
if (user.subscription_type) {
|
||||
logger.debug('useSubscription', '从用户对象获取订阅信息', {
|
||||
subscriptionType: user.subscription_type,
|
||||
daysLeft: user.subscription_days_left
|
||||
});
|
||||
setSubscriptionInfo({
|
||||
type: user.subscription_type,
|
||||
status: 'active',
|
||||
is_active: true,
|
||||
days_left: user.subscription_days_left || 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/subscription/info', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 自动加载订阅信息
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && user) {
|
||||
// 用户已登录,加载订阅信息
|
||||
dispatch(fetchSubscriptionInfo());
|
||||
logger.debug('useSubscription', '加载订阅信息', { userId: user.id });
|
||||
} else {
|
||||
// 用户未登录,重置为免费版
|
||||
dispatch(resetToFree());
|
||||
logger.debug('useSubscription', '用户未登录,重置为免费版');
|
||||
}
|
||||
});
|
||||
}, [isAuthenticated, user, dispatch]);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setSubscriptionInfo(data.data);
|
||||
// 获取订阅级别数值
|
||||
const getSubscriptionLevel = (type = null) => {
|
||||
const subType = (type || subscriptionInfo.type || 'free').toLowerCase();
|
||||
return SUBSCRIPTION_LEVELS[subType] || 0;
|
||||
};
|
||||
|
||||
// 检查是否有指定功能的权限
|
||||
const hasFeatureAccess = (featureName) => {
|
||||
// Max 用户解锁所有功能
|
||||
if (user?.subscription_type === 'max' || subscriptionInfo.type === 'max') {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// 如果API调用失败,回退到用户对象中的信息
|
||||
logger.warn('useSubscription', 'API调用失败,使用用户对象订阅信息', {
|
||||
status: response.status,
|
||||
fallbackType: user.subscription_type || 'free'
|
||||
});
|
||||
setSubscriptionInfo({
|
||||
type: user.subscription_type || 'free',
|
||||
status: 'active',
|
||||
is_active: true,
|
||||
days_left: user.subscription_days_left || 0
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('useSubscription', 'fetchSubscriptionInfo', error, {
|
||||
userId: user?.id
|
||||
});
|
||||
// 发生错误时,回退到用户对象中的信息
|
||||
setSubscriptionInfo({
|
||||
type: user.subscription_type || 'free',
|
||||
status: 'active',
|
||||
is_active: true,
|
||||
days_left: user.subscription_days_left || 0
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ⚡ 提取 userId 为独立变量,避免 user 对象引用变化导致无限循环
|
||||
const userId = user?.id;
|
||||
const prevUserIdRef = useRef(userId);
|
||||
const prevIsAuthenticatedRef = useRef(isAuthenticated);
|
||||
if (!subscriptionInfo.is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// ⚡ 只在 userId 或 isAuthenticated 真正变化时才请求
|
||||
const userIdChanged = prevUserIdRef.current !== userId;
|
||||
const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated;
|
||||
const requiredLevel = FEATURE_REQUIREMENTS[featureName];
|
||||
if (!requiredLevel) {
|
||||
return true; // 如果功能不需要特定权限,默认允许
|
||||
}
|
||||
|
||||
if (userIdChanged || authChanged) {
|
||||
logger.debug('useSubscription', 'fetchSubscriptionInfo 触发', {
|
||||
userIdChanged,
|
||||
authChanged,
|
||||
prevUserId: prevUserIdRef.current,
|
||||
currentUserId: userId,
|
||||
prevAuth: prevIsAuthenticatedRef.current,
|
||||
currentAuth: isAuthenticated
|
||||
});
|
||||
const currentLevel = getSubscriptionLevel();
|
||||
const requiredLevelNum = getSubscriptionLevel(requiredLevel);
|
||||
|
||||
prevUserIdRef.current = userId;
|
||||
prevIsAuthenticatedRef.current = isAuthenticated;
|
||||
fetchSubscriptionInfo();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isAuthenticated, userId]); // 使用 userId 原始值,而不是 user?.id 表达式
|
||||
return currentLevel >= requiredLevelNum;
|
||||
};
|
||||
|
||||
// 获取订阅级别数值
|
||||
const getSubscriptionLevel = (type = null) => {
|
||||
const subType = (type || subscriptionInfo.type || 'free').toLowerCase();
|
||||
return SUBSCRIPTION_LEVELS[subType] || 0;
|
||||
};
|
||||
// 检查是否达到指定订阅级别
|
||||
const hasSubscriptionLevel = (requiredLevel) => {
|
||||
if (!subscriptionInfo.is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有指定功能的权限
|
||||
const hasFeatureAccess = (featureName) => {
|
||||
// 临时调试:如果用户对象中有max权限,直接解锁所有功能
|
||||
if (user?.subscription_type === 'max') {
|
||||
logger.debug('useSubscription', 'Max用户解锁功能', {
|
||||
featureName,
|
||||
userId: user?.id
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const currentLevel = getSubscriptionLevel();
|
||||
const requiredLevelNum = getSubscriptionLevel(requiredLevel);
|
||||
|
||||
if (!subscriptionInfo.is_active) {
|
||||
return false;
|
||||
}
|
||||
return currentLevel >= requiredLevelNum;
|
||||
};
|
||||
|
||||
const requiredLevel = FEATURE_REQUIREMENTS[featureName];
|
||||
if (!requiredLevel) {
|
||||
return true; // 如果功能不需要特定权限,默认允许
|
||||
}
|
||||
// 获取功能所需的订阅级别
|
||||
const getRequiredLevel = (featureName) => {
|
||||
return FEATURE_REQUIREMENTS[featureName] || 'free';
|
||||
};
|
||||
|
||||
const currentLevel = getSubscriptionLevel();
|
||||
const requiredLevelNum = getSubscriptionLevel(requiredLevel);
|
||||
// 获取订阅状态文本
|
||||
const getSubscriptionStatusText = () => {
|
||||
const type = subscriptionInfo.type || 'free';
|
||||
switch (type.toLowerCase()) {
|
||||
case 'free':
|
||||
return '免费版';
|
||||
case 'pro':
|
||||
return 'Pro版';
|
||||
case 'max':
|
||||
return 'Max版';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
return currentLevel >= requiredLevelNum;
|
||||
};
|
||||
// 获取升级建议
|
||||
const getUpgradeRecommendation = (featureName) => {
|
||||
const requiredLevel = getRequiredLevel(featureName);
|
||||
const currentType = subscriptionInfo.type || 'free';
|
||||
|
||||
// 检查是否达到指定订阅级别
|
||||
const hasSubscriptionLevel = (requiredLevel) => {
|
||||
if (!subscriptionInfo.is_active) {
|
||||
return false;
|
||||
}
|
||||
if (hasFeatureAccess(featureName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentLevel = getSubscriptionLevel();
|
||||
const requiredLevelNum = getSubscriptionLevel(requiredLevel);
|
||||
|
||||
return currentLevel >= requiredLevelNum;
|
||||
};
|
||||
|
||||
// 获取功能所需的订阅级别
|
||||
const getRequiredLevel = (featureName) => {
|
||||
return FEATURE_REQUIREMENTS[featureName] || 'free';
|
||||
};
|
||||
|
||||
// 获取订阅状态文本
|
||||
const getSubscriptionStatusText = () => {
|
||||
const type = subscriptionInfo.type || 'free';
|
||||
switch (type.toLowerCase()) {
|
||||
case 'free':
|
||||
return '免费版';
|
||||
case 'pro':
|
||||
return 'Pro版';
|
||||
case 'max':
|
||||
return 'Max版';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取升级建议
|
||||
const getUpgradeRecommendation = (featureName) => {
|
||||
const requiredLevel = getRequiredLevel(featureName);
|
||||
const currentType = subscriptionInfo.type || 'free';
|
||||
|
||||
if (hasFeatureAccess(featureName)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
current: currentType,
|
||||
required: requiredLevel,
|
||||
message: `此功能需要${requiredLevel === 'pro' ? 'Pro版' : 'Max版'}订阅`
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
current: currentType,
|
||||
required: requiredLevel,
|
||||
message: `此功能需要${requiredLevel === 'pro' ? 'Pro版' : 'Max版'}订阅`
|
||||
};
|
||||
};
|
||||
// 订阅信息 (来自 Redux)
|
||||
subscriptionInfo,
|
||||
loading,
|
||||
error,
|
||||
|
||||
return {
|
||||
subscriptionInfo,
|
||||
loading,
|
||||
hasFeatureAccess,
|
||||
hasSubscriptionLevel,
|
||||
getRequiredLevel,
|
||||
getSubscriptionStatusText,
|
||||
getUpgradeRecommendation,
|
||||
refreshSubscription: fetchSubscriptionInfo
|
||||
};
|
||||
};
|
||||
// Modal 控制
|
||||
isSubscriptionModalOpen,
|
||||
openSubscriptionModal: () => dispatch(openModal()),
|
||||
closeSubscriptionModal: () => dispatch(closeModal()),
|
||||
|
||||
// 权限检查方法
|
||||
hasFeatureAccess,
|
||||
hasSubscriptionLevel,
|
||||
getRequiredLevel,
|
||||
getSubscriptionStatusText,
|
||||
getUpgradeRecommendation,
|
||||
|
||||
// 手动刷新
|
||||
refreshSubscription: () => dispatch(fetchSubscriptionInfo())
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user