diff --git a/app.py b/app.py index e54f3e4d..549ffe60 100755 --- a/app.py +++ b/app.py @@ -6407,6 +6407,11 @@ def get_stock_kline(stock_code): chart_type = request.args.get('type', 'minute') event_time = request.args.get('event_time') + # 是否跳过"下一个交易日"逻辑: + # - 如果没有传 event_time(灵活屏等实时行情场景),盘后应显示当天数据 + # - 如果传了 event_time(Community 事件等场景),使用原逻辑 + skip_next_day = event_time is None + try: event_datetime = datetime.fromisoformat(event_time) if event_time else datetime.now() except ValueError: @@ -6426,7 +6431,7 @@ def get_stock_kline(stock_code): if chart_type == 'daily': return get_daily_kline(stock_code, event_datetime, stock_name) elif chart_type == 'minute': - return get_minute_kline(stock_code, event_datetime, stock_name) + return get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=skip_next_day) elif chart_type == 'timeline': return get_timeline_data(stock_code, event_datetime, stock_name) else: @@ -7584,15 +7589,23 @@ def get_daily_kline(stock_code, event_datetime, stock_name): }) -def get_minute_kline(stock_code, event_datetime, stock_name): - """处理分钟K线数据""" +def get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=False): + """处理分钟K线数据 + + Args: + stock_code: 股票代码 + event_datetime: 事件时间 + stock_name: 股票名称 + skip_next_day: 是否跳过"下一个交易日"逻辑(用于灵活屏盘后查看当天数据) + """ client = get_clickhouse_client() target_date = get_trading_day_near_date(event_datetime.date()) is_after_market = event_datetime.time() > dt_time(15, 0) - # 核心逻辑改动:先判断当前日期是否是交易日,以及是否已收盘 - if target_date and is_after_market: + # 只有在指定了 event_time 参数时(如 Community 页面事件)才跳转到下一个交易日 + # 灵活屏等实时行情场景,盘后应显示当天数据 + if target_date and is_after_market and not skip_next_day: # 如果是交易日且已收盘,查找下一个交易日 next_trade_date = get_trading_day_near_date(target_date + timedelta(days=1)) if next_trade_date: diff --git a/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts b/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts index d7bec4ad..d8fb5693 100644 --- a/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts +++ b/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts @@ -219,20 +219,46 @@ const handleSZSERealtimeMessage = ( case 'afterhours_trading': { const afterhoursData = data as SZSEAfterhoursData; const existing = prevQuotes[fullCode]; + const afterhoursInfo = { + bidPx: afterhoursData.bid_px, + bidSize: afterhoursData.bid_size, + offerPx: afterhoursData.offer_px, + offerSize: afterhoursData.offer_size, + volume: afterhoursData.volume, + amount: afterhoursData.amount, + numTrades: afterhoursData.num_trades || 0, + }; + if (existing) { + // 合并到现有数据 updated[fullCode] = { ...existing, - afterhours: { - bidPx: afterhoursData.bid_px, - bidSize: afterhoursData.bid_size, - offerPx: afterhoursData.offer_px, - offerSize: afterhoursData.offer_size, - volume: afterhoursData.volume, - amount: afterhoursData.amount, - numTrades: afterhoursData.num_trades || 0, - }, + afterhours: afterhoursInfo, updateTime: timestamp, } as QuoteData; + } else { + // 盘后首次收到数据(刷新页面后),创建基础行情结构 + updated[fullCode] = { + code: fullCode, + name: '', + price: 0, + prevClose: afterhoursData.prev_close, + open: 0, + high: 0, + low: 0, + volume: 0, + amount: 0, + change: 0, + changePct: 0, + bidPrices: [], + bidVolumes: [], + askPrices: [], + askVolumes: [], + tradingPhase: afterhoursData.trading_phase, + afterhours: afterhoursInfo, + updateTime: timestamp, + exchange: 'SZSE', + } as QuoteData; } break; } @@ -360,6 +386,9 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn = const wsRefs = useRef>({ SSE: null, SZSE: null }); const heartbeatRefs = useRef>({ SSE: null, SZSE: null }); const reconnectRefs = useRef>({ SSE: null, SZSE: null }); + // 重连计数器(避免无限重连刷屏) + const reconnectCountRef = useRef>({ SSE: 0, SZSE: 0 }); + const MAX_RECONNECT_ATTEMPTS = 5; // 深交所 WebSocket 就绪状态(收到 welcome 消息后才能订阅) const szseReadyRef = useRef(false); // 待发送的深交所订阅队列(在 welcome 之前收到的订阅请求) @@ -539,8 +568,10 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn = wsRefs.current[exchange] = ws; ws.onopen = () => { - logger.info('FlexScreen', `${exchange} WebSocket 已连接`); + logger.info('FlexScreen', `${exchange} WebSocket 已连接`, { url: wsUrl }); setConnected(prev => ({ ...prev, [exchange]: true })); + // 连接成功,重置重连计数 + reconnectCountRef.current[exchange] = 0; if (exchange === 'SSE') { // 上交所:连接后立即发送订阅 @@ -569,7 +600,11 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn = }; ws.onerror = (error: Event) => { - logger.error('FlexScreen', `${exchange} WebSocket 错误`, error); + logger.error('FlexScreen', `${exchange} WebSocket 连接失败`, { + url: wsUrl, + readyState: ws.readyState, + hint: '请检查:1) 后端服务是否启动 2) Nginx 代理是否配置正确', + }); }; ws.onclose = () => { @@ -582,14 +617,28 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn = szseReadyRef.current = false; } - // 自动重连 - if (!reconnectRefs.current[exchange] && subscribedCodes.current[exchange].size > 0) { + // 自动重连(有次数限制,避免刷屏) + const currentAttempts = reconnectCountRef.current[exchange]; + if ( + !reconnectRefs.current[exchange] && + subscribedCodes.current[exchange].size > 0 && + currentAttempts < MAX_RECONNECT_ATTEMPTS + ) { + reconnectCountRef.current[exchange] = currentAttempts + 1; + // 指数退避:3秒、6秒、12秒、24秒、48秒 + const delay = RECONNECT_INTERVAL * Math.pow(2, currentAttempts); + logger.info('FlexScreen', `${exchange} 将在 ${delay / 1000} 秒后重连 (${currentAttempts + 1}/${MAX_RECONNECT_ATTEMPTS})`); + reconnectRefs.current[exchange] = setTimeout(() => { reconnectRefs.current[exchange] = null; if (subscribedCodes.current[exchange].size > 0) { createConnection(exchange); } - }, RECONNECT_INTERVAL); + }, delay); + } else if (currentAttempts >= MAX_RECONNECT_ATTEMPTS) { + logger.warn('FlexScreen', `${exchange} 达到最大重连次数,停止重连。请检查 WebSocket 服务是否正常。`, { + url: wsUrl, + }); } }; } catch (e) {