diff --git a/app.py b/app.py index ea20cb70..d978f1d0 100755 --- a/app.py +++ b/app.py @@ -8650,6 +8650,144 @@ def get_stock_basic_info(stock_code): return jsonify({'success': False, 'error': str(e)}), 500 +@app.route('/api/stock//quote-detail', methods=['GET']) +def get_stock_quote_detail(stock_code): + """获取股票完整行情数据 - 供 StockQuoteCard 使用 + + 返回数据包括: + - 基础信息:名称、代码、行业分类 + - 价格信息:现价、涨跌幅、开盘、收盘、最高、最低 + - 关键指标:市盈率、市净率、流通市值、52周高低 + - 主力动态:主力净流入、机构持仓(如有) + """ + try: + # 标准化股票代码(去除后缀) + base_code = stock_code.split('.')[0] if '.' in stock_code else stock_code + + result_data = { + 'code': stock_code, + 'name': '', + 'industry': '', + 'industry_l1': '', + 'sw_industry_l1': '', + 'sw_industry_l2': '', + + # 价格信息 + 'current_price': None, + 'change_percent': None, + 'today_open': None, + 'yesterday_close': None, + 'today_high': None, + 'today_low': None, + + # 关键指标 + 'pe': None, + 'pb': None, + 'eps': None, + 'market_cap': None, + 'circ_mv': None, + 'turnover_rate': None, + 'week52_high': None, + 'week52_low': None, + + # 主力动态(预留字段) + 'main_net_inflow': None, + 'institution_holding': None, + 'buy_ratio': None, + 'sell_ratio': None, + + 'update_time': None + } + + with engine.connect() as conn: + # 1. 获取最新交易数据(来自 ea_trade) + trade_query = text(""" + SELECT + t.SECCODE, + t.SECNAME, + t.TRADEDATE, + t.F002N as pre_close, + t.F003N as open_price, + t.F004N as volume, + t.F005N as high, + t.F006N as low, + t.F007N as close_price, + t.F010N as change_pct, + t.F011N as amount, + t.F012N as turnover_rate, + t.F020N as total_shares, + t.F021N as float_shares, + t.F026N as pe_ratio, + b.F034V as sw_industry_l1, + b.F036V as sw_industry_l2, + b.F030V as industry_l1 + FROM ea_trade t + LEFT JOIN ea_baseinfo b ON t.SECCODE = b.SECCODE + WHERE t.SECCODE = :stock_code + ORDER BY t.TRADEDATE DESC + LIMIT 1 + """) + + trade_result = conn.execute(trade_query, {'stock_code': base_code}).fetchone() + + if trade_result: + row = row_to_dict(trade_result) + result_data['name'] = row.get('SECNAME') or '' + result_data['current_price'] = float(row.get('close_price') or 0) + result_data['change_percent'] = float(row.get('change_pct') or 0) + result_data['today_open'] = float(row.get('open_price') or 0) + result_data['yesterday_close'] = float(row.get('pre_close') or 0) + result_data['today_high'] = float(row.get('high') or 0) + result_data['today_low'] = float(row.get('low') or 0) + result_data['pe'] = float(row.get('pe_ratio') or 0) if row.get('pe_ratio') else None + result_data['turnover_rate'] = float(row.get('turnover_rate') or 0) + result_data['sw_industry_l1'] = row.get('sw_industry_l1') or '' + result_data['sw_industry_l2'] = row.get('sw_industry_l2') or '' + result_data['industry_l1'] = row.get('industry_l1') or '' + result_data['industry'] = row.get('sw_industry_l2') or row.get('sw_industry_l1') or '' + + # 计算流通市值(亿元) + float_shares = float(row.get('float_shares') or 0) + close_price = float(row.get('close_price') or 0) + if float_shares > 0 and close_price > 0: + circ_mv = (float_shares * close_price) / 100000000 # 转为亿 + result_data['circ_mv'] = round(circ_mv, 2) + result_data['market_cap'] = f"{round(circ_mv, 2)}亿" + + trade_date = row.get('TRADEDATE') + if trade_date: + if hasattr(trade_date, 'strftime'): + result_data['update_time'] = trade_date.strftime('%Y-%m-%d') + else: + result_data['update_time'] = str(trade_date) + + # 2. 获取52周高低价 + week52_query = text(""" + SELECT + MAX(F005N) as week52_high, + MIN(F006N) as week52_low + FROM ea_trade + WHERE SECCODE = :stock_code + AND TRADEDATE >= DATE_SUB(CURDATE(), INTERVAL 52 WEEK) + AND F005N > 0 AND F006N > 0 + """) + + week52_result = conn.execute(week52_query, {'stock_code': base_code}).fetchone() + if week52_result: + w52 = row_to_dict(week52_result) + result_data['week52_high'] = float(w52.get('week52_high') or 0) + result_data['week52_low'] = float(w52.get('week52_low') or 0) + + return jsonify({ + 'success': True, + 'data': result_data + }) + + except Exception as e: + app.logger.error(f"Error getting stock quote detail: {e}", exc_info=True) + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/stock//announcements', methods=['GET']) def get_stock_announcements(stock_code): """获取股票公告列表""" diff --git a/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts b/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts index 3d1e28a9..497e1a5e 100644 --- a/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts +++ b/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts @@ -1,11 +1,11 @@ /** * useStockQuoteData - 股票行情数据获取 Hook * - * 合并获取行情数据和基本信息,供 StockQuoteCard 内部使用 + * 使用 /api/stock/{code}/quote-detail 接口获取完整行情数据 + * 供 StockQuoteCard 内部使用 */ import { useState, useEffect, useCallback } from 'react'; -import { stockService } from '@services/eventService'; import { logger } from '@utils/logger'; import axios from '@utils/axiosConfig'; import type { StockQuoteCardData } from '../types'; @@ -77,16 +77,23 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult = const refetchRef = useCallback(async () => { if (!stockCode) return; - // 获取行情数据 + // 标准化股票代码(去除后缀) + const baseCode = stockCode.split('.')[0]; + + // 获取行情详情数据(使用新的 quote-detail 接口) setQuoteLoading(true); setError(null); try { - logger.debug('useStockQuoteData', '获取股票行情', { stockCode }); - const quotes = await stockService.getQuotes([stockCode]); - const quoteResult = quotes?.[stockCode] || quotes; - const transformedData = transformQuoteData(quoteResult, stockCode); - logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData }); - setQuoteData(transformedData); + logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode }); + const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`); + if (result.success && result.data) { + const transformedData = transformQuoteData(result.data, stockCode); + logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData }); + setQuoteData(transformedData); + } else { + setError('获取行情数据失败'); + setQuoteData(null); + } } catch (err) { logger.error('useStockQuoteData', '获取行情失败', err); setError('获取行情数据失败'); @@ -95,10 +102,10 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult = setQuoteLoading(false); } - // 获取基本信息 + // 获取基本信息(公司简介等) setBasicLoading(true); try { - const { data: result } = await axios.get(`/api/stock/${stockCode}/basic-info`); + const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`); if (result.success) { setBasicInfo(result.data); } @@ -120,18 +127,27 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult = const controller = new AbortController(); let isCancelled = false; + // 标准化股票代码(去除后缀) + const baseCode = stockCode.split('.')[0]; + const fetchData = async () => { - // 获取行情数据 + // 获取行情详情数据(使用新的 quote-detail 接口) setQuoteLoading(true); setError(null); try { - logger.debug('useStockQuoteData', '获取股票行情', { stockCode }); - const quotes = await stockService.getQuotes([stockCode]); + logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode }); + const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`, { + signal: controller.signal, + }); if (isCancelled) return; - const quoteResult = quotes?.[stockCode] || quotes; - const transformedData = transformQuoteData(quoteResult, stockCode); - logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData }); - setQuoteData(transformedData); + if (result.success && result.data) { + const transformedData = transformQuoteData(result.data, stockCode); + logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData }); + setQuoteData(transformedData); + } else { + setError('获取行情数据失败'); + setQuoteData(null); + } } catch (err: any) { if (isCancelled || err.name === 'CanceledError') return; logger.error('useStockQuoteData', '获取行情失败', err); @@ -141,10 +157,10 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult = if (!isCancelled) setQuoteLoading(false); } - // 获取基本信息 + // 获取基本信息(公司简介等) setBasicLoading(true); try { - const { data: result } = await axios.get(`/api/stock/${stockCode}/basic-info`, { + const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`, { signal: controller.signal, }); if (isCancelled) return;