From 38b510c681179d0e2885343a6a8a581a5107d86e Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Wed, 24 Dec 2025 14:18:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E6=9D=A1=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 105 ++++++++++++++++++++++ src/views/Concept/ConceptTimelineModal.js | 52 +++-------- 2 files changed, 119 insertions(+), 38 deletions(-) diff --git a/app.py b/app.py index 76bf3027..648697fd 100755 --- a/app.py +++ b/app.py @@ -7261,6 +7261,111 @@ def get_events_by_stocks(): return jsonify({'success': False, 'error': str(e)}), 500 +@app.route('/api/events/by-concept', methods=['POST']) +def get_events_by_concept(): + """ + 通过概念名称获取关联的事件 + 用于概念中心时间轴:直接通过 related_concepts 表查询,无需先获取股票列表 + + 请求体: + { + "concept_name": "人工智能", # 概念名称 + "start_date": "2024-01-01", # 可选,开始日期 + "end_date": "2024-12-31", # 可选,结束日期 + "limit": 200 # 可选,限制返回数量,默认200 + } + """ + try: + data = request.get_json() + concept_name = data.get('concept_name', '').strip() + start_date_str = data.get('start_date') + end_date_str = data.get('end_date') + limit = data.get('limit', 200) + + if not concept_name: + return jsonify({'success': False, 'error': '缺少概念名称'}), 400 + + # 通过 RelatedConcepts 表查询关联的事件 + query = db.session.query(Event).join( + RelatedConcepts, Event.id == RelatedConcepts.event_id + ).filter( + RelatedConcepts.concept == concept_name + ) + + # 日期过滤 + if start_date_str: + try: + start_date = datetime.strptime(start_date_str, '%Y-%m-%d') + query = query.filter(Event.start_time >= 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.start_time <= end_date) + except ValueError: + pass + + # 去重并排序 + query = query.distinct().order_by(Event.start_time.desc()) + + # 限制数量 + if limit: + query = query.limit(limit) + + events = query.all() + + # 构建返回数据 + events_data = [] + for event in events: + # 获取该事件关联的所有股票信息 + related_stocks_list = [ + { + 'stock_code': rs.stock_code, + 'stock_name': rs.stock_name, + 'sector': rs.sector + } + for rs in event.related_stocks + ] + + # 获取该事件关联的所有概念(包括关联原因) + related_concepts_list = [ + { + 'concept': rc.concept, + 'reason': rc.reason + } + for rc in event.related_concepts + ] + + events_data.append({ + 'id': event.id, + 'title': event.title, + 'description': event.description, + 'event_date': event.start_time.isoformat() if event.start_time else None, + 'published_time': event.start_time.strftime('%Y-%m-%d %H:%M:%S') if event.start_time else None, + 'source': 'event', + 'importance': event.importance, + 'view_count': event.view_count, + 'hot_score': event.hot_score, + 'related_stocks': related_stocks_list, + 'related_concepts': related_concepts_list, + 'event_type': event.event_type, + '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: + app.logger.error(f'[by-concept] 查询失败: {str(e)}') + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/events//concepts', methods=['GET']) def get_related_concepts(event_id): """获取相关概念列表(AI分析结果)""" diff --git a/src/views/Concept/ConceptTimelineModal.js b/src/views/Concept/ConceptTimelineModal.js index 48f8afb6..4b5df638 100644 --- a/src/views/Concept/ConceptTimelineModal.js +++ b/src/views/Concept/ConceptTimelineModal.js @@ -392,50 +392,25 @@ const ConceptTimelineModal = ({ }) ); - // 获取新闻(通过股票代码聚合关联事件) - // 新逻辑:每个概念有关联的股票,通过 related_stock 表聚合所有股票的关联新闻/事件 + // 获取新闻(通过概念名称直接查询关联事件) + // 新逻辑:直接通过 related_concepts 表查询,无需先获取股票列表 const fetchNews = async () => { try { - // 提取股票代码列表 - let stockCodes = (stocks || []) - .map(s => s.code || s.stock_code) - .filter(Boolean); - - // 如果 stocks 为空但有 conceptId,尝试从概念详情接口获取股票列表 - if (stockCodes.length === 0 && conceptId) { - logger.info('ConceptTimelineModal', '股票列表为空,尝试从概念详情接口获取', { conceptId, conceptName }); - try { - const detailRes = await fetch(`${API_BASE_URL}/concept/${encodeURIComponent(conceptId)}`); - if (detailRes.ok) { - const detailData = await detailRes.json(); - const detailStocks = detailData.stocks || []; - stockCodes = detailStocks - .map(s => s.code || s.stock_code) - .filter(Boolean); - logger.info('ConceptTimelineModal', `从概念详情获取到 ${stockCodes.length} 只股票`, { conceptId }); - } else { - logger.warn('ConceptTimelineModal', '获取概念详情失败', { conceptId, status: detailRes.status }); - } - } catch (detailErr) { - logger.error('ConceptTimelineModal', '获取概念详情异常', detailErr, { conceptId }); - } - } - - if (stockCodes.length === 0) { - logger.warn('ConceptTimelineModal', '概念没有关联股票,无法获取新闻', { conceptName }); + if (!conceptName) { + logger.warn('ConceptTimelineModal', '缺少概念名称,无法获取新闻'); return []; } - logger.info('ConceptTimelineModal', `通过 ${stockCodes.length} 只股票获取关联新闻`, { conceptName, stockCodes: stockCodes.slice(0, 5) }); + logger.info('ConceptTimelineModal', `通过概念名称获取关联事件`, { conceptName }); - // 调用后端新 API 获取股票关联的事件 - const res = await fetch(`${MAIN_API_URL}/api/events/by-stocks`, { + // 调用后端 API 通过概念名称直接获取关联事件 + const res = await fetch(`${MAIN_API_URL}/api/events/by-concept`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - stock_codes: stockCodes, + concept_name: conceptName, start_date: startDateStr, end_date: endDateStr, limit: 200 @@ -445,14 +420,14 @@ const ConceptTimelineModal = ({ 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) }); + logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Concept 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 }); + logger.warn('ConceptTimelineModal', '获取概念关联事件失败', { conceptName, error: result.error }); return []; } @@ -465,13 +440,14 @@ const ConceptTimelineModal = ({ published_time: event.event_date || event.created_at, source: 'event', // 标记为事件来源 url: null, // 事件没有外链 - related_stocks: event.related_stocks || [] // 保留关联股票信息 + related_stocks: event.related_stocks || [], // 保留关联股票信息 + related_concepts: event.related_concepts || [] // 保留关联概念信息 })); - logger.info('ConceptTimelineModal', `获取到 ${newsArray.length} 条股票关联事件`, { conceptName }); + logger.info('ConceptTimelineModal', `获取到 ${newsArray.length} 条概念关联事件`, { conceptName }); return newsArray; } catch (err) { - logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Stocks API', err, { conceptName }); + logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Concept API', err, { conceptName }); return []; } };