fix: 会员过期时跳过 API 请求 & 限制 STOMP WebSocket 重连次数
- DynamicNewsDetailPanel: 添加会员过期判断,过期时显示续费提示 - RelatedConceptsSection: 会员过期时跳过概念 API 请求 - TransmissionChainAnalysis: 会员过期时跳过传导链 API 请求 - BytedeskWidget: 限制 STOMP WebSocket 最多重连 3 次,屏蔽相关日志
This commit is contained in:
@@ -19,6 +19,10 @@ import PropTypes from 'prop-types';
|
|||||||
let widgetInitialized = false;
|
let widgetInitialized = false;
|
||||||
let idleCallbackId = null;
|
let idleCallbackId = null;
|
||||||
|
|
||||||
|
// ⚡ STOMP WebSocket 重连限制(最多重试 3 次)
|
||||||
|
let stompRetryCount = 0;
|
||||||
|
const MAX_STOMP_RETRIES = 3;
|
||||||
|
|
||||||
const BytedeskWidget = ({
|
const BytedeskWidget = ({
|
||||||
config,
|
config,
|
||||||
autoLoad = true,
|
autoLoad = true,
|
||||||
@@ -118,18 +122,79 @@ const BytedeskWidget = ({
|
|||||||
// 5 秒后停止监听(避免性能问题)
|
// 5 秒后停止监听(避免性能问题)
|
||||||
setTimeout(() => observer.disconnect(), 5000);
|
setTimeout(() => observer.disconnect(), 5000);
|
||||||
|
|
||||||
// ⚡ 屏蔽 STOMP WebSocket 错误日志(不影响功能)
|
// ⚡ 屏蔽 STOMP WebSocket 相关日志(控制台降噪)
|
||||||
|
// STOMP 连接失败是因为后端服务配置问题,不影响客服功能(使用 HTTP 轮询降级)
|
||||||
|
const isStompLog = (args) => {
|
||||||
|
const msg = args.map(a => String(a)).join(' ');
|
||||||
|
// 只屏蔽包含 /stomp 路径的日志,避免误屏蔽其他 WebSocket 日志
|
||||||
|
return msg.includes('/stomp') || msg.includes('stomp onWebSocketError');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 屏蔽 console.error
|
||||||
const originalConsoleError = console.error;
|
const originalConsoleError = console.error;
|
||||||
console.error = function(...args) {
|
console.error = function(...args) {
|
||||||
const errorMsg = args.join(' ');
|
if (isStompLog(args)) return;
|
||||||
if (errorMsg.includes('/stomp') ||
|
|
||||||
errorMsg.includes('stomp onWebSocketError') ||
|
|
||||||
(errorMsg.includes('WebSocket connection to') && errorMsg.includes('/stomp'))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
originalConsoleError.apply(console, args);
|
originalConsoleError.apply(console, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 屏蔽 console.warn
|
||||||
|
const originalConsoleWarn = console.warn;
|
||||||
|
console.warn = function(...args) {
|
||||||
|
if (isStompLog(args)) return;
|
||||||
|
originalConsoleWarn.apply(console, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 屏蔽 console.log(仅屏蔽 STOMP 相关)
|
||||||
|
const originalConsoleLog = console.log;
|
||||||
|
console.log = function(...args) {
|
||||||
|
if (isStompLog(args)) return;
|
||||||
|
originalConsoleLog.apply(console, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⚡ 限制 STOMP WebSocket 重连次数(最多 3 次)
|
||||||
|
// 通过代理 WebSocket 构造函数实现
|
||||||
|
const OriginalWebSocket = window.WebSocket;
|
||||||
|
const WebSocketProxy = function(url, protocols) {
|
||||||
|
// 检查是否是 STOMP 连接
|
||||||
|
if (url && url.includes('/stomp')) {
|
||||||
|
stompRetryCount++;
|
||||||
|
|
||||||
|
// 超过最大重试次数,阻止连接
|
||||||
|
if (stompRetryCount > MAX_STOMP_RETRIES) {
|
||||||
|
// 返回一个假的 WebSocket 对象,不实际连接
|
||||||
|
const fakeWs = Object.create(OriginalWebSocket.prototype);
|
||||||
|
fakeWs.url = url;
|
||||||
|
fakeWs.readyState = 3; // CLOSED
|
||||||
|
fakeWs.send = () => {};
|
||||||
|
fakeWs.close = () => {};
|
||||||
|
fakeWs.addEventListener = () => {};
|
||||||
|
fakeWs.removeEventListener = () => {};
|
||||||
|
fakeWs.onopen = null;
|
||||||
|
fakeWs.onclose = null;
|
||||||
|
fakeWs.onerror = null;
|
||||||
|
fakeWs.onmessage = null;
|
||||||
|
// 异步触发 onerror 和 onclose
|
||||||
|
setTimeout(() => {
|
||||||
|
if (fakeWs.onerror) fakeWs.onerror(new Event('error'));
|
||||||
|
if (fakeWs.onclose) fakeWs.onclose(new CloseEvent('close', { code: 1006 }));
|
||||||
|
}, 0);
|
||||||
|
return fakeWs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常创建 WebSocket
|
||||||
|
return protocols
|
||||||
|
? new OriginalWebSocket(url, protocols)
|
||||||
|
: new OriginalWebSocket(url);
|
||||||
|
};
|
||||||
|
// 保留原始 WebSocket 的静态属性和原型链
|
||||||
|
WebSocketProxy.prototype = OriginalWebSocket.prototype;
|
||||||
|
WebSocketProxy.CONNECTING = OriginalWebSocket.CONNECTING;
|
||||||
|
WebSocketProxy.OPEN = OriginalWebSocket.OPEN;
|
||||||
|
WebSocketProxy.CLOSING = OriginalWebSocket.CLOSING;
|
||||||
|
WebSocketProxy.CLOSED = OriginalWebSocket.CLOSED;
|
||||||
|
window.WebSocket = WebSocketProxy;
|
||||||
|
|
||||||
if (onLoad) {
|
if (onLoad) {
|
||||||
onLoad(bytedesk);
|
onLoad(bytedesk);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { getImportanceConfig } from '@constants/importanceLevels';
|
|||||||
import { eventService } from '@services/eventService';
|
import { eventService } from '@services/eventService';
|
||||||
import { useEventStocks } from '@components/Charts/Stock';
|
import { useEventStocks } from '@components/Charts/Stock';
|
||||||
import { toggleEventFollow, selectEventFollowStatus } from '@store/slices/communityDataSlice';
|
import { toggleEventFollow, selectEventFollowStatus } from '@store/slices/communityDataSlice';
|
||||||
|
import { selectSubscriptionInfo } from '@store/slices/subscriptionSlice';
|
||||||
import { useAuth } from '@contexts/AuthContext';
|
import { useAuth } from '@contexts/AuthContext';
|
||||||
import EventHeaderInfo from './EventHeaderInfo';
|
import EventHeaderInfo from './EventHeaderInfo';
|
||||||
import CompactMetaBar from './CompactMetaBar';
|
import CompactMetaBar from './CompactMetaBar';
|
||||||
@@ -102,6 +103,10 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
// 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type)
|
// 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type)
|
||||||
const userTier = user?.subscription_type || 'free';
|
const userTier = user?.subscription_type || 'free';
|
||||||
|
|
||||||
|
// 获取订阅信息,用于判断会员是否过期
|
||||||
|
const subscriptionInfo = useSelector(selectSubscriptionInfo);
|
||||||
|
const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active;
|
||||||
|
|
||||||
// 从 Redux 读取关注状态
|
// 从 Redux 读取关注状态
|
||||||
const eventFollowStatus = useSelector(selectEventFollowStatus);
|
const eventFollowStatus = useSelector(selectEventFollowStatus);
|
||||||
const isFollowing = event?.id ? (eventFollowStatus[event.id]?.isFollowing || false) : false;
|
const isFollowing = event?.id ? (eventFollowStatus[event.id]?.isFollowing || false) : false;
|
||||||
@@ -111,12 +116,16 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
const [fullEventDetail, setFullEventDetail] = useState(null);
|
const [fullEventDetail, setFullEventDetail] = useState(null);
|
||||||
const [loadingDetail, setLoadingDetail] = useState(false);
|
const [loadingDetail, setLoadingDetail] = useState(false);
|
||||||
|
|
||||||
// 权限判断函数
|
// 权限判断函数 - 会员过期时视为无权限
|
||||||
const hasAccess = useCallback((requiredTier) => {
|
const hasAccess = useCallback((requiredTier) => {
|
||||||
|
// 会员已过期,视为无权限
|
||||||
|
if (isSubscriptionExpired) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const tierLevel = { free: 0, pro: 1, max: 2 };
|
const tierLevel = { free: 0, pro: 1, max: 2 };
|
||||||
const result = tierLevel[userTier] >= tierLevel[requiredTier];
|
const result = tierLevel[userTier] >= tierLevel[requiredTier];
|
||||||
return result;
|
return result;
|
||||||
}, [userTier]);
|
}, [userTier, isSubscriptionExpired]);
|
||||||
|
|
||||||
// 升级弹窗状态
|
// 升级弹窗状态
|
||||||
const [upgradeModal, setUpgradeModal] = useState({
|
const [upgradeModal, setUpgradeModal] = useState({
|
||||||
@@ -169,14 +178,14 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
// 子区块折叠状态管理 - 使用 useReducer 整合
|
// 子区块折叠状态管理 - 使用 useReducer 整合
|
||||||
const [sectionState, dispatchSection] = useReducer(sectionReducer, initialSectionState);
|
const [sectionState, dispatchSection] = useReducer(sectionReducer, initialSectionState);
|
||||||
|
|
||||||
// 锁定点击处理 - 弹出升级弹窗
|
// 锁定点击处理 - 弹出升级弹窗(会员过期时显示续费提示)
|
||||||
const handleLockedClick = useCallback((featureName, requiredLevel) => {
|
const handleLockedClick = useCallback((featureName, requiredLevel) => {
|
||||||
setUpgradeModal({
|
setUpgradeModal({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
requiredLevel,
|
requiredLevel: isSubscriptionExpired ? subscriptionInfo.type : requiredLevel,
|
||||||
featureName
|
featureName: isSubscriptionExpired ? `${featureName}(会员已过期,请续费)` : featureName
|
||||||
});
|
});
|
||||||
}, []);
|
}, [isSubscriptionExpired, subscriptionInfo.type]);
|
||||||
|
|
||||||
// 关闭升级弹窗
|
// 关闭升级弹窗
|
||||||
const handleCloseUpgradeModal = useCallback(() => {
|
const handleCloseUpgradeModal = useCallback(() => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// 相关概念区组件 - 便当盒网格布局
|
// 相关概念区组件 - 便当盒网格布局
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
import { selectSubscriptionInfo } from '@store/slices/subscriptionSlice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单个概念卡片组件(便当盒样式)
|
* 单个概念卡片组件(便当盒样式)
|
||||||
@@ -100,6 +102,10 @@ const RelatedConceptsSection = ({
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 获取订阅信息,用于判断会员是否过期
|
||||||
|
const subscriptionInfo = useSelector(selectSubscriptionInfo);
|
||||||
|
const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active;
|
||||||
|
|
||||||
// 颜色配置 - 使用深色主题固定颜色
|
// 颜色配置 - 使用深色主题固定颜色
|
||||||
const sectionBg = 'transparent';
|
const sectionBg = 'transparent';
|
||||||
const headingColor = '#e2e8f0';
|
const headingColor = '#e2e8f0';
|
||||||
@@ -107,7 +113,7 @@ const RelatedConceptsSection = ({
|
|||||||
const countBadgeBg = '#3182ce';
|
const countBadgeBg = '#3182ce';
|
||||||
const countBadgeColor = '#ffffff';
|
const countBadgeColor = '#ffffff';
|
||||||
|
|
||||||
// 获取相关概念
|
// 获取相关概念 - 如果被锁定或会员过期则跳过 API 请求
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchConcepts = async () => {
|
const fetchConcepts = async () => {
|
||||||
if (!eventId) {
|
if (!eventId) {
|
||||||
@@ -115,6 +121,13 @@ const RelatedConceptsSection = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果被锁定或会员已过期,不发起 API 请求
|
||||||
|
if (isLocked || isSubscriptionExpired) {
|
||||||
|
setLoading(false);
|
||||||
|
setConcepts([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -152,7 +165,7 @@ const RelatedConceptsSection = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchConcepts();
|
fetchConcepts();
|
||||||
}, [eventId]);
|
}, [eventId, isLocked, isSubscriptionExpired]);
|
||||||
|
|
||||||
// 跳转到概念中心
|
// 跳转到概念中心
|
||||||
const handleNavigate = (concept) => {
|
const handleNavigate = (concept) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -39,6 +40,7 @@ import CitedContent from '../../../components/Citation/CitedContent';
|
|||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
import { getApiBase } from '../../../utils/apiConfig';
|
import { getApiBase } from '../../../utils/apiConfig';
|
||||||
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
|
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
|
||||||
|
import { selectSubscriptionInfo } from '../../../store/slices/subscriptionSlice';
|
||||||
|
|
||||||
// 节点样式配置 - 完全复刻Flask版本
|
// 节点样式配置 - 完全复刻Flask版本
|
||||||
const NODE_STYLES = {
|
const NODE_STYLES = {
|
||||||
@@ -460,6 +462,10 @@ function getSankeyOption(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TransmissionChainAnalysis = ({ eventId }) => {
|
const TransmissionChainAnalysis = ({ eventId }) => {
|
||||||
|
// 获取订阅信息,用于判断会员是否过期
|
||||||
|
const subscriptionInfo = useSelector(selectSubscriptionInfo);
|
||||||
|
const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active;
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [graphData, setGraphData] = useState(null);
|
const [graphData, setGraphData] = useState(null);
|
||||||
const [sankeyData, setSankeyData] = useState(null);
|
const [sankeyData, setSankeyData] = useState(null);
|
||||||
@@ -474,7 +480,7 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
|||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
totalNodes: 0,
|
totalNodes: 0,
|
||||||
involvedIndustries: 0,
|
involvedIndustries: 0,
|
||||||
relatedCompanies: 0,
|
relatedCompanies: 0,
|
||||||
positiveImpact: 0,
|
positiveImpact: 0,
|
||||||
negativeImpact: 0,
|
negativeImpact: 0,
|
||||||
circularEffect: 0
|
circularEffect: 0
|
||||||
@@ -514,9 +520,18 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
|||||||
}
|
}
|
||||||
}, [graphData]);
|
}, [graphData]);
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据 - 如果会员过期则跳过 API 请求
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
|
// 会员已过期,不发起 API 请求
|
||||||
|
if (isSubscriptionExpired) {
|
||||||
|
logger.debug('TransmissionChain', '会员已过期,跳过传导链数据加载', { eventId });
|
||||||
|
setLoading(false);
|
||||||
|
setGraphData(null);
|
||||||
|
setSankeyData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -563,7 +578,7 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
|||||||
if (eventId) {
|
if (eventId) {
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
}, [eventId]);
|
}, [eventId, isSubscriptionExpired]);
|
||||||
|
|
||||||
// BFS路径查找 - 完全复刻Flask版本
|
// BFS路径查找 - 完全复刻Flask版本
|
||||||
function findPath(nodes, edges, fromId, toId) {
|
function findPath(nodes, edges, fromId, toId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user