update pay ui
This commit is contained in:
23
app.py
23
app.py
@@ -6407,6 +6407,11 @@ def get_stock_kline(stock_code):
|
|||||||
chart_type = request.args.get('type', 'minute')
|
chart_type = request.args.get('type', 'minute')
|
||||||
event_time = request.args.get('event_time')
|
event_time = request.args.get('event_time')
|
||||||
|
|
||||||
|
# 是否跳过"下一个交易日"逻辑:
|
||||||
|
# - 如果没有传 event_time(灵活屏等实时行情场景),盘后应显示当天数据
|
||||||
|
# - 如果传了 event_time(Community 事件等场景),使用原逻辑
|
||||||
|
skip_next_day = event_time is None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
event_datetime = datetime.fromisoformat(event_time) if event_time else datetime.now()
|
event_datetime = datetime.fromisoformat(event_time) if event_time else datetime.now()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -6426,7 +6431,7 @@ def get_stock_kline(stock_code):
|
|||||||
if chart_type == 'daily':
|
if chart_type == 'daily':
|
||||||
return get_daily_kline(stock_code, event_datetime, stock_name)
|
return get_daily_kline(stock_code, event_datetime, stock_name)
|
||||||
elif chart_type == 'minute':
|
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':
|
elif chart_type == 'timeline':
|
||||||
return get_timeline_data(stock_code, event_datetime, stock_name)
|
return get_timeline_data(stock_code, event_datetime, stock_name)
|
||||||
else:
|
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):
|
def get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=False):
|
||||||
"""处理分钟K线数据"""
|
"""处理分钟K线数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stock_code: 股票代码
|
||||||
|
event_datetime: 事件时间
|
||||||
|
stock_name: 股票名称
|
||||||
|
skip_next_day: 是否跳过"下一个交易日"逻辑(用于灵活屏盘后查看当天数据)
|
||||||
|
"""
|
||||||
client = get_clickhouse_client()
|
client = get_clickhouse_client()
|
||||||
|
|
||||||
target_date = get_trading_day_near_date(event_datetime.date())
|
target_date = get_trading_day_near_date(event_datetime.date())
|
||||||
is_after_market = event_datetime.time() > dt_time(15, 0)
|
is_after_market = event_datetime.time() > dt_time(15, 0)
|
||||||
|
|
||||||
# 核心逻辑改动:先判断当前日期是否是交易日,以及是否已收盘
|
# 只有在指定了 event_time 参数时(如 Community 页面事件)才跳转到下一个交易日
|
||||||
if target_date and is_after_market:
|
# 灵活屏等实时行情场景,盘后应显示当天数据
|
||||||
|
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))
|
next_trade_date = get_trading_day_near_date(target_date + timedelta(days=1))
|
||||||
if next_trade_date:
|
if next_trade_date:
|
||||||
|
|||||||
@@ -219,20 +219,46 @@ const handleSZSERealtimeMessage = (
|
|||||||
case 'afterhours_trading': {
|
case 'afterhours_trading': {
|
||||||
const afterhoursData = data as SZSEAfterhoursData;
|
const afterhoursData = data as SZSEAfterhoursData;
|
||||||
const existing = prevQuotes[fullCode];
|
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) {
|
if (existing) {
|
||||||
|
// 合并到现有数据
|
||||||
updated[fullCode] = {
|
updated[fullCode] = {
|
||||||
...existing,
|
...existing,
|
||||||
afterhours: {
|
afterhours: 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,
|
|
||||||
},
|
|
||||||
updateTime: timestamp,
|
updateTime: timestamp,
|
||||||
} as QuoteData;
|
} 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;
|
break;
|
||||||
}
|
}
|
||||||
@@ -360,6 +386,9 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
|||||||
const wsRefs = useRef<Record<Exchange, WebSocket | null>>({ SSE: null, SZSE: null });
|
const wsRefs = useRef<Record<Exchange, WebSocket | null>>({ SSE: null, SZSE: null });
|
||||||
const heartbeatRefs = useRef<Record<Exchange, NodeJS.Timeout | null>>({ SSE: null, SZSE: null });
|
const heartbeatRefs = useRef<Record<Exchange, NodeJS.Timeout | null>>({ SSE: null, SZSE: null });
|
||||||
const reconnectRefs = useRef<Record<Exchange, NodeJS.Timeout | null>>({ SSE: null, SZSE: null });
|
const reconnectRefs = useRef<Record<Exchange, NodeJS.Timeout | null>>({ SSE: null, SZSE: null });
|
||||||
|
// 重连计数器(避免无限重连刷屏)
|
||||||
|
const reconnectCountRef = useRef<Record<Exchange, number>>({ SSE: 0, SZSE: 0 });
|
||||||
|
const MAX_RECONNECT_ATTEMPTS = 5;
|
||||||
// 深交所 WebSocket 就绪状态(收到 welcome 消息后才能订阅)
|
// 深交所 WebSocket 就绪状态(收到 welcome 消息后才能订阅)
|
||||||
const szseReadyRef = useRef<boolean>(false);
|
const szseReadyRef = useRef<boolean>(false);
|
||||||
// 待发送的深交所订阅队列(在 welcome 之前收到的订阅请求)
|
// 待发送的深交所订阅队列(在 welcome 之前收到的订阅请求)
|
||||||
@@ -539,8 +568,10 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
|||||||
wsRefs.current[exchange] = ws;
|
wsRefs.current[exchange] = ws;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
logger.info('FlexScreen', `${exchange} WebSocket 已连接`);
|
logger.info('FlexScreen', `${exchange} WebSocket 已连接`, { url: wsUrl });
|
||||||
setConnected(prev => ({ ...prev, [exchange]: true }));
|
setConnected(prev => ({ ...prev, [exchange]: true }));
|
||||||
|
// 连接成功,重置重连计数
|
||||||
|
reconnectCountRef.current[exchange] = 0;
|
||||||
|
|
||||||
if (exchange === 'SSE') {
|
if (exchange === 'SSE') {
|
||||||
// 上交所:连接后立即发送订阅
|
// 上交所:连接后立即发送订阅
|
||||||
@@ -569,7 +600,11 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error: Event) => {
|
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 = () => {
|
ws.onclose = () => {
|
||||||
@@ -582,14 +617,28 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
|||||||
szseReadyRef.current = false;
|
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] = setTimeout(() => {
|
||||||
reconnectRefs.current[exchange] = null;
|
reconnectRefs.current[exchange] = null;
|
||||||
if (subscribedCodes.current[exchange].size > 0) {
|
if (subscribedCodes.current[exchange].size > 0) {
|
||||||
createConnection(exchange);
|
createConnection(exchange);
|
||||||
}
|
}
|
||||||
}, RECONNECT_INTERVAL);
|
}, delay);
|
||||||
|
} else if (currentAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||||
|
logger.warn('FlexScreen', `${exchange} 达到最大重连次数,停止重连。请检查 WebSocket 服务是否正常。`, {
|
||||||
|
url: wsUrl,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user