diff --git a/concept_api_v2.py b/concept_api_v2.py
new file mode 100644
index 00000000..896f5d5e
--- /dev/null
+++ b/concept_api_v2.py
@@ -0,0 +1,1667 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+概念搜索API V2 - 适配 concept_library_v3 索引
+新特性:
+1. 支持层级结构(lv1/lv2/lv3)
+2. 返回 tags, outbreak_dates, insight 等新字段
+3. 新增层级浏览接口
+"""
+import json
+import openai
+from typing import List, Dict, Optional, Union, Any
+from fastapi import FastAPI, HTTPException, Query
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel, Field
+from elasticsearch import Elasticsearch
+from datetime import datetime, date
+import logging
+import re
+from contextlib import asynccontextmanager
+import aiomysql
+import os
+
+# 配置日志
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# ==================== 配置 ====================
+
+# ES配置
+ES_HOST = 'http://127.0.0.1:9200'
+INDEX_NAME = 'concept_library_v3'
+
+# Embedding配置
+OPENAI_BASE_URL = "http://127.0.0.1:8000/v1"
+OPENAI_API_KEY = "dummy"
+EMBEDDING_MODEL = "qwen3-embedding-8b"
+
+# 层级结构文件
+HIERARCHY_FILE = 'concept_hierarchy_v3.json'
+
+# MySQL配置
+MYSQL_CONFIG = {
+ 'host': '192.168.1.8',
+ 'port': 3306,
+ 'user': 'root',
+ 'password': 'Zzl5588161!',
+ 'db': 'stock',
+ 'charset': 'utf8mb4',
+ 'autocommit': True,
+ 'minsize': 1,
+ 'maxsize': 10
+}
+
+# ==================== 全局变量 ====================
+
+es_client = None
+openai_client = None
+mysql_pool = None
+
+# 层级结构相关
+hierarchy_data = {} # 原始层级数据
+concept_to_hierarchy = {} # 概念名称 -> 层级信息的映射
+
+
+def load_hierarchy():
+ """加载层级结构并建立概念到层级的映射"""
+ global hierarchy_data, concept_to_hierarchy
+
+ hierarchy_path = os.path.join(os.path.dirname(__file__), HIERARCHY_FILE)
+ if not os.path.exists(hierarchy_path):
+ logger.warning(f"层级文件不存在: {hierarchy_path}")
+ return
+
+ try:
+ with open(hierarchy_path, 'r', encoding='utf-8') as f:
+ hierarchy_data = json.load(f)
+
+ # 遍历层级结构,建立概念到层级的映射
+ for lv1 in hierarchy_data.get('hierarchy', []):
+ lv1_name = lv1.get('lv1', '')
+ lv1_id = lv1.get('lv1_id', '')
+
+ for child in lv1.get('children', []):
+ lv2_name = child.get('lv2', '')
+ lv2_id = child.get('lv2_id', '')
+
+ # 检查是否有 lv3 子级
+ if 'children' in child:
+ for lv3_child in child.get('children', []):
+ lv3_name = lv3_child.get('lv3', '')
+ lv3_id = lv3_child.get('lv3_id', '')
+
+ # lv3 级别的概念
+ for concept in lv3_child.get('concepts', []):
+ concept_to_hierarchy[concept] = {
+ 'lv1': lv1_name,
+ 'lv1_id': lv1_id,
+ 'lv2': lv2_name,
+ 'lv2_id': lv2_id,
+ 'lv3': lv3_name,
+ 'lv3_id': lv3_id
+ }
+ else:
+ # lv2 级别直接有概念(没有 lv3)
+ for concept in child.get('concepts', []):
+ concept_to_hierarchy[concept] = {
+ 'lv1': lv1_name,
+ 'lv1_id': lv1_id,
+ 'lv2': lv2_name,
+ 'lv2_id': lv2_id,
+ 'lv3': None,
+ 'lv3_id': None
+ }
+
+ logger.info(f"加载层级结构完成,共 {len(concept_to_hierarchy)} 个概念有层级信息")
+
+ except Exception as e:
+ logger.error(f"加载层级结构失败: {e}")
+
+
+def get_concept_hierarchy(concept_name: str) -> Optional[Dict]:
+ """获取概念的层级信息"""
+ return concept_to_hierarchy.get(concept_name)
+
+
+# ==================== 生命周期管理 ====================
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ global es_client, openai_client, mysql_pool
+
+ # 加载层级结构
+ load_hierarchy()
+
+ # 初始化ES客户端
+ es_client = Elasticsearch(
+ [ES_HOST],
+ timeout=30,
+ max_retries=3,
+ retry_on_timeout=True
+ )
+ logger.info(f"Connected to Elasticsearch at {ES_HOST}, index: {INDEX_NAME}")
+
+ # 初始化OpenAI客户端
+ openai_client = openai.OpenAI(
+ api_key=OPENAI_API_KEY,
+ base_url=OPENAI_BASE_URL,
+ timeout=60,
+ )
+ logger.info(f"Initialized OpenAI client")
+
+ # 初始化MySQL连接池
+ try:
+ mysql_pool = await aiomysql.create_pool(**MYSQL_CONFIG)
+ logger.info(f"Connected to MySQL at {MYSQL_CONFIG['host']}")
+ except Exception as e:
+ logger.error(f"Failed to connect to MySQL: {e}")
+
+ yield
+
+ # 清理资源
+ if es_client:
+ es_client.close()
+ if mysql_pool:
+ mysql_pool.close()
+ await mysql_pool.wait_closed()
+ logger.info("Cleanup completed")
+
+
+# ==================== FastAPI 应用 ====================
+
+app = FastAPI(
+ title="概念搜索API V2",
+ description="支持层级结构的概念库搜索API,适配 concept_library_v3 索引",
+ version="2.0.0",
+ lifespan=lifespan
+)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+# ==================== 数据模型 ====================
+
+class HierarchyInfo(BaseModel):
+ """层级信息"""
+ lv1: Optional[str] = Field(None, description="一级分类")
+ lv1_id: Optional[str] = None
+ lv2: Optional[str] = Field(None, description="二级分类")
+ lv2_id: Optional[str] = None
+ lv3: Optional[str] = Field(None, description="三级分类")
+ lv3_id: Optional[str] = None
+
+
+class StockInfo(BaseModel):
+ """股票信息"""
+ name: str = Field(..., description="股票名称")
+ code: Optional[str] = Field(None, description="股票代码")
+
+
+class ConceptPriceInfo(BaseModel):
+ """概念涨跌幅信息"""
+ trade_date: date
+ avg_change_pct: Optional[float] = Field(None, description="平均涨跌幅(%)")
+
+
+class SearchRequest(BaseModel):
+ """搜索请求"""
+ query: str = Field(..., description="搜索查询文本")
+ size: int = Field(10, ge=1, le=100, description="每页返回结果数量")
+ page: int = Field(1, ge=1, description="页码")
+ search_size: int = Field(100, ge=10, le=1000, description="搜索数量")
+ semantic_weight: Optional[float] = Field(None, ge=0.0, le=1.0, description="语义搜索权重")
+ filter_stocks: Optional[List[str]] = Field(None, description="过滤特定股票")
+ filter_lv1: Optional[str] = Field(None, description="过滤一级分类")
+ filter_lv2: Optional[str] = Field(None, description="过滤二级分类")
+ trade_date: Optional[date] = Field(None, description="交易日期")
+ sort_by: str = Field("_score", description="排序: _score, change_pct, stock_count, outbreak_date")
+ use_knn: bool = Field(True, description="是否使用KNN搜索")
+
+
+class ConceptResult(BaseModel):
+ """概念搜索结果"""
+ concept_id: str
+ concept: str
+ description: Optional[str] = None
+ tags: Optional[List[str]] = Field(None, description="标签")
+ outbreak_dates: Optional[List[str]] = Field(None, description="爆发日期")
+ stocks: List[StockInfo] = Field(default_factory=list)
+ stock_count: int = 0
+ hierarchy: Optional[HierarchyInfo] = Field(None, description="层级信息")
+ score: float = 0.0
+ match_type: str = "keyword"
+ highlights: Optional[Dict[str, List[str]]] = None
+ price_info: Optional[ConceptPriceInfo] = None
+
+
+class SearchResponse(BaseModel):
+ """搜索响应"""
+ total: int
+ took_ms: int
+ results: List[ConceptResult]
+ search_info: Dict[str, Any]
+ price_date: Optional[date] = None
+ page: int = 1
+ total_pages: int = 1
+
+
+class HierarchyLevel(BaseModel):
+ """层级节点"""
+ id: str
+ name: str
+ concept_count: int = 0
+ children: Optional[List['HierarchyLevel']] = None
+ concepts: Optional[List[str]] = None
+
+
+class HierarchyResponse(BaseModel):
+ """层级结构响应"""
+ hierarchy: List[HierarchyLevel]
+ total_concepts: int
+
+
+class ConceptDetailResponse(BaseModel):
+ """概念详情响应"""
+ concept_id: str
+ concept: str
+ description: Optional[str] = None
+ insight: Optional[str] = None
+ tags: Optional[List[str]] = None
+ outbreak_dates: Optional[List[str]] = None
+ stocks: List[Dict[str, Any]] = Field(default_factory=list)
+ stock_count: int = 0
+ hierarchy: Optional[HierarchyInfo] = None
+ folders: Optional[List[str]] = None
+ created_at: Optional[str] = None
+ price_info: Optional[ConceptPriceInfo] = None
+
+
+# ==================== 辅助函数 ====================
+
+def generate_embedding(text: str) -> List[float]:
+ """生成文本向量"""
+ try:
+ if not text or len(text.strip()) == 0:
+ return []
+
+ text = text[:8000] if len(text) > 8000 else text
+
+ if not openai_client:
+ return []
+
+ response = openai_client.embeddings.create(
+ model=EMBEDDING_MODEL,
+ input=[text]
+ )
+ return response.data[0].embedding
+
+ except Exception as e:
+ logger.warning(f"Embedding生成失败: {e}")
+ return []
+
+
+def calculate_semantic_weight(query: str) -> float:
+ """根据查询长度动态计算语义权重"""
+ query_length = len(query)
+
+ if query_length < 10:
+ return 0.3
+ elif query_length < 50:
+ return 0.5
+ elif query_length < 200:
+ return 0.6
+ else:
+ return 0.7
+
+
+async def get_concept_price_data(concept_ids: List[str], trade_date: Optional[date] = None) -> Dict[str, ConceptPriceInfo]:
+ """获取概念的涨跌幅数据"""
+ if not mysql_pool or not concept_ids:
+ return {}
+
+ try:
+ async with mysql_pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ placeholders = ','.join(['%s'] * len(concept_ids))
+
+ if trade_date:
+ query = f"""
+ SELECT concept_id, trade_date, avg_change_pct
+ FROM concept_daily_stats
+ WHERE concept_id IN ({placeholders}) AND trade_date = %s
+ """
+ await cursor.execute(query, (*concept_ids, trade_date))
+ else:
+ query = f"""
+ SELECT cds.concept_id, cds.trade_date, cds.avg_change_pct
+ FROM concept_daily_stats cds
+ INNER JOIN (
+ SELECT concept_id, MAX(trade_date) as max_date
+ FROM concept_daily_stats
+ WHERE concept_id IN ({placeholders})
+ GROUP BY concept_id
+ ) latest ON cds.concept_id = latest.concept_id
+ AND cds.trade_date = latest.max_date
+ """
+ await cursor.execute(query, concept_ids)
+
+ rows = await cursor.fetchall()
+
+ result = {}
+ for row in rows:
+ result[row['concept_id']] = ConceptPriceInfo(
+ trade_date=row['trade_date'],
+ avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] is not None else None
+ )
+ return result
+
+ except Exception as e:
+ logger.error(f"获取涨跌幅数据失败: {e}")
+ return {}
+
+
+def build_keyword_query(query: str, stock_filters: List[str] = None) -> Dict:
+ """构建关键词查询 - 适配 v3 索引"""
+ must_queries = []
+
+ if query.strip():
+ must_queries.append({
+ "multi_match": {
+ "query": query,
+ "fields": [
+ "concept^3",
+ "description^2",
+ "tags.text^2",
+ "tags^1.5"
+ ],
+ "type": "best_fields",
+ "analyzer": "ik_smart"
+ }
+ })
+
+ if stock_filters:
+ stock_query = {
+ "nested": {
+ "path": "stocks",
+ "query": {
+ "bool": {
+ "should": [
+ {"terms": {"stocks.name": stock_filters}},
+ {"terms": {"stocks.code": stock_filters}}
+ ]
+ }
+ }
+ }
+ }
+ must_queries.append(stock_query)
+
+ return {
+ "bool": {
+ "must": must_queries if must_queries else [{"match_all": {}}]
+ }
+ }
+
+
+def parse_stocks_from_es(source: Dict) -> tuple:
+ """从ES文档解析股票信息"""
+ # nested stocks 字段(只有 name, code)
+ stocks_nested = source.get('stocks', [])
+ stocks_list = []
+
+ for s in stocks_nested[:30]: # 限制返回数量
+ stocks_list.append(StockInfo(
+ name=s.get('name', ''),
+ code=s.get('code')
+ ))
+
+ # 完整股票信息从 stocks_json 解析
+ stocks_full = []
+ stocks_json = source.get('stocks_json', '')
+ if stocks_json:
+ try:
+ stocks_full = json.loads(stocks_json)
+ except:
+ pass
+
+ return stocks_list, stocks_full
+
+
+# ==================== API 端点 ====================
+
+@app.get("/", tags=["Health"])
+async def root():
+ """健康检查"""
+ return {
+ "status": "healthy",
+ "service": "概念搜索API V2",
+ "version": "2.0.0",
+ "index": INDEX_NAME,
+ "hierarchy_concepts": len(concept_to_hierarchy)
+ }
+
+
+@app.post("/search", response_model=SearchResponse, tags=["Search"])
+async def search_concepts(request: SearchRequest):
+ """
+ 搜索概念 - 支持语义搜索和层级过滤
+ """
+ start_time = datetime.now()
+
+ try:
+ # 计算语义权重
+ if request.semantic_weight is not None:
+ semantic_weight = request.semantic_weight
+ else:
+ semantic_weight = calculate_semantic_weight(request.query)
+
+ # 生成embedding
+ embedding = []
+ if semantic_weight > 0:
+ embedding = generate_embedding(request.query)
+ if not embedding:
+ semantic_weight = 0
+
+ # 确定搜索数量
+ effective_search_size = request.search_size
+ if request.sort_by in ["change_pct", "outbreak_date"]:
+ effective_search_size = min(500, request.search_size * 5)
+
+ # 构建查询
+ search_body = {}
+ match_type = "keyword"
+
+ if semantic_weight == 0:
+ # 纯关键词搜索
+ search_body = {
+ "query": build_keyword_query(request.query, request.filter_stocks),
+ "size": effective_search_size
+ }
+ match_type = "keyword"
+
+ elif request.use_knn and embedding:
+ # KNN + 关键词混合搜索
+ keyword_weight = 1.0 - semantic_weight
+
+ search_body = {
+ "knn": {
+ "field": "description_embedding",
+ "query_vector": embedding,
+ "k": effective_search_size,
+ "num_candidates": min(effective_search_size * 2, 10000),
+ "boost": semantic_weight
+ },
+ "query": {
+ "bool": {
+ "must": [build_keyword_query(request.query, request.filter_stocks)],
+ "boost": keyword_weight
+ }
+ },
+ "size": effective_search_size
+ }
+ match_type = "hybrid_knn"
+
+ else:
+ # 传统混合搜索
+ search_body = {
+ "query": build_keyword_query(request.query, request.filter_stocks),
+ "size": effective_search_size
+ }
+ match_type = "keyword"
+
+ # 添加高亮和源过滤
+ search_body.update({
+ "highlight": {
+ "fields": {
+ "concept": {},
+ "description": {"fragment_size": 150},
+ "tags": {}
+ }
+ },
+ "_source": {
+ "excludes": ["description_embedding", "insight"]
+ },
+ "track_total_hits": True
+ })
+
+ # 执行搜索
+ es_response = es_client.search(
+ index=INDEX_NAME,
+ body=search_body,
+ timeout="30s"
+ )
+
+ # 处理结果
+ all_results = []
+ concept_ids = []
+
+ for hit in es_response['hits']['hits']:
+ source = hit['_source']
+ concept_name = source.get('concept', '')
+ concept_id = source.get('concept_id', '')
+ concept_ids.append(concept_id)
+
+ # 解析股票
+ stocks_list, _ = parse_stocks_from_es(source)
+
+ # 获取层级信息
+ hierarchy_info = get_concept_hierarchy(concept_name)
+ hierarchy = None
+ if hierarchy_info:
+ hierarchy = HierarchyInfo(**hierarchy_info)
+
+ # 层级过滤
+ if request.filter_lv1 and (not hierarchy_info or hierarchy_info.get('lv1') != request.filter_lv1):
+ continue
+ if request.filter_lv2 and (not hierarchy_info or hierarchy_info.get('lv2') != request.filter_lv2):
+ continue
+
+ # 高亮信息
+ highlights = hit.get('highlight', {})
+
+ result = ConceptResult(
+ concept_id=concept_id,
+ concept=concept_name,
+ description=source.get('description'),
+ tags=source.get('tags', []),
+ outbreak_dates=source.get('outbreak_dates', []),
+ stocks=stocks_list,
+ stock_count=len(source.get('stocks', [])),
+ hierarchy=hierarchy,
+ score=hit['_score'] or 0,
+ match_type=match_type,
+ highlights=highlights,
+ price_info=None
+ )
+ all_results.append(result)
+
+ # 获取涨跌幅数据
+ price_data = {}
+ actual_price_date = None
+
+ if concept_ids:
+ price_data = await get_concept_price_data(concept_ids, request.trade_date)
+ if price_data:
+ actual_price_date = next(iter(price_data.values())).trade_date
+
+ for result in all_results:
+ result.price_info = price_data.get(result.concept_id)
+
+ # 排序
+ if request.sort_by == "change_pct":
+ all_results.sort(
+ key=lambda x: (
+ x.price_info.avg_change_pct
+ if x.price_info and x.price_info.avg_change_pct is not None
+ else -999
+ ),
+ reverse=True
+ )
+ elif request.sort_by == "stock_count":
+ all_results.sort(key=lambda x: x.stock_count, reverse=True)
+ elif request.sort_by == "outbreak_date":
+ all_results.sort(
+ key=lambda x: (
+ x.outbreak_dates[0] if x.outbreak_dates else '1900-01-01'
+ ),
+ reverse=True
+ )
+
+ # 分页
+ total_results = len(all_results)
+ total_pages = max(1, (total_results + request.size - 1) // request.size)
+ current_page = min(request.page, total_pages)
+
+ start_idx = (current_page - 1) * request.size
+ end_idx = start_idx + request.size
+ page_results = all_results[start_idx:end_idx]
+
+ took_ms = int((datetime.now() - start_time).total_seconds() * 1000)
+
+ return SearchResponse(
+ total=es_response['hits']['total']['value'],
+ took_ms=took_ms,
+ results=page_results,
+ search_info={
+ "query": request.query,
+ "semantic_weight": semantic_weight,
+ "match_type": match_type,
+ "filter_lv1": request.filter_lv1,
+ "filter_lv2": request.filter_lv2,
+ "sort_by": request.sort_by,
+ "has_embedding": bool(embedding)
+ },
+ price_date=actual_price_date,
+ page=current_page,
+ total_pages=total_pages
+ )
+
+ except Exception as e:
+ logger.error(f"搜索失败: {e}", exc_info=True)
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/concept/{concept_id}", response_model=ConceptDetailResponse, tags=["Concepts"])
+async def get_concept_detail(
+ concept_id: str,
+ trade_date: Optional[date] = Query(None, description="交易日期")
+):
+ """获取概念详情"""
+ try:
+ result = es_client.get(index=INDEX_NAME, id=concept_id)
+ source = result['_source']
+
+ concept_name = source.get('concept', '')
+
+ # 解析完整股票信息
+ _, stocks_full = parse_stocks_from_es(source)
+
+ # 获取层级信息
+ hierarchy_info = get_concept_hierarchy(concept_name)
+ hierarchy = HierarchyInfo(**hierarchy_info) if hierarchy_info else None
+
+ # 获取涨跌幅
+ price_data = await get_concept_price_data([concept_id], trade_date)
+ price_info = price_data.get(concept_id)
+
+ return ConceptDetailResponse(
+ concept_id=concept_id,
+ concept=concept_name,
+ description=source.get('description'),
+ insight=source.get('insight'),
+ tags=source.get('tags', []),
+ outbreak_dates=source.get('outbreak_dates', []),
+ stocks=stocks_full,
+ stock_count=len(stocks_full),
+ hierarchy=hierarchy,
+ folders=source.get('folders', []),
+ created_at=source.get('created_at'),
+ price_info=price_info
+ )
+
+ except Exception as e:
+ if "NotFoundError" in str(type(e)):
+ raise HTTPException(status_code=404, detail="概念不存在")
+ logger.error(f"获取概念详情失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/hierarchy", response_model=HierarchyResponse, tags=["Hierarchy"])
+async def get_hierarchy():
+ """获取完整层级结构"""
+ try:
+ result = []
+
+ for lv1 in hierarchy_data.get('hierarchy', []):
+ lv1_node = HierarchyLevel(
+ id=lv1.get('lv1_id', ''),
+ name=lv1.get('lv1', ''),
+ concept_count=0,
+ children=[]
+ )
+
+ for child in lv1.get('children', []):
+ lv2_node = HierarchyLevel(
+ id=child.get('lv2_id', ''),
+ name=child.get('lv2', ''),
+ concept_count=0,
+ children=[]
+ )
+
+ if 'children' in child:
+ # 有 lv3
+ for lv3_child in child.get('children', []):
+ concepts = lv3_child.get('concepts', [])
+ lv3_node = HierarchyLevel(
+ id=lv3_child.get('lv3_id', ''),
+ name=lv3_child.get('lv3', ''),
+ concept_count=len(concepts),
+ concepts=concepts
+ )
+ lv2_node.children.append(lv3_node)
+ lv2_node.concept_count += len(concepts)
+ else:
+ # lv2 直接有概念
+ concepts = child.get('concepts', [])
+ lv2_node.concept_count = len(concepts)
+ lv2_node.concepts = concepts
+
+ lv1_node.children.append(lv2_node)
+ lv1_node.concept_count += lv2_node.concept_count
+
+ result.append(lv1_node)
+
+ total_concepts = sum(lv1.concept_count for lv1 in result)
+
+ return HierarchyResponse(
+ hierarchy=result,
+ total_concepts=total_concepts
+ )
+
+ except Exception as e:
+ logger.error(f"获取层级结构失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/hierarchy/{lv1_id}", tags=["Hierarchy"])
+async def get_hierarchy_level(
+ lv1_id: str,
+ lv2_id: Optional[str] = Query(None, description="二级分类ID")
+):
+ """获取指定层级的概念列表"""
+ try:
+ for lv1 in hierarchy_data.get('hierarchy', []):
+ if lv1.get('lv1_id') == lv1_id:
+ if lv2_id:
+ # 返回指定 lv2 下的概念
+ for child in lv1.get('children', []):
+ if child.get('lv2_id') == lv2_id:
+ concepts = []
+ if 'children' in child:
+ for lv3_child in child.get('children', []):
+ concepts.extend(lv3_child.get('concepts', []))
+ else:
+ concepts = child.get('concepts', [])
+
+ return {
+ "lv1": lv1.get('lv1'),
+ "lv1_id": lv1_id,
+ "lv2": child.get('lv2'),
+ "lv2_id": lv2_id,
+ "concepts": concepts,
+ "concept_count": len(concepts)
+ }
+ raise HTTPException(status_code=404, detail=f"未找到二级分类: {lv2_id}")
+ else:
+ # 返回 lv1 下所有概念
+ concepts = []
+ for child in lv1.get('children', []):
+ if 'children' in child:
+ for lv3_child in child.get('children', []):
+ concepts.extend(lv3_child.get('concepts', []))
+ else:
+ concepts.extend(child.get('concepts', []))
+
+ return {
+ "lv1": lv1.get('lv1'),
+ "lv1_id": lv1_id,
+ "concepts": concepts,
+ "concept_count": len(concepts),
+ "children": lv1.get('children', [])
+ }
+
+ raise HTTPException(status_code=404, detail=f"未找到一级分类: {lv1_id}")
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"获取层级详情失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/stock/{stock_code}/concepts", tags=["Stocks"])
+async def get_stock_concepts(
+ stock_code: str,
+ size: int = Query(50, ge=1, le=200),
+ trade_date: Optional[date] = Query(None)
+):
+ """根据股票代码查询相关概念"""
+ try:
+ query = {
+ "nested": {
+ "path": "stocks",
+ "query": {
+ "bool": {
+ "should": [
+ {"term": {"stocks.code": stock_code}},
+ {"term": {"stocks.name": stock_code}}
+ ]
+ }
+ }
+ }
+ }
+
+ search_body = {
+ "query": query,
+ "size": size,
+ "_source": {
+ "excludes": ["description_embedding", "insight", "stocks_json"]
+ }
+ }
+
+ es_response = es_client.search(index=INDEX_NAME, body=search_body)
+
+ concept_ids = []
+ concepts = []
+
+ for hit in es_response['hits']['hits']:
+ source = hit['_source']
+ concept_name = source.get('concept', '')
+ concept_id = source.get('concept_id', '')
+ concept_ids.append(concept_id)
+
+ # 获取层级
+ hierarchy_info = get_concept_hierarchy(concept_name)
+
+ concepts.append({
+ "concept_id": concept_id,
+ "concept": concept_name,
+ "description": source.get('description'),
+ "tags": source.get('tags', []),
+ "stock_count": len(source.get('stocks', [])),
+ "hierarchy": hierarchy_info,
+ "outbreak_dates": source.get('outbreak_dates', [])
+ })
+
+ # 获取涨跌幅
+ price_data = await get_concept_price_data(concept_ids, trade_date)
+ for concept in concepts:
+ concept['price_info'] = price_data.get(concept['concept_id'])
+
+ return {
+ "stock_code": stock_code,
+ "total": es_response['hits']['total']['value'],
+ "concepts": concepts
+ }
+
+ except Exception as e:
+ logger.error(f"查询股票概念失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/statistics/hierarchy", tags=["Statistics"])
+async def get_hierarchy_statistics():
+ """获取层级统计信息"""
+ try:
+ stats = []
+
+ for lv1 in hierarchy_data.get('hierarchy', []):
+ lv1_stats = {
+ "lv1": lv1.get('lv1'),
+ "lv1_id": lv1.get('lv1_id'),
+ "lv2_count": len(lv1.get('children', [])),
+ "concept_count": 0,
+ "children": []
+ }
+
+ for child in lv1.get('children', []):
+ lv2_concept_count = 0
+ if 'children' in child:
+ for lv3_child in child.get('children', []):
+ lv2_concept_count += len(lv3_child.get('concepts', []))
+ else:
+ lv2_concept_count = len(child.get('concepts', []))
+
+ lv1_stats['children'].append({
+ "lv2": child.get('lv2'),
+ "lv2_id": child.get('lv2_id'),
+ "concept_count": lv2_concept_count
+ })
+ lv1_stats['concept_count'] += lv2_concept_count
+
+ stats.append(lv1_stats)
+
+ # 按概念数量排序
+ stats.sort(key=lambda x: x['concept_count'], reverse=True)
+
+ return {
+ "total_lv1": len(stats),
+ "total_concepts": sum(s['concept_count'] for s in stats),
+ "statistics": stats
+ }
+
+ except Exception as e:
+ logger.error(f"获取层级统计失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+# ==================== 股票搜索相关 ====================
+
+class StockSearchResult(BaseModel):
+ """股票搜索结果"""
+ stock_code: str
+ stock_name: str
+ concept_count: int
+
+
+class StockSearchResponse(BaseModel):
+ """股票搜索响应"""
+ total: int
+ stocks: List[StockSearchResult]
+
+
+@app.get("/stock/search", response_model=StockSearchResponse, tags=["Stocks"])
+async def search_stocks(
+ keyword: str = Query(..., description="股票关键词(代码或名称)"),
+ size: int = Query(20, ge=1, le=100, description="返回数量")
+):
+ """搜索股票名称或代码"""
+ try:
+ # v3 索引中股票字段是 stocks.name 和 stocks.code
+ search_body = {
+ "query": {
+ "nested": {
+ "path": "stocks",
+ "query": {
+ "bool": {
+ "should": [
+ {"wildcard": {"stocks.code": f"*{keyword}*"}},
+ {"match": {"stocks.name": keyword}}
+ ]
+ }
+ }
+ }
+ },
+ "size": 0,
+ "aggs": {
+ "unique_stocks": {
+ "nested": {
+ "path": "stocks"
+ },
+ "aggs": {
+ "stock_codes": {
+ "terms": {
+ "field": "stocks.code",
+ "size": size * 2
+ },
+ "aggs": {
+ "stock_names": {
+ "terms": {
+ "field": "stocks.name",
+ "size": 1
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ es_response = es_client.search(index=INDEX_NAME, body=search_body)
+
+ stocks = []
+ buckets = es_response['aggregations']['unique_stocks']['stock_codes']['buckets']
+
+ for bucket in buckets:
+ stock_code = bucket['key']
+ concept_count = bucket['doc_count']
+
+ name_buckets = bucket['stock_names']['buckets']
+ stock_name = name_buckets[0]['key'] if name_buckets else stock_code
+
+ if keyword in stock_code or keyword in stock_name:
+ stocks.append(StockSearchResult(
+ stock_code=stock_code,
+ stock_name=stock_name,
+ concept_count=concept_count
+ ))
+
+ stocks.sort(key=lambda x: x.concept_count, reverse=True)
+
+ return StockSearchResponse(
+ total=len(stocks),
+ stocks=stocks[:size]
+ )
+
+ except Exception as e:
+ logger.error(f"搜索股票失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+# ==================== 价格相关接口 ====================
+
+class PriceTimeSeriesItem(BaseModel):
+ """价格时间序列数据点"""
+ trade_date: date
+ avg_change_pct: Optional[float] = Field(None, description="平均涨跌幅(%)")
+ stock_count: Optional[int] = Field(None, description="当日股票数量")
+
+
+class PriceTimeSeriesResponse(BaseModel):
+ """价格时间序列响应"""
+ concept_id: str
+ concept_name: str
+ start_date: date
+ end_date: date
+ data_points: int
+ timeseries: List[PriceTimeSeriesItem]
+
+
+async def get_latest_trade_date() -> Optional[date]:
+ """获取最新的交易日期"""
+ if not mysql_pool:
+ return None
+
+ try:
+ async with mysql_pool.acquire() as conn:
+ async with conn.cursor() as cursor:
+ query = "SELECT MAX(trade_date) as latest_date FROM concept_daily_stats"
+ await cursor.execute(query)
+ result = await cursor.fetchone()
+ return result[0] if result and result[0] else None
+ except Exception as e:
+ logger.error(f"获取最新交易日期失败: {e}")
+ return None
+
+
+@app.get("/price/latest", tags=["Price"])
+async def get_latest_price_date():
+ """获取最新的涨跌幅数据日期"""
+ try:
+ latest_date = await get_latest_trade_date()
+ return {
+ "latest_trade_date": latest_date,
+ "has_data": latest_date is not None
+ }
+ except Exception as e:
+ logger.error(f"获取最新价格日期失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/concept/{concept_id}/price-timeseries", response_model=PriceTimeSeriesResponse, tags=["Price"])
+async def get_concept_price_timeseries(
+ concept_id: str,
+ start_date: date = Query(..., description="开始日期,格式:YYYY-MM-DD"),
+ end_date: date = Query(..., description="结束日期,格式:YYYY-MM-DD")
+):
+ """获取概念在指定日期范围内的涨跌幅时间序列数据"""
+ if not mysql_pool:
+ logger.warning(f"[PriceTimeseries] MySQL 连接不可用")
+ return PriceTimeSeriesResponse(
+ concept_id=concept_id,
+ concept_name=concept_id,
+ start_date=start_date,
+ end_date=end_date,
+ data_points=0,
+ timeseries=[]
+ )
+
+ if start_date > end_date:
+ raise HTTPException(status_code=400, detail="开始日期不能晚于结束日期")
+
+ try:
+ async with mysql_pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ query = """
+ SELECT trade_date, concept_name, avg_change_pct, stock_count
+ FROM concept_daily_stats
+ WHERE concept_id = %s
+ AND trade_date >= %s
+ AND trade_date <= %s
+ ORDER BY trade_date ASC
+ """
+
+ await cursor.execute(query, (concept_id, start_date, end_date))
+ rows = await cursor.fetchall()
+
+ if not rows:
+ raise HTTPException(
+ status_code=404,
+ detail=f"未找到概念ID {concept_id} 在 {start_date} 至 {end_date} 期间的数据"
+ )
+
+ timeseries = []
+ concept_name = ""
+
+ for row in rows:
+ if not concept_name and row.get('concept_name'):
+ concept_name = row['concept_name']
+
+ item = PriceTimeSeriesItem(
+ trade_date=row['trade_date'],
+ avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] is not None else None,
+ stock_count=row.get('stock_count')
+ )
+ timeseries.append(item)
+
+ return PriceTimeSeriesResponse(
+ concept_id=concept_id,
+ concept_name=concept_name or concept_id,
+ start_date=start_date,
+ end_date=end_date,
+ data_points=len(timeseries),
+ timeseries=timeseries
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"获取价格时间序列失败: {e}")
+ raise HTTPException(status_code=500, detail=f"获取时间序列数据失败: {str(e)}")
+
+
+# ==================== 统计接口 ====================
+
+class ConceptStatItem(BaseModel):
+ """概念统计项"""
+ name: str
+ concept_id: Optional[str] = None
+ change_pct: Optional[float] = None
+ stock_count: Optional[int] = None
+ news_count: Optional[int] = None
+ report_count: Optional[int] = None
+ total_mentions: Optional[int] = None
+ volatility: Optional[float] = None
+ avg_change: Optional[float] = None
+ max_change: Optional[float] = None
+ consecutive_days: Optional[int] = None
+ total_change: Optional[float] = None
+ avg_daily: Optional[float] = None
+ hierarchy: Optional[HierarchyInfo] = None
+
+
+class ConceptStatistics(BaseModel):
+ """概念统计数据"""
+ hot_concepts: List[ConceptStatItem]
+ cold_concepts: List[ConceptStatItem]
+ active_concepts: List[ConceptStatItem]
+ volatile_concepts: List[ConceptStatItem]
+ momentum_concepts: List[ConceptStatItem]
+ summary: Dict[str, Any]
+
+
+class ConceptStatisticsResponse(BaseModel):
+ """概念统计响应"""
+ success: bool
+ data: ConceptStatistics
+ params: Dict[str, Any]
+ note: Optional[str] = None
+
+
+def get_fallback_statistics(start_date, end_date) -> ConceptStatistics:
+ """返回示例统计数据"""
+ return ConceptStatistics(
+ hot_concepts=[
+ ConceptStatItem(name="人工智能", change_pct=8.76, stock_count=45, news_count=12),
+ ConceptStatItem(name="机器人", change_pct=7.54, stock_count=38, news_count=10),
+ ConceptStatItem(name="半导体", change_pct=6.43, stock_count=52, news_count=15),
+ ConceptStatItem(name="低空经济", change_pct=5.21, stock_count=28, news_count=6),
+ ConceptStatItem(name="量子科技", change_pct=4.98, stock_count=15, news_count=9),
+ ],
+ cold_concepts=[
+ ConceptStatItem(name="房地产", change_pct=-5.76, stock_count=33, news_count=5),
+ ConceptStatItem(name="煤炭", change_pct=-4.32, stock_count=25, news_count=3),
+ ConceptStatItem(name="钢铁", change_pct=-3.21, stock_count=28, news_count=4),
+ ConceptStatItem(name="传统零售", change_pct=-2.98, stock_count=19, news_count=2),
+ ConceptStatItem(name="纺织服装", change_pct=-2.45, stock_count=15, news_count=2),
+ ],
+ active_concepts=[
+ ConceptStatItem(name="人工智能", news_count=45, report_count=15, total_mentions=60),
+ ConceptStatItem(name="半导体", news_count=42, report_count=12, total_mentions=54),
+ ConceptStatItem(name="机器人", news_count=38, report_count=8, total_mentions=46),
+ ConceptStatItem(name="新能源", news_count=28, report_count=6, total_mentions=34),
+ ConceptStatItem(name="量子科技", news_count=25, report_count=5, total_mentions=30),
+ ],
+ volatile_concepts=[
+ ConceptStatItem(name="数字货币", volatility=25.6, avg_change=2.1, max_change=15.2),
+ ConceptStatItem(name="元宇宙", volatility=23.8, avg_change=1.8, max_change=13.9),
+ ConceptStatItem(name="虚拟现实", volatility=21.2, avg_change=-0.5, max_change=10.1),
+ ConceptStatItem(name="游戏", volatility=19.7, avg_change=3.2, max_change=12.8),
+ ConceptStatItem(name="短剧", volatility=18.3, avg_change=-1.1, max_change=8.1),
+ ],
+ momentum_concepts=[
+ ConceptStatItem(name="AI应用", consecutive_days=6, total_change=19.2, avg_daily=3.2),
+ ConceptStatItem(name="算力", consecutive_days=5, total_change=16.8, avg_daily=3.36),
+ ConceptStatItem(name="光通信", consecutive_days=4, total_change=13.1, avg_daily=3.28),
+ ConceptStatItem(name="存储", consecutive_days=4, total_change=12.4, avg_daily=3.1),
+ ConceptStatItem(name="PCB", consecutive_days=3, total_change=9.6, avg_daily=3.2),
+ ],
+ summary={
+ 'total_concepts': 500,
+ 'positive_count': 320,
+ 'negative_count': 180,
+ 'avg_change': 1.8,
+ 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'date_range': f"{start_date} 至 {end_date}",
+ 'days': (end_date - start_date).days + 1 if start_date and end_date else 7,
+ 'start_date': str(start_date) if start_date else '',
+ 'end_date': str(end_date) if end_date else ''
+ }
+ )
+
+
+@app.get("/statistics", response_model=ConceptStatisticsResponse, tags=["Statistics"])
+async def get_concept_statistics(
+ days: Optional[int] = Query(None, ge=1, le=90, description="统计天数范围"),
+ start_date: Optional[date] = Query(None, description="开始日期"),
+ end_date: Optional[date] = Query(None, description="结束日期"),
+ min_stock_count: int = Query(3, ge=1, description="最少股票数量过滤"),
+ concept_type: Optional[str] = Query(None, description="概念类型过滤: leaf/lv1/lv2/lv3,默认为leaf")
+):
+ """获取概念板块统计数据 - 涨幅榜、跌幅榜、活跃榜、波动榜、连涨榜"""
+ from datetime import timedelta
+
+ # 计算日期范围
+ if days is not None and (start_date is not None or end_date is not None):
+ raise HTTPException(status_code=400, detail="days参数与start_date/end_date参数不能同时使用")
+
+ if start_date is not None and end_date is not None:
+ if start_date > end_date:
+ raise HTTPException(status_code=400, detail="开始日期不能晚于结束日期")
+ if (end_date - start_date).days > 90:
+ raise HTTPException(status_code=400, detail="日期范围不能超过90天")
+ elif days is not None:
+ end_date = datetime.now().date()
+ start_date = end_date - timedelta(days=days)
+ elif start_date is not None:
+ end_date = datetime.now().date()
+ elif end_date is not None:
+ start_date = end_date - timedelta(days=7)
+ else:
+ end_date = datetime.now().date()
+ start_date = end_date - timedelta(days=7)
+
+ if not mysql_pool:
+ logger.warning("[Statistics] MySQL 连接不可用,使用示例数据")
+ return ConceptStatisticsResponse(
+ success=True,
+ data=get_fallback_statistics(start_date, end_date),
+ params={
+ 'days': (end_date - start_date).days + 1,
+ 'min_stock_count': min_stock_count,
+ 'start_date': str(start_date),
+ 'end_date': str(end_date)
+ },
+ note="MySQL 连接不可用,使用示例数据"
+ )
+
+ # 默认只查询叶子概念
+ type_filter = concept_type if concept_type in ['leaf', 'lv1', 'lv2', 'lv3'] else 'leaf'
+
+ try:
+ async with mysql_pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+
+ # 1. 涨幅榜
+ hot_query = """
+ SELECT concept_id, concept_name,
+ AVG(avg_change_pct) as avg_change_pct,
+ AVG(stock_count) as avg_stock_count,
+ COUNT(*) as trading_days
+ FROM concept_daily_stats
+ WHERE trade_date >= %s AND trade_date <= %s
+ AND avg_change_pct IS NOT NULL AND stock_count >= %s
+ AND concept_type = %s
+ GROUP BY concept_id, concept_name
+ HAVING COUNT(*) >= 2
+ ORDER BY AVG(avg_change_pct) DESC
+ LIMIT 10
+ """
+ await cursor.execute(hot_query, (start_date, end_date, min_stock_count, type_filter))
+ hot_rows = await cursor.fetchall()
+
+ # 2. 跌幅榜
+ cold_query = """
+ SELECT concept_id, concept_name,
+ AVG(avg_change_pct) as avg_change_pct,
+ AVG(stock_count) as avg_stock_count,
+ COUNT(*) as trading_days
+ FROM concept_daily_stats
+ WHERE trade_date >= %s AND trade_date <= %s
+ AND avg_change_pct IS NOT NULL AND stock_count >= %s
+ AND concept_type = %s
+ GROUP BY concept_id, concept_name
+ HAVING COUNT(*) >= 2
+ ORDER BY AVG(avg_change_pct) ASC
+ LIMIT 10
+ """
+ await cursor.execute(cold_query, (start_date, end_date, min_stock_count, type_filter))
+ cold_rows = await cursor.fetchall()
+
+ # 3. 活跃榜
+ active_query = """
+ SELECT concept_id, concept_name,
+ COUNT(*) as trading_days,
+ AVG(stock_count) as avg_stock_count
+ FROM concept_daily_stats
+ WHERE trade_date >= %s AND trade_date <= %s AND stock_count >= %s
+ AND concept_type = %s
+ GROUP BY concept_id, concept_name
+ ORDER BY COUNT(*) DESC, AVG(stock_count) DESC
+ LIMIT 10
+ """
+ await cursor.execute(active_query, (start_date, end_date, min_stock_count, type_filter))
+ active_rows = await cursor.fetchall()
+
+ # 4. 波动榜
+ volatile_query = """
+ SELECT concept_id, concept_name,
+ STDDEV(avg_change_pct) as volatility,
+ AVG(avg_change_pct) as avg_change_pct,
+ MAX(avg_change_pct) as max_change_pct
+ FROM concept_daily_stats
+ WHERE trade_date >= %s AND trade_date <= %s
+ AND avg_change_pct IS NOT NULL AND stock_count >= %s
+ AND concept_type = %s
+ GROUP BY concept_id, concept_name
+ HAVING COUNT(*) >= 3 AND STDDEV(avg_change_pct) IS NOT NULL
+ ORDER BY STDDEV(avg_change_pct) DESC
+ LIMIT 10
+ """
+ await cursor.execute(volatile_query, (start_date, end_date, min_stock_count, type_filter))
+ volatile_rows = await cursor.fetchall()
+
+ # 5. 连涨榜
+ momentum_query = """
+ SELECT concept_id, concept_name,
+ COUNT(*) as positive_days,
+ SUM(avg_change_pct) as total_change,
+ AVG(avg_change_pct) as avg_change_pct
+ FROM concept_daily_stats
+ WHERE trade_date >= %s AND trade_date <= %s
+ AND avg_change_pct > 0 AND stock_count >= %s
+ AND concept_type = %s
+ GROUP BY concept_id, concept_name
+ HAVING COUNT(*) >= 2
+ ORDER BY COUNT(*) DESC, AVG(avg_change_pct) DESC
+ LIMIT 10
+ """
+ await cursor.execute(momentum_query, (start_date, end_date, min_stock_count, type_filter))
+ momentum_rows = await cursor.fetchall()
+
+ # 6. 总体统计
+ total_query = """
+ SELECT COUNT(DISTINCT concept_id) as total_concepts,
+ COUNT(DISTINCT CASE WHEN avg_change_pct > 0 THEN concept_id END) as positive_concepts,
+ COUNT(DISTINCT CASE WHEN avg_change_pct < 0 THEN concept_id END) as negative_concepts,
+ AVG(avg_change_pct) as overall_avg_change
+ FROM concept_daily_stats
+ WHERE trade_date >= %s AND trade_date <= %s AND avg_change_pct IS NOT NULL
+ AND concept_type = %s
+ """
+ await cursor.execute(total_query, (start_date, end_date, type_filter))
+ total_row = await cursor.fetchone()
+
+ # 构建响应
+ def build_items(rows, item_type):
+ items = []
+ for row in rows:
+ concept_name = row['concept_name']
+ hierarchy_info = get_concept_hierarchy(concept_name)
+
+ item = ConceptStatItem(
+ name=concept_name,
+ concept_id=row.get('concept_id'),
+ hierarchy=HierarchyInfo(**hierarchy_info) if hierarchy_info else None
+ )
+
+ if item_type in ['hot', 'cold']:
+ item.change_pct = round(float(row['avg_change_pct']), 2) if row['avg_change_pct'] else 0.0
+ item.stock_count = int(row['avg_stock_count']) if row['avg_stock_count'] else 0
+ item.news_count = int(row['trading_days']) if row['trading_days'] else 0
+
+ elif item_type == 'active':
+ item.news_count = int(row['trading_days']) if row['trading_days'] else 0
+ item.stock_count = int(row['avg_stock_count']) if row['avg_stock_count'] else 0
+ item.report_count = max(1, int(row['trading_days'] * 0.3)) if row['trading_days'] else 1
+ item.total_mentions = item.news_count + item.report_count
+
+ elif item_type == 'volatile':
+ item.volatility = round(float(row['volatility']), 2) if row['volatility'] else 0.0
+ item.avg_change = round(float(row['avg_change_pct']), 2) if row['avg_change_pct'] else 0.0
+ item.max_change = round(float(row['max_change_pct']), 2) if row['max_change_pct'] else 0.0
+
+ elif item_type == 'momentum':
+ item.consecutive_days = int(row['positive_days']) if row['positive_days'] else 0
+ item.total_change = round(float(row['total_change']), 2) if row['total_change'] else 0.0
+ item.avg_daily = round(float(row['avg_change_pct']), 2) if row['avg_change_pct'] else 0.0
+
+ items.append(item)
+ return items
+
+ statistics = ConceptStatistics(
+ hot_concepts=build_items(hot_rows, 'hot'),
+ cold_concepts=build_items(cold_rows, 'cold'),
+ active_concepts=build_items(active_rows, 'active'),
+ volatile_concepts=build_items(volatile_rows, 'volatile'),
+ momentum_concepts=build_items(momentum_rows, 'momentum'),
+ summary={
+ 'total_concepts': int(total_row['total_concepts']) if total_row['total_concepts'] else 0,
+ 'positive_count': int(total_row['positive_concepts']) if total_row['positive_concepts'] else 0,
+ 'negative_count': int(total_row['negative_concepts']) if total_row['negative_concepts'] else 0,
+ 'avg_change': round(float(total_row['overall_avg_change']), 2) if total_row['overall_avg_change'] else 0.0,
+ 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'date_range': f"{start_date} 至 {end_date}",
+ 'days': (end_date - start_date).days + 1,
+ 'start_date': str(start_date),
+ 'end_date': str(end_date)
+ }
+ )
+
+ # 补充空数据
+ if not statistics.hot_concepts or not statistics.cold_concepts:
+ fallback = get_fallback_statistics(start_date, end_date)
+ if not statistics.hot_concepts:
+ statistics.hot_concepts = fallback.hot_concepts
+ if not statistics.cold_concepts:
+ statistics.cold_concepts = fallback.cold_concepts
+
+ return ConceptStatisticsResponse(
+ success=True,
+ data=statistics,
+ params={
+ 'days': (end_date - start_date).days + 1,
+ 'min_stock_count': min_stock_count,
+ 'start_date': str(start_date),
+ 'end_date': str(end_date),
+ 'concept_type': type_filter
+ }
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"获取概念统计失败: {e}")
+ return ConceptStatisticsResponse(
+ success=True,
+ data=get_fallback_statistics(start_date, end_date),
+ params={
+ 'days': (end_date - start_date).days + 1,
+ 'min_stock_count': min_stock_count,
+ 'start_date': str(start_date),
+ 'end_date': str(end_date),
+ 'concept_type': type_filter
+ },
+ note=f"使用示例数据,原因: {str(e)}"
+ )
+
+
+# ==================== 层级概念涨跌幅接口 ====================
+
+class HierarchyPriceItem(BaseModel):
+ """层级概念价格项"""
+ concept_id: str
+ concept_name: str
+ concept_type: str # lv1/lv2/lv3
+ trade_date: date
+ avg_change_pct: Optional[float] = None
+ stock_count: Optional[int] = None
+
+
+class HierarchyPriceResponse(BaseModel):
+ """层级概念价格响应"""
+ trade_date: date
+ lv1_concepts: List[HierarchyPriceItem]
+ lv2_concepts: List[HierarchyPriceItem]
+ lv3_concepts: List[HierarchyPriceItem]
+ total_count: int
+
+
+@app.get("/hierarchy/price", response_model=HierarchyPriceResponse, tags=["Hierarchy"])
+async def get_hierarchy_price(
+ trade_date: Optional[date] = Query(None, description="交易日期,默认最新"),
+ lv1_filter: Optional[str] = Query(None, description="筛选特定一级分类")
+):
+ """获取层级概念(lv1/lv2/lv3)的涨跌幅数据"""
+ if not mysql_pool:
+ raise HTTPException(status_code=503, detail="MySQL连接不可用")
+
+ try:
+ async with mysql_pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ # 获取交易日期
+ if trade_date is None:
+ await cursor.execute(
+ "SELECT MAX(trade_date) as max_date FROM concept_daily_stats WHERE concept_type != 'leaf'"
+ )
+ result = await cursor.fetchone()
+ if not result or not result['max_date']:
+ raise HTTPException(status_code=404, detail="无母概念涨跌幅数据")
+ trade_date = result['max_date']
+
+ # 构建查询
+ base_query = """
+ SELECT concept_id, concept_name, concept_type, trade_date, avg_change_pct, stock_count
+ FROM concept_daily_stats
+ WHERE trade_date = %s AND concept_type = %s
+ """
+
+ if lv1_filter:
+ base_query += " AND concept_name LIKE %s"
+
+ base_query += " ORDER BY avg_change_pct DESC"
+
+ lv1_concepts = []
+ lv2_concepts = []
+ lv3_concepts = []
+
+ # 查询 lv1
+ if lv1_filter:
+ await cursor.execute(base_query, (trade_date, 'lv1', f"%{lv1_filter}%"))
+ else:
+ await cursor.execute(base_query, (trade_date, 'lv1'))
+ for row in await cursor.fetchall():
+ lv1_concepts.append(HierarchyPriceItem(
+ concept_id=row['concept_id'],
+ concept_name=row['concept_name'],
+ concept_type='lv1',
+ trade_date=row['trade_date'],
+ avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] else None,
+ stock_count=row['stock_count']
+ ))
+
+ # 查询 lv2
+ if lv1_filter:
+ await cursor.execute(base_query, (trade_date, 'lv2', f"%{lv1_filter}%"))
+ else:
+ await cursor.execute(base_query, (trade_date, 'lv2'))
+ for row in await cursor.fetchall():
+ lv2_concepts.append(HierarchyPriceItem(
+ concept_id=row['concept_id'],
+ concept_name=row['concept_name'],
+ concept_type='lv2',
+ trade_date=row['trade_date'],
+ avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] else None,
+ stock_count=row['stock_count']
+ ))
+
+ # 查询 lv3
+ if lv1_filter:
+ await cursor.execute(base_query, (trade_date, 'lv3', f"%{lv1_filter}%"))
+ else:
+ await cursor.execute(base_query, (trade_date, 'lv3'))
+ for row in await cursor.fetchall():
+ lv3_concepts.append(HierarchyPriceItem(
+ concept_id=row['concept_id'],
+ concept_name=row['concept_name'],
+ concept_type='lv3',
+ trade_date=row['trade_date'],
+ avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] else None,
+ stock_count=row['stock_count']
+ ))
+
+ return HierarchyPriceResponse(
+ trade_date=trade_date,
+ lv1_concepts=lv1_concepts,
+ lv2_concepts=lv2_concepts,
+ lv3_concepts=lv3_concepts,
+ total_count=len(lv1_concepts) + len(lv2_concepts) + len(lv3_concepts)
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"获取层级涨跌幅失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/hierarchy/{hierarchy_id}/price-timeseries", tags=["Hierarchy"])
+async def get_hierarchy_price_timeseries(
+ hierarchy_id: str,
+ start_date: date = Query(..., description="开始日期"),
+ end_date: date = Query(..., description="结束日期")
+):
+ """获取层级概念(lv1/lv2/lv3)的价格时间序列"""
+ if not mysql_pool:
+ raise HTTPException(status_code=503, detail="MySQL连接不可用")
+
+ if start_date > end_date:
+ raise HTTPException(status_code=400, detail="开始日期不能晚于结束日期")
+
+ try:
+ async with mysql_pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ query = """
+ SELECT concept_id, concept_name, concept_type, trade_date, avg_change_pct, stock_count
+ FROM concept_daily_stats
+ WHERE concept_id = %s
+ AND trade_date >= %s AND trade_date <= %s
+ ORDER BY trade_date ASC
+ """
+ await cursor.execute(query, (hierarchy_id, start_date, end_date))
+ rows = await cursor.fetchall()
+
+ if not rows:
+ raise HTTPException(status_code=404, detail=f"未找到层级概念 {hierarchy_id} 的数据")
+
+ timeseries = []
+ concept_name = ""
+ concept_type = ""
+
+ for row in rows:
+ if not concept_name:
+ concept_name = row['concept_name']
+ concept_type = row['concept_type']
+
+ timeseries.append({
+ 'trade_date': row['trade_date'],
+ 'avg_change_pct': float(row['avg_change_pct']) if row['avg_change_pct'] else None,
+ 'stock_count': row['stock_count']
+ })
+
+ return {
+ 'concept_id': hierarchy_id,
+ 'concept_name': concept_name,
+ 'concept_type': concept_type,
+ 'start_date': start_date,
+ 'end_date': end_date,
+ 'data_points': len(timeseries),
+ 'timeseries': timeseries
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"获取层级价格时间序列失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+# ==================== 主函数 ====================
+
+if __name__ == "__main__":
+ import uvicorn
+
+ uvicorn.run(
+ "concept_api_v2:app",
+ host="0.0.0.0",
+ port=6801,
+ reload=True,
+ log_level="info"
+ )
diff --git a/concept_hierarchy_v3.json b/concept_hierarchy_v3.json
new file mode 100644
index 00000000..a7eb2200
--- /dev/null
+++ b/concept_hierarchy_v3.json
@@ -0,0 +1,1275 @@
+{
+ "hierarchy": [
+ {
+ "lv1": "人工智能",
+ "lv1_id": "lv1_1",
+ "children": [
+ {
+ "lv2": "AI基础设施",
+ "lv2_id": "lv2_1_1",
+ "children": [
+ {
+ "lv3": "AI算力硬件",
+ "lv3_id": "lv3_1_1_1",
+ "concepts": [
+ "AI一体机",
+ "AI算力芯片",
+ "AI芯片",
+ "DeepSeek智算一体机",
+ "GPU概念股",
+ "TPU芯片",
+ "一体机核心标的弹性测算",
+ "中昊芯英概念股",
+ "国产算力芯片",
+ "国产GPU",
+ "服务器",
+ "昇腾推理一体机",
+ "摩尔线程",
+ "摩尔线程IPO",
+ "沐曦集成",
+ "阿里AI芯片"
+ ]
+ },
+ {
+ "lv3": "AI关键组件",
+ "lv3_id": "lv3_1_1_2",
+ "concepts": [
+ "AI PCB",
+ "AI PCB英伟达M9",
+ "AI服务器钽电容",
+ "HBM",
+ "OCS光电路交换机",
+ "PCB",
+ "PCB设备及耗材",
+ "SRAM存储",
+ "光纤",
+ "光通信CPO",
+ "光通信",
+ "光芯片",
+ "光纤列阵单元FAU",
+ "博通交换机",
+ "改良型半加成工艺mSAP",
+ "服务器零部件",
+ "硅光技术",
+ "空芯光纤",
+ "薄膜铌酸锂",
+ "存储",
+ "存储芯片",
+ "存储芯片产业",
+ "铜互连",
+ "铜连接",
+ "钽电容",
+ "忆阻器",
+ "利基型存储DDR4"
+ ]
+ },
+ {
+ "lv3": "AI配套设施",
+ "lv3_id": "lv3_1_1_3",
+ "concepts": [
+ "AIDC供配电设备弹性",
+ "数据中心",
+ "数据中心液冷",
+ "数据中心电力设备",
+ "微泵液冷",
+ "微通道水冷板",
+ "液冷",
+ "液冷数据中心",
+ "液态金属散热",
+ "电池备份单元",
+ "电磁屏蔽",
+ "超级电容器",
+ "柴油发电机",
+ "钻石散热",
+ "英伟达电源方案"
+ ]
+ },
+ {
+ "lv3": "算力网络与服务",
+ "lv3_id": "lv3_1_1_4",
+ "concepts": [
+ "中国星际之门芜湖",
+ "上海算力",
+ "四川算力",
+ "大厂算力订单",
+ "字节算力",
+ "星际之门概念",
+ "杭州算力大会",
+ "核心城市智算算力",
+ "毫秒用算",
+ "算力"
+ ]
+ }
+ ]
+ },
+ {
+ "lv2": "AI模型与软件",
+ "lv2_id": "lv2_1_2",
+ "concepts": [
+ "DeepSeek FP8",
+ "DeepSeek",
+ "DeepSeek、国产算力",
+ "KIMI",
+ "MOE模型",
+ "Minimax",
+ "Nano Banana",
+ "SORA概念",
+ "国产大模型",
+ "文生视频",
+ "秘塔AI",
+ "阶跃星辰",
+ "马斯克Grok3大模型"
+ ]
+ },
+ {
+ "lv2": "AI应用",
+ "lv2_id": "lv2_1_3",
+ "children": [
+ {
+ "lv3": "智能体与陪伴",
+ "lv3_id": "lv3_1_3_1",
+ "concepts": [
+ "AI伴侣",
+ "AI成人陪伴",
+ "AI应用陪伴智能体",
+ "AI智能体",
+ "AI应用智能体",
+ "AI智能体AI应用",
+ "AI语音助手",
+ "AI陪伴",
+ "Manus",
+ "字节AI陪伴",
+ "开发智能体"
+ ]
+ },
+ {
+ "lv3": "行业应用",
+ "lv3_id": "lv3_1_3_2",
+ "concepts": [
+ "AI4S",
+ "AI应用AI语料",
+ "AI编程",
+ "低代码",
+ "内容审核概念",
+ "物理AI"
+ ]
+ }
+ ]
+ },
+ {
+ "lv2": "AI生态系统",
+ "lv2_id": "lv2_1_4",
+ "children": [
+ {
+ "lv3": "通用生态",
+ "lv3_id": "lv3_1_4_1",
+ "concepts": [
+ "云计算各厂商云",
+ "微软Azure云平台",
+ "甲骨文概念股",
+ "英伟达代理",
+ "英伟达概念",
+ "英伟达H20",
+ "超威半导体AMD",
+ "谷歌",
+ "谷歌概念"
+ ]
+ },
+ {
+ "lv3": "阿里生态",
+ "lv3_id": "lv3_1_4_2",
+ "concepts": [
+ "通义千问阿里云",
+ "阿里云",
+ "阿里云通义千问",
+ "阿里AI千问、灵光",
+ "阿里“千问”项目",
+ "阿里AI来听"
+ ]
+ },
+ {
+ "lv3": "腾讯生态",
+ "lv3_id": "lv3_1_4_3",
+ "concepts": [
+ "腾讯元宝",
+ "腾讯大模型",
+ "腾讯混元大模型",
+ "腾讯云及大模型合作公司"
+ ]
+ },
+ {
+ "lv3": "字节跳动生态",
+ "lv3_id": "lv3_1_4_4",
+ "concepts": [
+ "努比亚手机",
+ "华为抖音支付",
+ "字节概念豆包AI手机",
+ "字节豆包概念股",
+ "抖音概念",
+ "豆包大模型"
+ ]
+ }
+ ]
+ },
+ {
+ "lv2": "AI综合与趋势",
+ "lv2_id": "lv2_1_5",
+ "concepts": [
+ "AI-细分延伸更新",
+ "AI合集"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "半导体",
+ "lv1_id": "lv1_2",
+ "children": [
+ {
+ "lv2": "半导体设备",
+ "lv2_id": "lv2_2_1",
+ "concepts": [
+ "EDA",
+ "上海微电子",
+ "光刻机",
+ "光刻机宇量昇",
+ "半导体设备",
+ "大湾区芯片展览会-新凯莱",
+ "新凯来概念股",
+ "新凯来示波器",
+ "电子束光刻机“羲之”"
+ ]
+ },
+ {
+ "lv2": "半导体材料",
+ "lv2_id": "lv2_2_2",
+ "concepts": [
+ "PSPI",
+ "先进陶瓷",
+ "光刻胶",
+ "半导体抛光液",
+ "半导体材料",
+ "国产光刻胶",
+ "石英砂",
+ "磷化铟"
+ ]
+ },
+ {
+ "lv2": "芯片设计与制造",
+ "lv2_id": "lv2_2_3",
+ "concepts": [
+ "ASIC",
+ "GAA晶体管",
+ "ISP视觉",
+ "RISC-V",
+ "功率半导体",
+ "半导体设计",
+ "第三代半导体",
+ "端侧AI芯片",
+ "碳化硅",
+ "英诺赛科概念股"
+ ]
+ },
+ {
+ "lv2": "先进封装",
+ "lv2_id": "lv2_2_4",
+ "concepts": [
+ "半导体封测",
+ "半导体混合键合技术",
+ "玻璃基板",
+ "盛合晶微",
+ "盛合晶微概念股"
+ ]
+ },
+ {
+ "lv2": "重点企业与IPO",
+ "lv2_id": "lv2_2_5",
+ "concepts": [
+ "地平线",
+ "地平线概念",
+ "紫光展锐IPO",
+ "长鑫存储",
+ "长鑫、长江产业链",
+ "高通概念"
+ ]
+ },
+ {
+ "lv2": "综合与政策",
+ "lv2_id": "lv2_2_6",
+ "concepts": [
+ "半导体产业链",
+ "国产半导体",
+ "国产芯片参股公司",
+ "科特估半导体",
+ "芯片替代",
+ "英特尔概念股"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "机器人",
+ "lv1_id": "lv1_3",
+ "children": [
+ {
+ "lv2": "人形机器人整机",
+ "lv2_id": "lv2_3_1",
+ "concepts": [
+ "Optimus特斯拉机器人",
+ "乐聚机器人",
+ "人形机器人",
+ "人形机器人Figure",
+ "云深处",
+ "优必选",
+ "优必选机器人",
+ "华为人形机器人",
+ "各厂商机器人",
+ "奇瑞机器人潜在产业链",
+ "天太机器人",
+ "天工机器人",
+ "宇树人形机器人",
+ "宇树机器人",
+ "小鹏机器人",
+ "小米机器人",
+ "小米智元机器人产业链机构版",
+ "开普勒机器人",
+ "松延动力机器人",
+ "特斯拉人形机器人",
+ "智元机器人",
+ "荣耀华为人形机器人",
+ "赛力斯机器人"
+ ]
+ },
+ {
+ "lv2": "机器人核心零部件",
+ "lv2_id": "lv2_3_2",
+ "concepts": [
+ "PCB轴向磁通电机",
+ "人形机器人-滚柱丝杆丝杠",
+ "人形机器人万向节",
+ "人形机器人腱绳",
+ "机电",
+ "机器人皮肤仿生皮肤",
+ "摆线减速器",
+ "电子皮肤",
+ "轴向磁通电机"
+ ]
+ },
+ {
+ "lv2": "机器人产业链",
+ "lv2_id": "lv2_3_3",
+ "children": [
+ {
+ "lv3": "综合与价值链",
+ "lv3_id": "lv3_3_3_1",
+ "concepts": [
+ "人形机器产业链",
+ "人形机器人核心标的估值弹性测算",
+ "人形机器人核心标的概览",
+ "特斯拉人形机器人价值量",
+ "特斯拉人形机器人弹性测算",
+ "特斯拉产业链"
+ ]
+ },
+ {
+ "lv3": "轻量化材料",
+ "lv3_id": "lv3_3_3_2",
+ "concepts": [
+ "人形机器人轻量化-PEEK",
+ "人形机器人轻量化-PEEK材料",
+ "机器人轻量化-镁铝合金",
+ "机器人轻量化—碳纤维",
+ "超高分子量聚乙烯纤维"
+ ]
+ },
+ {
+ "lv3": "制造工艺与设备",
+ "lv3_id": "lv3_3_3_3",
+ "concepts": [
+ "MIM概念",
+ "冷锻产业链",
+ "机器人零部件加工设备",
+ "金属粉末注射成形MIM"
+ ]
+ }
+ ]
+ },
+ {
+ "lv2": "机器人软件与AI",
+ "lv2_id": "lv2_3_4",
+ "concepts": [
+ "机器人-神经网络",
+ "机器人动作捕捉"
+ ]
+ },
+ {
+ "lv2": "其他类型机器人",
+ "lv2_id": "lv2_3_5",
+ "concepts": [
+ "外骨骼机器人",
+ "工业机器人",
+ "机器狗四足机器人",
+ "美的库卡机器人"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "消费电子",
+ "lv1_id": "lv1_4",
+ "children": [
+ {
+ "lv2": "智能终端(端侧AI)",
+ "lv2_id": "lv2_4_1",
+ "concepts": [
+ "2025CES参展公司",
+ "AI PC",
+ "AIPC",
+ "AI手机"
+ ]
+ },
+ {
+ "lv2": "XR与空间计算",
+ "lv2_id": "lv2_4_2",
+ "concepts": [
+ "AI手势识别",
+ "AI眼镜",
+ "AI隔空投送",
+ "AR镀膜",
+ "AR眼镜",
+ "META智能眼镜",
+ "MR",
+ "Rokid AR",
+ "消费电子-玄玑感知系统",
+ "智能穿戴",
+ "智能眼镜",
+ "小米眼镜",
+ "阿里夸克AI眼镜",
+ "谷歌AI眼镜-合作XREAL"
+ ]
+ },
+ {
+ "lv2": "华为产业链",
+ "lv2_id": "lv2_4_3",
+ "children": [
+ {
+ "lv3": "终端产品",
+ "lv3_id": "lv3_4_3_1",
+ "concepts": [
+ "华为P70",
+ "华为Mate80",
+ "华为Pura70",
+ "华为Mate70手表",
+ "华为MATE70"
+ ]
+ },
+ {
+ "lv3": "技术与生态",
+ "lv3_id": "lv3_4_3_2",
+ "concepts": [
+ "华为",
+ "华为5G",
+ "华为AI容器",
+ "华为云",
+ "华为昇腾",
+ "华为昇腾超节点",
+ "华为通信大模型",
+ "华为海思星闪",
+ "华为鸿蒙",
+ "华为鸿蒙甄选与支付",
+ "华字辈",
+ "昇腾异构计算架构-CANN",
+ "鸿蒙PC"
+ ]
+ },
+ {
+ "lv3": "芯片与算力",
+ "lv3_id": "lv3_4_3_3",
+ "concepts": [
+ "华为910C",
+ "华为AI存储",
+ "华为存储OceanStor",
+ "华为麒麟芯片",
+ "磁电存储"
+ ]
+ }
+ ]
+ },
+ {
+ "lv2": "苹果产业链",
+ "lv2_id": "lv2_4_4",
+ "concepts": [
+ "果链OPEN AI复用",
+ "苹果MR产业链",
+ "苹果供应商核心公司",
+ "苹果机器人",
+ "苹果手机产业链"
+ ]
+ },
+ {
+ "lv2": "新型显示技术",
+ "lv2_id": "lv2_4_5",
+ "concepts": [
+ "华为三折叠屏",
+ "折叠屏",
+ "显影液及硅基OLED",
+ "苹果OLED潜在受益",
+ "苹果折叠屏",
+ "面板"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "智能驾驶与汽车",
+ "lv1_id": "lv1_5",
+ "children": [
+ {
+ "lv2": "自动驾驶解决方案",
+ "lv2_id": "lv2_5_1",
+ "concepts": [
+ "Robotaxi",
+ "京东物流Robovan",
+ "小马智行",
+ "文远知行",
+ "无人物流",
+ "无人物流车九识智能",
+ "无人驾驶",
+ "特斯拉FSD",
+ "特斯拉RoboTaxi概念",
+ "特斯拉无人驾驶出租车Robotaxi",
+ "自动驾驶",
+ "菜鸟无人物流车"
+ ]
+ },
+ {
+ "lv2": "智能汽车产业链",
+ "lv2_id": "lv2_5_2",
+ "concepts": [
+ "比亚迪产业链",
+ "比亚迪智驾",
+ "孙潇雅团队概念股",
+ "小米YU7供应链弹性测算",
+ "小米大模型",
+ "小米概念",
+ "小米算力AI互联",
+ "小米汽车产业链",
+ "小米汽车产业链弹性",
+ "小米汽车产业链弹性测算",
+ "小鹏产业链",
+ "理想汽车"
+ ]
+ },
+ {
+ "lv2": "车路协同与特定场景",
+ "lv2_id": "lv2_5_3",
+ "concepts": [
+ "无人驾驶-线控转向",
+ "无人驾驶公交",
+ "无人环卫车",
+ "矿山智驾",
+ "车路云-车路协同运营建设",
+ "车路云一体化",
+ "车路协同"
+ ]
+ },
+ {
+ "lv2": "产业链综合",
+ "lv2_id": "lv2_5_4",
+ "concepts": [
+ "mobileye替代概念",
+ "智能驾驶产业链",
+ "汽车安全"
+ ]
+ },
+ {
+ "lv2": "出行服务",
+ "lv2_id": "lv2_5_5",
+ "concepts": [
+ "网约车"
+ ]
+ },
+ {
+ "lv2": "智能座舱",
+ "lv2_id": "lv2_5_6",
+ "concepts": [
+ "空中成像"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "新能源与电力",
+ "lv1_id": "lv1_6",
+ "children": [
+ {
+ "lv2": "新型电池技术",
+ "lv2_id": "lv2_6_1",
+ "children": [
+ {
+ "lv3": "固态电池",
+ "lv3_id": "lv3_6_1_1",
+ "concepts": [
+ "固态电池",
+ "固态电池-硅基负极",
+ "固态电池-硫化物",
+ "固态电池产业链",
+ "固态电池设备",
+ "固态电池负极集流体材料-铜箔",
+ "陶瓷隔膜骨架膜"
+ ]
+ },
+ {
+ "lv3": "其他材料与技术",
+ "lv3_id": "lv3_6_1_2",
+ "concepts": [
+ "复合集流体",
+ "富锂锰基材料",
+ "硅基负极材料",
+ "钠离子电池",
+ "隔膜"
+ ]
+ }
+ ]
+ },
+ {
+ "lv2": "电力设备与电网",
+ "lv2_id": "lv2_6_2",
+ "concepts": [
+ "北美缺电AI电力",
+ "变压器出海",
+ "固体氧化物燃料电池-SOFC",
+ "固态变压器SST",
+ "燃料电池",
+ "燃气设备",
+ "燃气轮机HRSG",
+ "电力",
+ "电力产业链",
+ "电力电网",
+ "电力设备"
+ ]
+ },
+ {
+ "lv2": "清洁能源",
+ "lv2_id": "lv2_6_3",
+ "children": [
+ {
+ "lv3": "光伏",
+ "lv3_id": "lv3_6_3_1",
+ "concepts": [
+ "N型产业链",
+ "光伏",
+ "光伏行业兼并重组",
+ "光伏产业链",
+ "反内卷光伏",
+ "叠层钙钛矿",
+ "钙钛矿电池"
+ ]
+ },
+ {
+ "lv3": "核能",
+ "lv3_id": "lv3_6_3_2",
+ "concepts": [
+ "可控核聚变",
+ "微型核电",
+ "核电产业链",
+ "核电钍基熔盐堆",
+ "核聚变超导",
+ "超聚变",
+ "高温概念"
+ ]
+ }
+ ]
+ },
+ {
+ "lv2": "充电桩与补能",
+ "lv2_id": "lv2_6_4",
+ "concepts": [
+ "充电桩",
+ "华为智能充电",
+ "华为智能充电网络",
+ "换电",
+ "换电重卡",
+ "机器人充电",
+ "汽车无线充电",
+ "比亚迪兆瓦闪充"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "空天经济",
+ "lv1_id": "lv1_7",
+ "children": [
+ {
+ "lv2": "低空经济",
+ "lv2_id": "lv2_7_1",
+ "concepts": [
+ "eVTOL材料",
+ "九天无人机",
+ "亿航智能订单量",
+ "低空经济",
+ "低空经济&飞行汽车",
+ "低空经济产业链汇集",
+ "低空经济亿航智能",
+ "低空管控",
+ "低空物流",
+ "低空设计",
+ "小鹏汇天",
+ "小鹏汇天供应商",
+ "飞行汽车eVTOL",
+ "长安飞行汽车机器人概念"
+ ]
+ },
+ {
+ "lv2": "商业航天",
+ "lv2_id": "lv2_7_2",
+ "concepts": [
+ "凌空天行",
+ "北斗信使",
+ "北斗导航",
+ "商业航天",
+ "商业航天卫星通信",
+ "卫星出海",
+ "卫星互联网",
+ "太空行走",
+ "太空旅行",
+ "太空算力",
+ "手机直连卫星",
+ "星河动力",
+ "星网",
+ "蓝箭航天朱雀三号"
+ ]
+ },
+ {
+ "lv2": "通信技术",
+ "lv2_id": "lv2_7_3",
+ "concepts": [
+ "5.5G",
+ "5G-A",
+ "5G毫米波",
+ "6G概念",
+ "eSIM概念",
+ "通感一体"
+ ]
+ },
+ {
+ "lv2": "民用航空",
+ "lv2_id": "lv2_7_4",
+ "concepts": [
+ "大飞机",
+ "空客合作"
+ ]
+ },
+ {
+ "lv2": "综合与主题",
+ "lv2_id": "lv2_7_5",
+ "concepts": [
+ "珠海航展"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "国防军工",
+ "lv1_id": "lv1_8",
+ "children": [
+ {
+ "lv2": "无人作战与信息化",
+ "lv2_id": "lv2_8_1",
+ "concepts": [
+ "AI军工",
+ "AI无人机军工信息化",
+ "信息支援概念整理",
+ "军工信息化",
+ "军用无人机反无人机",
+ "无人机蜂群"
+ ]
+ },
+ {
+ "lv2": "海军装备",
+ "lv2_id": "lv2_8_2",
+ "concepts": [
+ "军工水面水下作战",
+ "国产航母",
+ "水下军工",
+ "海军",
+ "电磁弹射概念股",
+ "电磁发射设备",
+ "航母福建舰240430"
+ ]
+ },
+ {
+ "lv2": "空军装备",
+ "lv2_id": "lv2_8_3",
+ "concepts": [
+ "军机"
+ ]
+ },
+ {
+ "lv2": "陆军装备",
+ "lv2_id": "lv2_8_4",
+ "concepts": [
+ "远程火力"
+ ]
+ },
+ {
+ "lv2": "军贸出海",
+ "lv2_id": "lv2_8_5",
+ "concepts": [
+ "军贸",
+ "巴印军贸",
+ "巴黎航展"
+ ]
+ },
+ {
+ "lv2": "综合与主题",
+ "lv2_id": "lv2_8_6",
+ "concepts": [
+ "军工",
+ "军工-阅兵",
+ "国防军工"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "政策与主题",
+ "lv1_id": "lv1_9",
+ "children": [
+ {
+ "lv2": "国企改革与市值管理",
+ "lv2_id": "lv2_9_1",
+ "concepts": [
+ "中兵集团并购重组",
+ "中字头",
+ "中字头央企",
+ "中船合并",
+ "央企市值管理",
+ "央国企",
+ "央国企地产",
+ "央国企重组",
+ "安徽国资",
+ "地面兵装",
+ "整车央企重组",
+ "河南国资能源集团重组",
+ "市值管理16条-破净股",
+ "珠海国资",
+ "破净央国企",
+ "破净股合集",
+ "福建国资",
+ "湖北三资改革",
+ "国资高息股",
+ "高送转概念股",
+ "央国企AI一张图"
+ ]
+ },
+ {
+ "lv2": "并购重组",
+ "lv2_id": "lv2_9_2",
+ "concepts": [
+ "IPO终止相关企业重组预期",
+ "上海并购重组",
+ "券商合并预期",
+ "宝德计算机",
+ "并购重组",
+ "并购重组预期",
+ "消费医疗重组预期",
+ "湘财合并大智慧",
+ "科创板并购重组",
+ "秦淮数据",
+ "科技重组",
+ "超聚变借壳预期",
+ "证券",
+ "荣耀股改",
+ "重组-中科院系&海光系"
+ ]
+ },
+ {
+ "lv2": "信创与自主可控",
+ "lv2_id": "lv2_9_3",
+ "concepts": [
+ "信创概念",
+ "关键软件",
+ "国产信创概览",
+ "工业软件",
+ "自主可控",
+ "软件自主可控",
+ "信息安全",
+ "安全概念股",
+ "网络安全",
+ "通信设备",
+ "通信安全"
+ ]
+ },
+ {
+ "lv2": "重大基建",
+ "lv2_id": "lv2_9_4",
+ "concepts": [
+ "三峡水运新通道",
+ "新藏铁路",
+ "新疆概念",
+ "水利",
+ "水利工程",
+ "混凝土减水剂、砂石设备",
+ "节水产业240423",
+ "西部大开发",
+ "西部大开发240424",
+ "西南水电",
+ "西南水电站",
+ "西南水电站-机构测算",
+ "隧洞设备盾构机",
+ "雅下水电对电力设备增量测算-机构",
+ "雅下水电站",
+ "雅下水电站大件物流"
+ ]
+ },
+ {
+ "lv2": "供给侧改革",
+ "lv2_id": "lv2_9_5",
+ "concepts": [
+ "反内卷",
+ "反内卷造纸",
+ "反内卷食用盐",
+ "反内卷快递",
+ "反内卷合集",
+ "生猪",
+ "物流",
+ "牛肉",
+ "统一大市场",
+ "钢铁"
+ ]
+ },
+ {
+ "lv2": "产业升级与制造",
+ "lv2_id": "lv2_9_6",
+ "concepts": [
+ "工业设备更新",
+ "工业母机",
+ "设备更新",
+ "灯塔工厂"
+ ]
+ },
+ {
+ "lv2": "国家战略",
+ "lv2_id": "lv2_9_7",
+ "concepts": [
+ "2025年政府工作报告利好行业及个股",
+ "新型经济",
+ "新质生产力",
+ "深海数智化",
+ "深海经济",
+ "深地经济",
+ "首发经济"
+ ]
+ },
+ {
+ "lv2": "区域发展与自贸区",
+ "lv2_id": "lv2_9_8",
+ "concepts": [
+ "上海自贸区",
+ "免税离境退税",
+ "新型离岸贸易",
+ "海南",
+ "海南自贸区",
+ "海南自贸港",
+ "零售消费免税"
+ ]
+ },
+ {
+ "lv2": "行业监管与规范",
+ "lv2_id": "lv2_9_9",
+ "concepts": [
+ "充电宝",
+ "农药证件厂家",
+ "食品安全",
+ "食品安全全链条",
+ "预制菜"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "周期与材料",
+ "lv1_id": "lv1_10",
+ "children": [
+ {
+ "lv2": "化工",
+ "lv2_id": "lv2_10_1",
+ "children": [
+ {
+ "lv3": "行业趋势",
+ "lv3_id": "lv3_10_1_1",
+ "concepts": [
+ "化工",
+ "化工概念",
+ "化工品涨价",
+ "涨价概念"
+ ]
+ },
+ {
+ "lv3": "具体品种",
+ "lv3_id": "lv3_10_1_2",
+ "concepts": [
+ "TMA偏苯三酸酐",
+ "乙烷",
+ "光引发剂",
+ "六氟磷酸锂",
+ "农药杀虫剂-氯虫苯甲酰胺",
+ "双季戊四醇",
+ "己内酰胺",
+ "有机硅",
+ "正丙醇",
+ "烧碱",
+ "涤纶长丝",
+ "电解液添加剂",
+ "甲苯二异氰酸酯-TDI",
+ "环氧丙烷",
+ "纯碱",
+ "磷化工",
+ "磷化工六氟磷酸锂",
+ "聚酯产业",
+ "维生素",
+ "苯酚丙酮",
+ "超硬材料",
+ "除草剂-烯草酮"
+ ]
+ },
+ {
+ "lv3": "产业链",
+ "lv3_id": "lv3_10_1_3",
+ "concepts": [
+ "有机硅产业链",
+ "电解液产业链"
+ ]
+ }
+ ]
+ },
+ {
+ "lv2": "有色金属",
+ "lv2_id": "lv2_10_2",
+ "concepts": [
+ "化工有色元素周期表",
+ "有色金属",
+ "电解铝",
+ "稀土",
+ "白银",
+ "铅酸电池",
+ "铜",
+ "铜产业",
+ "钨金属",
+ "钴",
+ "钴金属",
+ "钼金属",
+ "锡矿"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "大消费",
+ "lv1_id": "lv1_11",
+ "children": [
+ {
+ "lv2": "文化传媒",
+ "lv2_id": "lv2_11_1",
+ "concepts": [
+ "AI游戏",
+ "乙游",
+ "传媒出海",
+ "出版传媒",
+ "国产游戏黑神话",
+ "周杰伦概念股",
+ "幻兽帕鲁",
+ "影视",
+ "影视IP",
+ "影视传媒",
+ "影视院线",
+ "春节档重点影片(哪吒2)",
+ "智象未来",
+ "漫剧",
+ "游戏",
+ "游戏出海",
+ "疯狂动物城2",
+ "短剧",
+ "诡秘之主",
+ "腾讯短剧重点名单"
+ ]
+ },
+ {
+ "lv2": "新消费",
+ "lv2_id": "lv2_11_2",
+ "concepts": [
+ "上市潮玩盲盒公司",
+ "卡游文创玩具",
+ "布鲁可IP衍生品",
+ "布鲁可谷子经济",
+ "泡泡玛特概念",
+ "潮玩产业",
+ "积木玩具10大",
+ "谷子商城",
+ "谷子经济"
+ ]
+ },
+ {
+ "lv2": "人口与社会",
+ "lv2_id": "lv2_11_3",
+ "concepts": [
+ "三胎",
+ "多胎",
+ "多胎辅助生殖概念240926",
+ "学前教育",
+ "育儿补贴",
+ "辅助生殖"
+ ]
+ },
+ {
+ "lv2": "体育产业",
+ "lv2_id": "lv2_11_4",
+ "concepts": [
+ "体育",
+ "体育产业",
+ "冰雪经济",
+ "川超联赛",
+ "第十五届全运会",
+ "足球",
+ "足球-苏超联赛、体彩"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "数字经济与金融科技",
+ "lv1_id": "lv1_12",
+ "children": [
+ {
+ "lv2": "数据要素",
+ "lv2_id": "lv2_12_1",
+ "concepts": [
+ "RDA概念股",
+ "RWA上链— IoT设备数据采集",
+ "版权",
+ "地理信息",
+ "数据可信",
+ "数据交易所",
+ "数据要素",
+ "政务云政务IT",
+ "跨境数据数据要素"
+ ]
+ },
+ {
+ "lv2": "数字金融",
+ "lv2_id": "lv2_12_2",
+ "concepts": [
+ "上海浦江数链",
+ "互联网金融",
+ "复星稳定币",
+ "数字货币",
+ "树图链概念",
+ "稳定币-蚂蚁国际",
+ "稳定币RWA概念股",
+ "稳定币一体机",
+ "蚂蚁金服",
+ "香港金融牌照"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "全球宏观与贸易",
+ "lv1_id": "lv1_13",
+ "children": [
+ {
+ "lv2": "地缘政治与冲突",
+ "lv2_id": "lv2_13_1",
+ "concepts": [
+ "以伊冲突-天然气",
+ "以伊冲突-油运仓储",
+ "以伊冲突-航运",
+ "以伊冲突-资源化工",
+ "乌克兰战后重建概念",
+ "乌克兰重建",
+ "俄乌重建",
+ "油气",
+ "海外港口",
+ "海事反制",
+ "石油",
+ "航运",
+ "远洋航运",
+ "柬泰战争",
+ "黄岩岛概念股"
+ ]
+ },
+ {
+ "lv2": "贸易政策与关系",
+ "lv2_id": "lv2_13_2",
+ "concepts": [
+ "中美关系",
+ "中俄贸易",
+ "中欧贸易",
+ "出口管制",
+ "反制关税涨价预期",
+ "后关税战受益",
+ "关税减免出口链",
+ "关税豁免",
+ "绒毛浆",
+ "芬太尼管制",
+ "转口贸易出口转内销"
+ ]
+ },
+ {
+ "lv2": "供应链重构",
+ "lv2_id": "lv2_13_3",
+ "concepts": [
+ "二轮车全地形车",
+ "墨西哥汽车零部件",
+ "越南工厂"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "医药健康",
+ "lv1_id": "lv1_14",
+ "children": [
+ {
+ "lv2": "创新药",
+ "lv2_id": "lv2_14_1",
+ "concepts": [
+ "AI制药",
+ "创新药",
+ "创新药双抗",
+ "创新药相关",
+ "医药"
+ ]
+ },
+ {
+ "lv2": "细胞治疗",
+ "lv2_id": "lv2_14_2",
+ "concepts": [
+ "干细胞",
+ "干细胞概念股"
+ ]
+ }
+ ]
+ },
+ {
+ "lv1": "前沿科技",
+ "lv1_id": "lv1_15",
+ "children": [
+ {
+ "lv2": "量子科技",
+ "lv2_id": "lv2_15_1",
+ "concepts": [
+ "量子材料钛酸锶",
+ "量子科技",
+ "量子科技产业链",
+ "量子科技参股公司",
+ "量子计算"
+ ]
+ }
+ ]
+ }
+ ],
+ "uncategorized": []
+}
\ No newline at end of file
diff --git a/max.docx b/max.docx
new file mode 100644
index 00000000..6a9d0710
Binary files /dev/null and b/max.docx differ
diff --git a/pro.docx b/pro.docx
new file mode 100644
index 00000000..35100838
Binary files /dev/null and b/pro.docx differ
diff --git a/src/mocks/handlers/concept.js b/src/mocks/handlers/concept.js
index 8fda2180..26345eda 100644
--- a/src/mocks/handlers/concept.js
+++ b/src/mocks/handlers/concept.js
@@ -521,5 +521,184 @@ export const conceptHandlers = [
query: query,
mode: mode
});
+ }),
+
+ // ============ 层级结构 API ============
+
+ // 获取完整层级结构
+ http.get('/concept-api/hierarchy', async () => {
+ await delay(300);
+
+ console.log('[Mock Concept] 获取层级结构');
+
+ // 模拟层级结构数据
+ const hierarchy = [
+ {
+ id: 'lv1_1',
+ name: '人工智能',
+ concept_count: 98,
+ children: [
+ {
+ id: 'lv2_1_1',
+ name: 'AI基础设施',
+ concept_count: 52,
+ children: [
+ { id: 'lv3_1_1_1', name: 'AI算力硬件', concept_count: 16, concepts: ['AI芯片', 'GPU概念股', '服务器', 'AI一体机'] },
+ { id: 'lv3_1_1_2', name: 'AI关键组件', concept_count: 24, concepts: ['HBM', 'PCB', '光通信', '存储芯片'] },
+ { id: 'lv3_1_1_3', name: 'AI配套设施', concept_count: 12, concepts: ['数据中心', '液冷', '电力设备'] }
+ ]
+ },
+ {
+ id: 'lv2_1_2',
+ name: 'AI模型与软件',
+ concept_count: 13,
+ concepts: ['DeepSeek', 'KIMI', 'SORA概念', '国产大模型']
+ },
+ {
+ id: 'lv2_1_3',
+ name: 'AI应用',
+ concept_count: 17,
+ children: [
+ { id: 'lv3_1_3_1', name: '智能体与陪伴', concept_count: 11, concepts: ['AI伴侣', 'AI智能体', 'AI陪伴'] },
+ { id: 'lv3_1_3_2', name: '行业应用', concept_count: 6, concepts: ['AI编程', '低代码'] }
+ ]
+ }
+ ]
+ },
+ {
+ id: 'lv1_2',
+ name: '半导体',
+ concept_count: 45,
+ children: [
+ { id: 'lv2_2_1', name: '半导体设备', concept_count: 10, concepts: ['光刻机', 'EDA', '半导体设备'] },
+ { id: 'lv2_2_2', name: '半导体材料', concept_count: 8, concepts: ['光刻胶', '半导体材料', '石英砂'] },
+ { id: 'lv2_2_3', name: '芯片设计与制造', concept_count: 10, concepts: ['第三代半导体', '碳化硅', '功率半导体'] },
+ { id: 'lv2_2_4', name: '先进封装', concept_count: 5, concepts: ['玻璃基板', '半导体封测'] }
+ ]
+ },
+ {
+ id: 'lv1_3',
+ name: '机器人',
+ concept_count: 42,
+ children: [
+ { id: 'lv2_3_1', name: '人形机器人整机', concept_count: 20, concepts: ['特斯拉机器人', '人形机器人', '智元机器人'] },
+ { id: 'lv2_3_2', name: '机器人核心零部件', concept_count: 12, concepts: ['滚柱丝杆', '电子皮肤', '轴向磁通电机'] },
+ { id: 'lv2_3_3', name: '其他类型机器人', concept_count: 10, concepts: ['工业机器人', '机器狗', '外骨骼机器人'] }
+ ]
+ },
+ {
+ id: 'lv1_4',
+ name: '消费电子',
+ concept_count: 38,
+ children: [
+ { id: 'lv2_4_1', name: '智能终端', concept_count: 8, concepts: ['AI PC', 'AI手机'] },
+ { id: 'lv2_4_2', name: 'XR与空间计算', concept_count: 14, concepts: ['AR眼镜', 'MR', '智能眼镜'] },
+ { id: 'lv2_4_3', name: '华为产业链', concept_count: 16, concepts: ['华为Mate70', '鸿蒙', '华为昇腾'] }
+ ]
+ },
+ {
+ id: 'lv1_5',
+ name: '智能驾驶与汽车',
+ concept_count: 35,
+ children: [
+ { id: 'lv2_5_1', name: '自动驾驶解决方案', concept_count: 12, concepts: ['Robotaxi', '无人驾驶', '特斯拉FSD'] },
+ { id: 'lv2_5_2', name: '智能汽车产业链', concept_count: 15, concepts: ['比亚迪产业链', '小米汽车产业链'] },
+ { id: 'lv2_5_3', name: '车路协同', concept_count: 8, concepts: ['车路云一体化', '车路协同'] }
+ ]
+ },
+ {
+ id: 'lv1_6',
+ name: '新能源与电力',
+ concept_count: 52,
+ children: [
+ { id: 'lv2_6_1', name: '新型电池技术', concept_count: 18, concepts: ['固态电池', '钠离子电池', '硅基负极'] },
+ { id: 'lv2_6_2', name: '电力设备与电网', concept_count: 20, concepts: ['电力', '变压器出海', '燃料电池'] },
+ { id: 'lv2_6_3', name: '清洁能源', concept_count: 14, concepts: ['光伏', '核电', '可控核聚变'] }
+ ]
+ },
+ {
+ id: 'lv1_7',
+ name: '空天经济',
+ concept_count: 28,
+ children: [
+ { id: 'lv2_7_1', name: '低空经济', concept_count: 14, concepts: ['低空经济', 'eVTOL', '飞行汽车'] },
+ { id: 'lv2_7_2', name: '商业航天', concept_count: 14, concepts: ['卫星互联网', '商业航天', '北斗导航'] }
+ ]
+ },
+ {
+ id: 'lv1_8',
+ name: '国防军工',
+ concept_count: 25,
+ children: [
+ { id: 'lv2_8_1', name: '无人作战与信息化', concept_count: 10, concepts: ['AI军工', '无人机蜂群', '军工信息化'] },
+ { id: 'lv2_8_2', name: '海军装备', concept_count: 8, concepts: ['国产航母', '电磁弹射'] },
+ { id: 'lv2_8_3', name: '军贸出海', concept_count: 7, concepts: ['军贸', '巴黎航展'] }
+ ]
+ }
+ ];
+
+ return HttpResponse.json({
+ hierarchy,
+ total_lv1: hierarchy.length,
+ total_concepts: hierarchy.reduce((acc, h) => acc + h.concept_count, 0)
+ });
+ }),
+
+ // 获取层级统计数据(包含涨跌幅)
+ http.get('/concept-api/statistics/hierarchy', async () => {
+ await delay(300);
+
+ console.log('[Mock Concept] 获取层级统计数据');
+
+ const statistics = [
+ { lv1: '人工智能', concept_count: 98, avg_change_pct: 3.56, top_gainer: 'DeepSeek', top_gainer_change: 15.23 },
+ { lv1: '半导体', concept_count: 45, avg_change_pct: 2.12, top_gainer: '光刻机', top_gainer_change: 8.76 },
+ { lv1: '机器人', concept_count: 42, avg_change_pct: 4.28, top_gainer: '人形机器人', top_gainer_change: 12.45 },
+ { lv1: '消费电子', concept_count: 38, avg_change_pct: 1.45, top_gainer: 'AR眼镜', top_gainer_change: 6.78 },
+ { lv1: '智能驾驶与汽车', concept_count: 35, avg_change_pct: 2.89, top_gainer: 'Robotaxi', top_gainer_change: 9.32 },
+ { lv1: '新能源与电力', concept_count: 52, avg_change_pct: -0.56, top_gainer: '固态电池', top_gainer_change: 5.67 },
+ { lv1: '空天经济', concept_count: 28, avg_change_pct: 3.12, top_gainer: '低空经济', top_gainer_change: 11.23 },
+ { lv1: '国防军工', concept_count: 25, avg_change_pct: 1.78, top_gainer: 'AI军工', top_gainer_change: 7.89 }
+ ];
+
+ return HttpResponse.json({
+ statistics,
+ total_lv1: statistics.length,
+ total_concepts: statistics.reduce((acc, s) => acc + s.concept_count, 0),
+ market_avg_change: 2.34,
+ update_time: new Date().toISOString()
+ });
+ }),
+
+ // 获取指定层级的概念列表
+ http.get('/concept-api/hierarchy/:lv1Id', async ({ params, request }) => {
+ await delay(300);
+
+ const { lv1Id } = params;
+ const url = new URL(request.url);
+ const lv2Id = url.searchParams.get('lv2_id');
+
+ console.log('[Mock Concept] 获取层级概念列表:', { lv1Id, lv2Id });
+
+ // 返回该层级下的概念列表
+ let concepts = generatePopularConcepts(20);
+
+ // 添加层级信息
+ concepts = concepts.map(c => ({
+ ...c,
+ hierarchy: {
+ lv1: '人工智能',
+ lv1_id: lv1Id,
+ lv2: lv2Id ? 'AI基础设施' : null,
+ lv2_id: lv2Id
+ }
+ }));
+
+ return HttpResponse.json({
+ concepts,
+ total: concepts.length,
+ lv1_id: lv1Id,
+ lv2_id: lv2Id
+ });
})
];
diff --git a/src/views/Concept/components/BreadcrumbNav.js b/src/views/Concept/components/BreadcrumbNav.js
new file mode 100644
index 00000000..a8e1acf2
--- /dev/null
+++ b/src/views/Concept/components/BreadcrumbNav.js
@@ -0,0 +1,170 @@
+/**
+ * BreadcrumbNav - 层级筛选面包屑导航
+ *
+ * 功能:
+ * 1. 显示当前选中的层级路径
+ * 2. 支持点击返回上级或清除筛选
+ */
+import React from 'react';
+import {
+ Box,
+ HStack,
+ Text,
+ Icon,
+ Badge,
+ IconButton,
+ Flex,
+ Tooltip,
+} from '@chakra-ui/react';
+import { ChevronRightIcon, CloseIcon } from '@chakra-ui/icons';
+import { FaLayerGroup, FaFilter, FaTimes, FaHome } from 'react-icons/fa';
+
+const BreadcrumbNav = ({
+ filter,
+ onClearFilter,
+ onNavigate,
+}) => {
+ // 如果没有筛选条件,不显示
+ if (!filter || (!filter.lv1 && !filter.lv2 && !filter.lv3)) {
+ return null;
+ }
+
+ const breadcrumbs = [];
+
+ // 构建面包屑路径
+ if (filter.lv1) {
+ breadcrumbs.push({
+ label: filter.lv1,
+ level: 'lv1',
+ onClick: () => onNavigate({ lv1: filter.lv1, lv2: null, lv3: null }),
+ });
+ }
+
+ if (filter.lv2) {
+ breadcrumbs.push({
+ label: filter.lv2,
+ level: 'lv2',
+ onClick: () => onNavigate({ lv1: filter.lv1, lv2: filter.lv2, lv3: null }),
+ });
+ }
+
+ if (filter.lv3) {
+ breadcrumbs.push({
+ label: filter.lv3,
+ level: 'lv3',
+ onClick: null, // 最后一级不可点击
+ });
+ }
+
+ return (
+