From 4c7a761324bc36db7e9e319146b77f8c37348e5d Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 5 Dec 2025 19:40:11 +0800 Subject: [PATCH] update pay ui --- app.py | 96 ++++++++++++++++ src/views/Concept/ConceptTimelineModal.js | 103 ++++++++++-------- .../Concept/components/ForceGraphView.js | 86 +++++++-------- src/views/Concept/index.js | 33 +++--- 4 files changed, 214 insertions(+), 104 deletions(-) diff --git a/app.py b/app.py index 91940dab..7cf3941e 100755 --- a/app.py +++ b/app.py @@ -5508,6 +5508,102 @@ def delete_related_stock(stock_id): return jsonify({'success': False, 'error': str(e)}), 500 +@app.route('/api/events/by-stocks', methods=['POST']) +def get_events_by_stocks(): + """ + 通过股票代码列表获取关联的事件(新闻) + 用于概念中心时间轴:聚合概念下所有股票的相关新闻 + + 请求体: + { + "stock_codes": ["000001.SZ", "600000.SH", ...], # 股票代码列表 + "start_date": "2024-01-01", # 可选,开始日期 + "end_date": "2024-12-31", # 可选,结束日期 + "limit": 100 # 可选,限制返回数量,默认100 + } + """ + try: + data = request.get_json() + stock_codes = data.get('stock_codes', []) + start_date_str = data.get('start_date') + end_date_str = data.get('end_date') + limit = data.get('limit', 100) + + if not stock_codes: + return jsonify({'success': False, 'error': '缺少股票代码列表'}), 400 + + # 构建查询:通过 RelatedStock 表找到关联的事件 + query = db.session.query(Event).join( + RelatedStock, Event.id == RelatedStock.event_id + ).filter( + RelatedStock.stock_code.in_(stock_codes) + ) + + # 日期过滤 + if start_date_str: + try: + start_date = datetime.strptime(start_date_str, '%Y-%m-%d') + query = query.filter(Event.event_date >= start_date) + except ValueError: + pass + + if end_date_str: + try: + end_date = datetime.strptime(end_date_str, '%Y-%m-%d') + # 设置为当天结束 + end_date = end_date.replace(hour=23, minute=59, second=59) + query = query.filter(Event.event_date <= end_date) + except ValueError: + pass + + # 去重并排序 + query = query.distinct().order_by(Event.event_date.desc()) + + # 限制数量 + if limit: + query = query.limit(limit) + + events = query.all() + + # 构建返回数据 + events_data = [] + for event in events: + # 获取该事件关联的股票信息(在请求的股票列表中的) + related_stocks_in_list = [ + { + 'stock_code': rs.stock_code, + 'stock_name': rs.stock_name, + 'sector': rs.sector + } + for rs in event.related_stocks + if rs.stock_code in stock_codes + ] + + events_data.append({ + 'id': event.id, + 'title': event.title, + 'content': event.content, + 'event_date': event.event_date.isoformat() if event.event_date else None, + 'published_time': event.event_date.strftime('%Y-%m-%d %H:%M:%S') if event.event_date else None, + 'source': 'event', # 标记来源为事件系统 + 'importance': event.importance, + 'view_count': event.view_count, + 'like_count': event.like_count, + 'related_stocks': related_stocks_in_list, + 'cover_image': event.cover_image, + 'created_at': event.created_at.isoformat() if event.created_at else None + }) + + return jsonify({ + 'success': True, + 'data': events_data, + 'total': len(events_data) + }) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/events//concepts', methods=['GET']) def get_related_concepts(event_id): """获取相关概念列表""" diff --git a/src/views/Concept/ConceptTimelineModal.js b/src/views/Concept/ConceptTimelineModal.js index 63e8c0a4..ca8df932 100644 --- a/src/views/Concept/ConceptTimelineModal.js +++ b/src/views/Concept/ConceptTimelineModal.js @@ -78,11 +78,17 @@ const REPORT_API_URL = process.env.NODE_ENV === 'production' ? '/report-api' : 'http://111.198.58.126:8811'; +// 主应用后端 API 基础 URL(用于获取股票关联的事件/新闻) +const MAIN_API_URL = process.env.NODE_ENV === 'production' + ? '' + : 'http://111.198.58.126:5001'; + const ConceptTimelineModal = ({ isOpen, onClose, conceptName, - conceptId + conceptId, + stocks = [] }) => { const toast = useToast(); @@ -334,61 +340,66 @@ const ConceptTimelineModal = ({ }) ); - // 获取新闻(精确匹配,最近100天,最多100条) - // 🔄 添加回退逻辑:如果结果不足30条,去掉 exact_match 参数重新搜索 + // 获取新闻(通过股票代码聚合关联事件) + // 新逻辑:每个概念有关联的股票,通过 related_stock 表聚合所有股票的关联新闻/事件 const fetchNews = async () => { try { - // 第一次尝试:使用精确匹配 - const newsParams = new URLSearchParams({ - query: conceptName, - exact_match: 1, - start_date: startDateStr, - end_date: endDateStr, - top_k: 100 - }); + // 提取股票代码列表 + const stockCodes = (stocks || []) + .map(s => s.code || s.stock_code) + .filter(Boolean); - const newsUrl = `${NEWS_API_URL}/search_china_news?${newsParams}`; - const res = await fetch(newsUrl); - - if (!res.ok) { - const text = await res.text(); - logger.error('ConceptTimelineModal', 'fetchTimelineData - News API (exact_match=1)', new Error(`HTTP ${res.status}`), { conceptName, status: res.status, response: text.substring(0, 200) }); + if (stockCodes.length === 0) { + logger.warn('ConceptTimelineModal', '概念没有关联股票,无法获取新闻', { conceptName }); return []; } - const newsResult = await res.json(); - const newsArray = Array.isArray(newsResult) ? newsResult : []; + logger.info('ConceptTimelineModal', `通过 ${stockCodes.length} 只股票获取关联新闻`, { conceptName, stockCodes: stockCodes.slice(0, 5) }); - // 检查结果数量,如果不足30条则进行回退搜索 - if (newsArray.length < 30) { - logger.info('ConceptTimelineModal', `新闻精确搜索结果不足30条 (${newsArray.length}),尝试模糊搜索`, { conceptName }); - - // 第二次尝试:去掉精确匹配参数 - const fallbackParams = new URLSearchParams({ - query: conceptName, + // 调用后端新 API 获取股票关联的事件 + const res = await fetch(`${MAIN_API_URL}/api/events/by-stocks`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + stock_codes: stockCodes, start_date: startDateStr, end_date: endDateStr, - top_k: 100 - }); + limit: 200 + }), + credentials: 'include' + }); - const fallbackUrl = `${NEWS_API_URL}/search_china_news?${fallbackParams}`; - const fallbackRes = await fetch(fallbackUrl); - - if (!fallbackRes.ok) { - logger.warn('ConceptTimelineModal', '新闻模糊搜索失败,使用精确搜索结果', { conceptName }); - return newsArray; - } - - const fallbackResult = await fallbackRes.json(); - const fallbackArray = Array.isArray(fallbackResult) ? fallbackResult : []; - - logger.info('ConceptTimelineModal', `新闻模糊搜索成功,获取 ${fallbackArray.length} 条结果`, { conceptName }); - return fallbackArray; + if (!res.ok) { + const text = await res.text(); + logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Stocks API', new Error(`HTTP ${res.status}`), { conceptName, status: res.status, response: text.substring(0, 200) }); + return []; } + const result = await res.json(); + + if (!result.success) { + logger.warn('ConceptTimelineModal', '获取股票关联事件失败', { conceptName, error: result.error }); + return []; + } + + // 转换为新闻格式 + const events = result.data || []; + const newsArray = events.map(event => ({ + title: event.title, + detail: event.description || event.summary || '', + description: event.description || event.summary || '', + published_time: event.event_date || event.created_at, + source: 'event', // 标记为事件来源 + url: null, // 事件没有外链 + related_stocks: event.related_stocks || [] // 保留关联股票信息 + })); + + logger.info('ConceptTimelineModal', `获取到 ${newsArray.length} 条股票关联事件`, { conceptName }); return newsArray; } catch (err) { - logger.error('ConceptTimelineModal', 'fetchTimelineData - News API', err, { conceptName }); + logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Stocks API', err, { conceptName }); return []; } }; @@ -1376,10 +1387,10 @@ const ConceptTimelineModal = ({ {selectedNews?.source && ( - {selectedNews.source === 'zsxq' ? '知识星球' : selectedNews.source} + {selectedNews.source === 'zsxq' ? '知识星球' : selectedNews.source === 'event' ? '关联事件' : selectedNews.source} )} {selectedNews?.time && ( @@ -1411,8 +1422,8 @@ const ConceptTimelineModal = ({ - {/* zsxq来源不显示查看原文按钮 */} - {selectedNews?.url && selectedNews?.source !== 'zsxq' && ( + {/* zsxq和event来源不显示查看原文按钮 */} + {selectedNews?.url && selectedNews?.source !== 'zsxq' && selectedNews?.source !== 'event' && (