diff --git a/app.py b/app.py index 0639dda8..a83d489f 100755 --- a/app.py +++ b/app.py @@ -5555,11 +5555,41 @@ def get_events_by_stocks(): if not stock_codes: return jsonify({'success': False, 'error': '缺少股票代码列表'}), 400 + # 转换股票代码格式:概念API返回的是不带后缀的(如600000), + # 但related_stock表中存储的是带后缀的(如600000.SH) + def normalize_stock_code(code): + """将股票代码标准化为带后缀的格式""" + if not code: + return code + # 如果已经带后缀,直接返回 + if '.' in str(code): + return code + code = str(code).strip() + # 根据代码前缀判断交易所 + if code.startswith('6'): + return f"{code}.SH" # 上海 + elif code.startswith('0') or code.startswith('3'): + return f"{code}.SZ" # 深圳 + elif code.startswith('8') or code.startswith('4'): + return f"{code}.BJ" # 北交所 + else: + return code # 未知格式,保持原样 + + # 同时包含带后缀和不带后缀的版本,提高匹配率 + normalized_codes = set() + for code in stock_codes: + if code: + normalized_codes.add(str(code)) # 原始格式 + normalized_codes.add(normalize_stock_code(code)) # 带后缀格式 + # 如果原始带后缀,也加入不带后缀的版本 + if '.' in str(code): + normalized_codes.add(str(code).split('.')[0]) + # 构建查询:通过 RelatedStock 表找到关联的事件 query = db.session.query(Event).join( RelatedStock, Event.id == RelatedStock.event_id ).filter( - RelatedStock.stock_code.in_(stock_codes) + RelatedStock.stock_code.in_(list(normalized_codes)) ) # 日期过滤(使用 start_time 字段) @@ -12507,57 +12537,69 @@ def get_market_statistics(): @app.route('/api/concepts/daily-top', methods=['GET']) def get_daily_top_concepts(): - """获取每日涨幅靠前的概念板块 - - 修复:使用 /price/list 接口获取全部概念的正确排序(原 /search 接口只取前500个) - """ + """获取每日涨幅靠前的概念板块""" try: # 获取交易日期参数 trade_date = request.args.get('date') limit = request.args.get('limit', 6, type=int) - # 使用 /price/list 接口获取正确排序的涨幅数据 - concept_api_url = 'http://222.128.1.157:16801/price/list' + # 构建概念中心API的URL + concept_api_url = 'http://222.128.1.157:16801/search' - params = { - 'sort_by': 'change_desc', - 'limit': limit, - 'offset': 0, - 'concept_type': 'leaf' # 只获取叶子概念 + # 准备请求数据 + request_data = { + 'query': '', + 'size': limit, + 'page': 1, + 'sort_by': 'change_pct' } - if trade_date: - params['trade_date'] = trade_date - response = requests.get(concept_api_url, params=params, timeout=10) + if trade_date: + request_data['trade_date'] = trade_date + + # 调用概念中心API + response = requests.post(concept_api_url, json=request_data, timeout=10) if response.status_code == 200: data = response.json() top_concepts = [] - for concept in data.get('concepts', []): - # /price/list 返回的字段较少,适配为前端需要的格式 + for concept in data.get('results', []): + # 处理 stocks 字段:兼容 {name, code} 和 {stock_name, stock_code} 两种格式 + raw_stocks = concept.get('stocks', []) + formatted_stocks = [] + for stock in raw_stocks: + # 优先使用 stock_name,其次使用 name + stock_name = stock.get('stock_name') or stock.get('name', '') + stock_code = stock.get('stock_code') or stock.get('code', '') + formatted_stocks.append({ + 'stock_name': stock_name, + 'stock_code': stock_code, + 'name': stock_name, # 兼容旧格式 + 'code': stock_code # 兼容旧格式 + }) + + # 保持与 /concept-api/search 相同的字段结构,并添加新字段 top_concepts.append({ 'concept_id': concept.get('concept_id'), - 'concept': concept.get('concept_name'), - 'concept_name': concept.get('concept_name'), - 'description': None, # /price/list 不返回此字段 + 'concept': concept.get('concept'), # 原始字段名 + 'concept_name': concept.get('concept'), # 兼容旧字段名 + 'description': concept.get('description'), 'stock_count': concept.get('stock_count', 0), - 'score': None, - 'match_type': None, - 'price_info': { - 'avg_change_pct': concept.get('avg_change_pct') - }, - 'change_percent': concept.get('avg_change_pct', 0), - 'tags': [], # /price/list 不返回此字段 - 'outbreak_dates': [], # /price/list 不返回此字段 - 'hierarchy': concept.get('hierarchy'), - 'stocks': [], # /price/list 不返回此字段 - 'hot_score': None + 'score': concept.get('score'), + 'match_type': concept.get('match_type'), + 'price_info': concept.get('price_info', {}), # 完整的价格信息 + 'change_percent': concept.get('price_info', {}).get('avg_change_pct', 0), # 兼容旧字段 + 'tags': concept.get('tags', []), # 标签列表 + 'outbreak_dates': concept.get('outbreak_dates', []), # 爆发日期列表 + 'hierarchy': concept.get('hierarchy'), # 层级信息 {lv1, lv2, lv3} + 'stocks': formatted_stocks, # 返回格式化后的股票列表 + 'hot_score': concept.get('hot_score') }) # 格式化日期为 YYYY-MM-DD - price_date = data.get('trade_date', '') - formatted_date = str(price_date)[:10] if price_date else '' + price_date = data.get('price_date', '') + formatted_date = str(price_date).split(' ')[0][:10] if price_date else '' return jsonify({ 'success': True, diff --git a/concept_api_v2.py b/concept_api_v2.py index 9e8ed2e9..5c282ab2 100644 --- a/concept_api_v2.py +++ b/concept_api_v2.py @@ -469,9 +469,11 @@ async def search_concepts(request: SearchRequest): semantic_weight = 0 # 确定搜索数量 + # 修复:当按涨跌幅排序时,需要获取全部概念(800+)再排序 + # 原来限制 500 会导致排序不准确 effective_search_size = request.search_size if request.sort_by in ["change_pct", "outbreak_date"]: - effective_search_size = min(500, request.search_size * 5) + effective_search_size = min(1000, request.search_size * 10) # 构建查询 search_body = {} diff --git a/src/views/Concept/index.js b/src/views/Concept/index.js index c7a22996..a6b24332 100644 --- a/src/views/Concept/index.js +++ b/src/views/Concept/index.js @@ -449,102 +449,48 @@ const ConceptCenter = () => { setLoading(true); try { const sortToUse = customSortBy !== null ? customSortBy : sortBy; - const dateStr = date ? date.toISOString().split('T')[0] : null; - // 判断是否使用 /price/list 接口 - // 条件:无搜索词 + 按涨跌幅排序(升序或降序)+ 无层级筛选 - const isChangePctSort = sortToUse === 'change_pct' || sortToUse === 'change_pct_asc'; - const hasNoFilter = !filter?.lv1 && !filter?.lv2; - const shouldUsePriceList = !query && isChangePctSort && hasNoFilter; + const requestBody = { + query: query, + size: pageSize, + page: page, + sort_by: sortToUse + }; - if (shouldUsePriceList) { - // 使用 /price/list 接口获取全部概念的正确排序 - const sortParam = sortToUse === 'change_pct_asc' ? 'change_asc' : 'change_desc'; - const offset = (page - 1) * pageSize; - const params = new URLSearchParams({ - sort_by: sortParam, - limit: pageSize.toString(), - offset: offset.toString(), - concept_type: 'leaf' // 只获取叶子概念 - }); - if (dateStr) { - params.append('trade_date', dateStr); - } + if (date) { + requestBody.trade_date = date.toISOString().split('T')[0]; + } - const response = await fetch(`${API_BASE_URL}/price/list?${params}`); - if (!response.ok) throw new Error('获取涨跌幅数据失败'); + // 添加层级筛选参数 + if (filter?.lv1) { + requestBody.filter_lv1 = filter.lv1; + } + if (filter?.lv2) { + requestBody.filter_lv2 = filter.lv2; + } - const data = await response.json(); + const response = await fetch(`${API_BASE_URL}/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); - // 适配 /price/list 返回数据为 /search 格式 - const adaptedResults = data.concepts.map(item => ({ - concept_id: item.concept_id, - concept: item.concept_name, - concept_name: item.concept_name, - price_info: { - avg_change_pct: item.avg_change_pct - }, - stock_count: item.stock_count, - hierarchy: item.hierarchy, - // 以下字段 /price/list 不返回,填入默认值 - description: null, - tags: [], - outbreak_dates: [], - stocks: [] - })); + if (!response.ok) throw new Error('搜索失败'); - setConcepts(adaptedResults); - setTotalConcepts(data.total || 0); - setTotalPages(Math.ceil((data.total || 0) / pageSize)); - setCurrentPage(page); + const data = await response.json(); - if (data.trade_date) { - setSelectedDate(new Date(data.trade_date)); - } - } else { - // 使用原有的 /search 接口 - const requestBody = { - query: query, - size: pageSize, - page: page, - sort_by: sortToUse - }; + setConcepts(data.results || []); + setTotalConcepts(data.total || 0); + setTotalPages(data.total_pages || 1); + setCurrentPage(data.page || 1); - if (dateStr) { - requestBody.trade_date = dateStr; - } - - // 添加层级筛选参数 - if (filter?.lv1) { - requestBody.filter_lv1 = filter.lv1; - } - if (filter?.lv2) { - requestBody.filter_lv2 = filter.lv2; - } - - const response = await fetch(`${API_BASE_URL}/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) throw new Error('搜索失败'); - - const data = await response.json(); - - setConcepts(data.results || []); - setTotalConcepts(data.total || 0); - setTotalPages(data.total_pages || 1); - setCurrentPage(data.page || 1); - - if (data.price_date) { - setSelectedDate(new Date(data.price_date)); - } + if (data.price_date) { + setSelectedDate(new Date(data.price_date)); } } catch (error) { - logger.error('ConceptCenter', 'fetchConcepts', error, { query, page, date: date?.toISOString(), customSortBy, filter }); + logger.error('ConceptCenter', 'fetchConcepts', error, { query, page, date: date?.toISOString(), sortToUse, filter }); // ❌ 移除获取数据失败toast // toast({ title: '获取数据失败', description: error.message, status: 'error', duration: 3000, isClosable: true });