diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js
index 9641ec04..f9f22d5a 100644
--- a/src/components/Navbars/HomeNavbar.js
+++ b/src/components/Navbars/HomeNavbar.js
@@ -57,6 +57,9 @@ import BrandLogo from './components/BrandLogo';
import LoginButton from './components/LoginButton';
import CalendarButton from './components/CalendarButton';
+// Phase 2 优化: 使用 Redux 管理订阅数据
+import { useSubscription } from '../../hooks/useSubscription';
+
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
const SecondaryNav = ({ showCompletenessAlert }) => {
const navigate = useNavigate();
@@ -574,14 +577,13 @@ export default function HomeNavbar() {
// 添加标志位:追踪是否已经检查过资料完整性(避免重复请求)
const hasCheckedCompleteness = React.useRef(false);
- // 订阅信息状态
- const [subscriptionInfo, setSubscriptionInfo] = React.useState({
- type: 'free',
- status: 'active',
- days_left: 0,
- is_active: true
- });
- const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = React.useState(false);
+ // Phase 2: 使用 Redux 订阅数据
+ const {
+ subscriptionInfo,
+ isSubscriptionModalOpen,
+ openSubscriptionModal,
+ closeSubscriptionModal
+ } = useSubscription();
const loadWatchlistQuotes = useCallback(async () => {
try {
@@ -790,49 +792,7 @@ export default function HomeNavbar() {
}
}, [isAuthenticated, userId, checkProfileCompleteness, user]); // ⚡ 使用 userId
- // 加载订阅信息
- React.useEffect(() => {
- // ✅ 移除 ref 检查,直接根据登录状态加载
- if (isAuthenticated && user) {
- const loadSubscriptionInfo = async () => {
- try {
- const base = getApiBase();
- logger.debug('HomeNavbar', '开始加载订阅信息', { user_id: user?.id });
- const response = await fetch(base + '/api/subscription/current', {
- credentials: 'include',
- });
- if (response.ok) {
- const data = await response.json();
- logger.debug('HomeNavbar', 'API 返回订阅数据', data);
- if (data.success && data.data) {
- // 数据标准化处理:确保type字段是小写的 'free', 'pro', 或 'max'
- const normalizedData = {
- type: (data.data.type || data.data.subscription_type || 'free').toLowerCase(),
- status: data.data.status || 'active',
- days_left: data.data.days_left || 0,
- is_active: data.data.is_active !== false,
- end_date: data.data.end_date || null
- };
- logger.info('HomeNavbar', '订阅信息已更新', normalizedData);
- setSubscriptionInfo(normalizedData);
- }
- }
- } catch (error) {
- logger.error('HomeNavbar', '加载订阅信息失败', error);
- }
- };
- loadSubscriptionInfo();
- } else {
- // 用户未登录时,重置为免费版
- logger.debug('HomeNavbar', '用户未登录,重置订阅信息为免费版');
- setSubscriptionInfo({
- type: 'free',
- status: 'active',
- days_left: 0,
- is_active: true
- });
- }
- }, [isAuthenticated, userId, user]); // ✅ React 会自动去重,不会造成无限循环
+ // Phase 2: 加载订阅信息逻辑已移至 useSubscriptionData Hook
return (
<>
@@ -1127,7 +1087,7 @@ export default function HomeNavbar() {
setIsSubscriptionModalOpen(true)}
+ onClick={openSubscriptionModal}
>
setIsSubscriptionModalOpen(false)}
+ onClose={closeSubscriptionModal}
subscriptionInfo={subscriptionInfo}
/>
)}
@@ -1193,7 +1153,7 @@ export default function HomeNavbar() {
{/* 订阅管理 */}
- } onClick={() => setIsSubscriptionModalOpen(true)}>
+ } onClick={openSubscriptionModal}>
订阅管理
@@ -1206,7 +1166,7 @@ export default function HomeNavbar() {
{isSubscriptionModalOpen && (
setIsSubscriptionModalOpen(false)}
+ onClose={closeSubscriptionModal}
subscriptionInfo={subscriptionInfo}
/>
)}
diff --git a/src/hooks/useSubscription.js b/src/hooks/useSubscription.js
index 328578f7..e7a86dc1 100644
--- a/src/hooks/useSubscription.js
+++ b/src/hooks/useSubscription.js
@@ -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
- };
-};
\ No newline at end of file
+ // Modal 控制
+ isSubscriptionModalOpen,
+ openSubscriptionModal: () => dispatch(openModal()),
+ closeSubscriptionModal: () => dispatch(closeModal()),
+
+ // 权限检查方法
+ hasFeatureAccess,
+ hasSubscriptionLevel,
+ getRequiredLevel,
+ getSubscriptionStatusText,
+ getUpgradeRecommendation,
+
+ // 手动刷新
+ refreshSubscription: () => dispatch(fetchSubscriptionInfo())
+ };
+};
diff --git a/src/store/index.js b/src/store/index.js
index 9adafda6..1b08c189 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -5,6 +5,7 @@ import posthogReducer from './slices/posthogSlice';
import industryReducer from './slices/industrySlice';
import stockReducer from './slices/stockSlice';
import authModalReducer from './slices/authModalSlice';
+import subscriptionReducer from './slices/subscriptionSlice';
import posthogMiddleware from './middleware/posthogMiddleware';
export const store = configureStore({
@@ -14,6 +15,7 @@ export const store = configureStore({
industry: industryReducer, // ✅ 行业分类数据管理
stock: stockReducer, // ✅ 股票和事件数据管理
authModal: authModalReducer, // ✅ 认证弹窗状态管理
+ subscription: subscriptionReducer, // ✅ 订阅信息状态管理
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
diff --git a/src/store/slices/subscriptionSlice.js b/src/store/slices/subscriptionSlice.js
new file mode 100644
index 00000000..3f2ad364
--- /dev/null
+++ b/src/store/slices/subscriptionSlice.js
@@ -0,0 +1,143 @@
+// src/store/slices/subscriptionSlice.js
+// 订阅信息状态管理 Redux Slice
+
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import { logger } from '../../utils/logger';
+import { getApiBase } from '../../utils/apiConfig';
+
+/**
+ * 异步 Thunk: 获取用户订阅信息
+ */
+export const fetchSubscriptionInfo = createAsyncThunk(
+ 'subscription/fetchInfo',
+ async (_, { rejectWithValue }) => {
+ try {
+ const base = getApiBase();
+ logger.debug('subscriptionSlice', '开始加载订阅信息');
+
+ const response = await fetch(base + '/api/subscription/current', {
+ credentials: 'include',
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ if (data.success && data.data) {
+ // 数据标准化处理
+ const normalizedData = {
+ type: (data.data.type || data.data.subscription_type || 'free').toLowerCase(),
+ status: data.data.status || 'active',
+ days_left: data.data.days_left || 0,
+ is_active: data.data.is_active !== false,
+ end_date: data.data.end_date || null
+ };
+
+ logger.info('subscriptionSlice', '订阅信息加载成功', normalizedData);
+ return normalizedData;
+ } else {
+ // API 返回成功但无数据,返回默认免费版
+ return {
+ type: 'free',
+ status: 'active',
+ days_left: 0,
+ is_active: false,
+ end_date: null
+ };
+ }
+ } catch (error) {
+ logger.error('subscriptionSlice', '加载订阅信息失败', error);
+ return rejectWithValue(error.message);
+ }
+ }
+);
+
+/**
+ * Subscription Slice
+ * 管理用户订阅信息和订阅 Modal 状态
+ */
+const subscriptionSlice = createSlice({
+ name: 'subscription',
+ initialState: {
+ // 订阅信息
+ info: {
+ type: 'free',
+ status: 'active',
+ days_left: 0,
+ is_active: false,
+ end_date: null
+ },
+ // 加载状态
+ loading: false,
+ error: null,
+ // 订阅 Modal 状态
+ isModalOpen: false,
+ },
+ reducers: {
+ /**
+ * 打开订阅 Modal
+ */
+ openModal: (state) => {
+ state.isModalOpen = true;
+ logger.debug('subscriptionSlice', '打开订阅 Modal');
+ },
+
+ /**
+ * 关闭订阅 Modal
+ */
+ closeModal: (state) => {
+ state.isModalOpen = false;
+ logger.debug('subscriptionSlice', '关闭订阅 Modal');
+ },
+
+ /**
+ * 重置为免费版 (用户登出时调用)
+ */
+ resetToFree: (state) => {
+ state.info = {
+ type: 'free',
+ status: 'active',
+ days_left: 0,
+ is_active: false,
+ end_date: null
+ };
+ state.loading = false;
+ state.error = null;
+ logger.debug('subscriptionSlice', '重置订阅信息为免费版');
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ // fetchSubscriptionInfo - pending
+ .addCase(fetchSubscriptionInfo.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ // fetchSubscriptionInfo - fulfilled
+ .addCase(fetchSubscriptionInfo.fulfilled, (state, action) => {
+ state.loading = false;
+ state.info = action.payload;
+ state.error = null;
+ })
+ // fetchSubscriptionInfo - rejected
+ .addCase(fetchSubscriptionInfo.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload || 'Unknown error';
+ // 加载失败时保持当前状态,不重置为免费版
+ });
+ },
+});
+
+// 导出 actions
+export const { openModal, closeModal, resetToFree } = subscriptionSlice.actions;
+
+// 导出 selectors
+export const selectSubscriptionInfo = (state) => state.subscription.info;
+export const selectSubscriptionLoading = (state) => state.subscription.loading;
+export const selectSubscriptionError = (state) => state.subscription.error;
+export const selectSubscriptionModalOpen = (state) => state.subscription.isModalOpen;
+
+// 导出 reducer
+export default subscriptionSlice.reducer;