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:
zdl
2025-10-30 16:50:10 +08:00
parent 5387b2d032
commit e5205ce097
4 changed files with 309 additions and 246 deletions

View File

@@ -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())
};
};