滚动条样式更新
This commit is contained in:
105
app.py
105
app.py
@@ -7261,6 +7261,111 @@ def get_events_by_stocks():
|
|||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
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/<int:event_id>/concepts', methods=['GET'])
|
@app.route('/api/events/<int:event_id>/concepts', methods=['GET'])
|
||||||
def get_related_concepts(event_id):
|
def get_related_concepts(event_id):
|
||||||
"""获取相关概念列表(AI分析结果)"""
|
"""获取相关概念列表(AI分析结果)"""
|
||||||
|
|||||||
@@ -392,50 +392,25 @@ const ConceptTimelineModal = ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取新闻(通过股票代码聚合关联事件)
|
// 获取新闻(通过概念名称直接查询关联事件)
|
||||||
// 新逻辑:每个概念有关联的股票,通过 related_stock 表聚合所有股票的关联新闻/事件
|
// 新逻辑:直接通过 related_concepts 表查询,无需先获取股票列表
|
||||||
const fetchNews = async () => {
|
const fetchNews = async () => {
|
||||||
try {
|
try {
|
||||||
// 提取股票代码列表
|
if (!conceptName) {
|
||||||
let stockCodes = (stocks || [])
|
logger.warn('ConceptTimelineModal', '缺少概念名称,无法获取新闻');
|
||||||
.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 });
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('ConceptTimelineModal', `通过 ${stockCodes.length} 只股票获取关联新闻`, { conceptName, stockCodes: stockCodes.slice(0, 5) });
|
logger.info('ConceptTimelineModal', `通过概念名称获取关联事件`, { conceptName });
|
||||||
|
|
||||||
// 调用后端新 API 获取股票关联的事件
|
// 调用后端 API 通过概念名称直接获取关联事件
|
||||||
const res = await fetch(`${MAIN_API_URL}/api/events/by-stocks`, {
|
const res = await fetch(`${MAIN_API_URL}/api/events/by-concept`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
stock_codes: stockCodes,
|
concept_name: conceptName,
|
||||||
start_date: startDateStr,
|
start_date: startDateStr,
|
||||||
end_date: endDateStr,
|
end_date: endDateStr,
|
||||||
limit: 200
|
limit: 200
|
||||||
@@ -445,14 +420,14 @@ const ConceptTimelineModal = ({
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text();
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
logger.warn('ConceptTimelineModal', '获取股票关联事件失败', { conceptName, error: result.error });
|
logger.warn('ConceptTimelineModal', '获取概念关联事件失败', { conceptName, error: result.error });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,13 +440,14 @@ const ConceptTimelineModal = ({
|
|||||||
published_time: event.event_date || event.created_at,
|
published_time: event.event_date || event.created_at,
|
||||||
source: 'event', // 标记为事件来源
|
source: 'event', // 标记为事件来源
|
||||||
url: null, // 事件没有外链
|
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;
|
return newsArray;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Stocks API', err, { conceptName });
|
logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Concept API', err, { conceptName });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user