From c529626ce2003e22b122fed3d963731ec90ba15f Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 24 Dec 2025 15:24:06 +0800 Subject: [PATCH] =?UTF-8?q?=20=20fix:=20=E4=BC=9A=E5=91=98=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E6=97=B6=E8=B7=B3=E8=BF=87=20API=20=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=20&=20=E9=99=90=E5=88=B6=20STOMP=20WebSocket=20=E9=87=8D?= =?UTF-8?q?=E8=BF=9E=E6=AC=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DynamicNewsDetailPanel: 添加会员过期判断,过期时显示续费提示 - RelatedConceptsSection: 会员过期时跳过概念 API 请求 - TransmissionChainAnalysis: 会员过期时跳过传导链 API 请求 - BytedeskWidget: 限制 STOMP WebSocket 最多重连 3 次,屏蔽相关日志 --- .../components/BytedeskWidget.jsx | 79 +++++++++++++++++-- .../DynamicNewsDetailPanel.js | 21 +++-- .../RelatedConceptsSection/index.js | 17 +++- .../components/TransmissionChainAnalysis.js | 21 ++++- 4 files changed, 120 insertions(+), 18 deletions(-) diff --git a/src/bytedesk-integration/components/BytedeskWidget.jsx b/src/bytedesk-integration/components/BytedeskWidget.jsx index af2eddcf..9fac11d3 100644 --- a/src/bytedesk-integration/components/BytedeskWidget.jsx +++ b/src/bytedesk-integration/components/BytedeskWidget.jsx @@ -19,6 +19,10 @@ import PropTypes from 'prop-types'; let widgetInitialized = false; let idleCallbackId = null; +// ⚡ STOMP WebSocket 重连限制(最多重试 3 次) +let stompRetryCount = 0; +const MAX_STOMP_RETRIES = 3; + const BytedeskWidget = ({ config, autoLoad = true, @@ -118,18 +122,79 @@ const BytedeskWidget = ({ // 5 秒后停止监听(避免性能问题) 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; console.error = function(...args) { - const errorMsg = args.join(' '); - if (errorMsg.includes('/stomp') || - errorMsg.includes('stomp onWebSocketError') || - (errorMsg.includes('WebSocket connection to') && errorMsg.includes('/stomp'))) { - return; - } + if (isStompLog(args)) return; 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) { onLoad(bytedesk); } diff --git a/src/components/EventDetailPanel/DynamicNewsDetailPanel.js b/src/components/EventDetailPanel/DynamicNewsDetailPanel.js index 6d92d16b..56f34308 100644 --- a/src/components/EventDetailPanel/DynamicNewsDetailPanel.js +++ b/src/components/EventDetailPanel/DynamicNewsDetailPanel.js @@ -18,6 +18,7 @@ import { getImportanceConfig } from '@constants/importanceLevels'; import { eventService } from '@services/eventService'; import { useEventStocks } from '@components/Charts/Stock'; import { toggleEventFollow, selectEventFollowStatus } from '@store/slices/communityDataSlice'; +import { selectSubscriptionInfo } from '@store/slices/subscriptionSlice'; import { useAuth } from '@contexts/AuthContext'; import EventHeaderInfo from './EventHeaderInfo'; import CompactMetaBar from './CompactMetaBar'; @@ -102,6 +103,10 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { // 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type) const userTier = user?.subscription_type || 'free'; + // 获取订阅信息,用于判断会员是否过期 + const subscriptionInfo = useSelector(selectSubscriptionInfo); + const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active; + // 从 Redux 读取关注状态 const eventFollowStatus = useSelector(selectEventFollowStatus); 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 [loadingDetail, setLoadingDetail] = useState(false); - // 权限判断函数 + // 权限判断函数 - 会员过期时视为无权限 const hasAccess = useCallback((requiredTier) => { + // 会员已过期,视为无权限 + if (isSubscriptionExpired) { + return false; + } const tierLevel = { free: 0, pro: 1, max: 2 }; const result = tierLevel[userTier] >= tierLevel[requiredTier]; return result; - }, [userTier]); + }, [userTier, isSubscriptionExpired]); // 升级弹窗状态 const [upgradeModal, setUpgradeModal] = useState({ @@ -169,14 +178,14 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { // 子区块折叠状态管理 - 使用 useReducer 整合 const [sectionState, dispatchSection] = useReducer(sectionReducer, initialSectionState); - // 锁定点击处理 - 弹出升级弹窗 + // 锁定点击处理 - 弹出升级弹窗(会员过期时显示续费提示) const handleLockedClick = useCallback((featureName, requiredLevel) => { setUpgradeModal({ isOpen: true, - requiredLevel, - featureName + requiredLevel: isSubscriptionExpired ? subscriptionInfo.type : requiredLevel, + featureName: isSubscriptionExpired ? `${featureName}(会员已过期,请续费)` : featureName }); - }, []); + }, [isSubscriptionExpired, subscriptionInfo.type]); // 关闭升级弹窗 const handleCloseUpgradeModal = useCallback(() => { diff --git a/src/components/EventDetailPanel/RelatedConceptsSection/index.js b/src/components/EventDetailPanel/RelatedConceptsSection/index.js index 2cc2f335..d93a483b 100644 --- a/src/components/EventDetailPanel/RelatedConceptsSection/index.js +++ b/src/components/EventDetailPanel/RelatedConceptsSection/index.js @@ -2,6 +2,7 @@ // 相关概念区组件 - 便当盒网格布局 import React, { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; import { Box, Flex, @@ -18,6 +19,7 @@ import { import { useNavigate } from 'react-router-dom'; import { logger } from '@utils/logger'; import { getApiBase } from '@utils/apiConfig'; +import { selectSubscriptionInfo } from '@store/slices/subscriptionSlice'; /** * 单个概念卡片组件(便当盒样式) @@ -100,6 +102,10 @@ const RelatedConceptsSection = ({ const [error, setError] = useState(null); const navigate = useNavigate(); + // 获取订阅信息,用于判断会员是否过期 + const subscriptionInfo = useSelector(selectSubscriptionInfo); + const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active; + // 颜色配置 - 使用深色主题固定颜色 const sectionBg = 'transparent'; const headingColor = '#e2e8f0'; @@ -107,7 +113,7 @@ const RelatedConceptsSection = ({ const countBadgeBg = '#3182ce'; const countBadgeColor = '#ffffff'; - // 获取相关概念 + // 获取相关概念 - 如果被锁定或会员过期则跳过 API 请求 useEffect(() => { const fetchConcepts = async () => { if (!eventId) { @@ -115,6 +121,13 @@ const RelatedConceptsSection = ({ return; } + // 如果被锁定或会员已过期,不发起 API 请求 + if (isLocked || isSubscriptionExpired) { + setLoading(false); + setConcepts([]); + return; + } + try { setLoading(true); setError(null); @@ -152,7 +165,7 @@ const RelatedConceptsSection = ({ }; fetchConcepts(); - }, [eventId]); + }, [eventId, isLocked, isSubscriptionExpired]); // 跳转到概念中心 const handleNavigate = (concept) => { diff --git a/src/views/EventDetail/components/TransmissionChainAnalysis.js b/src/views/EventDetail/components/TransmissionChainAnalysis.js index 412fdf43..cf10f159 100644 --- a/src/views/EventDetail/components/TransmissionChainAnalysis.js +++ b/src/views/EventDetail/components/TransmissionChainAnalysis.js @@ -1,4 +1,5 @@ import React, { useEffect, useState, useRef } from 'react'; +import { useSelector } from 'react-redux'; import { Box, Button, @@ -39,6 +40,7 @@ import CitedContent from '../../../components/Citation/CitedContent'; import { logger } from '../../../utils/logger'; import { getApiBase } from '../../../utils/apiConfig'; import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme'; +import { selectSubscriptionInfo } from '../../../store/slices/subscriptionSlice'; // 节点样式配置 - 完全复刻Flask版本 const NODE_STYLES = { @@ -460,6 +462,10 @@ function getSankeyOption(data) { } const TransmissionChainAnalysis = ({ eventId }) => { + // 获取订阅信息,用于判断会员是否过期 + const subscriptionInfo = useSelector(selectSubscriptionInfo); + const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active; + // 状态管理 const [graphData, setGraphData] = useState(null); const [sankeyData, setSankeyData] = useState(null); @@ -474,7 +480,7 @@ const TransmissionChainAnalysis = ({ eventId }) => { const [stats, setStats] = useState({ totalNodes: 0, involvedIndustries: 0, - relatedCompanies: 0, + relatedCompanies: 0, positiveImpact: 0, negativeImpact: 0, circularEffect: 0 @@ -514,9 +520,18 @@ const TransmissionChainAnalysis = ({ eventId }) => { } }, [graphData]); - // 加载数据 + // 加载数据 - 如果会员过期则跳过 API 请求 useEffect(() => { async function fetchData() { + // 会员已过期,不发起 API 请求 + if (isSubscriptionExpired) { + logger.debug('TransmissionChain', '会员已过期,跳过传导链数据加载', { eventId }); + setLoading(false); + setGraphData(null); + setSankeyData(null); + return; + } + setLoading(true); setError(null); try { @@ -563,7 +578,7 @@ const TransmissionChainAnalysis = ({ eventId }) => { if (eventId) { fetchData(); } - }, [eventId]); + }, [eventId, isSubscriptionExpired]); // BFS路径查找 - 完全复刻Flask版本 function findPath(nodes, edges, fromId, toId) {