"""题材数据路由:题材列表、题材详情、题材相关股票""" from fastapi import APIRouter, Query, HTTPException from fastapi.responses import JSONResponse from database import get_connection, dict_from_row from services import themes router = APIRouter() # 上游反代/CDN 可能按 path 缓存,显式禁止缓存 _NO_CACHE_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"} @router.get("", summary="题材列表") async def theme_list( sort_field: int = Query(1, ge=1, le=5, description="排序字段:1=涨幅 3=强度 4=热度排名 5=成交额"), asc: bool = Query(False, description="True=升序, False=降序"), ): if sort_field not in (1, 3, 4, 5): raise HTTPException(status_code=400, detail="排序字段仅支持 1/3/4/5") data = await themes.fetch_theme_list(sort_field, asc) return JSONResponse( {"data": data, "count": len(data), "sort_field": sort_field, "asc": asc}, headers=_NO_CACHE_HEADERS, ) @router.get("/graph", summary="热点穿透:题材-股票网状关系图") async def theme_graph( sort_field: int = Query(1, description="题材排序:1=涨幅 4=热度"), top: int = Query(30, ge=1, le=60, description="题材数量"), limit: int = Query(1000, ge=100, le=2000, description="下发的股票节点上限(按穿透度取前 N 只)"), ): if sort_field not in (1, 4): raise HTTPException(status_code=400, detail="排序字段仅支持 1(涨幅)/4(热度)") result = await themes.fetch_theme_graph(sort_field, top, limit) return JSONResponse(result, headers=_NO_CACHE_HEADERS) @router.get("/history", summary="指定交易日题材涨幅前20") async def theme_history(date: str = Query(..., description="交易日 YYYY-MM-DD")): conn = get_connection() try: rows = conn.execute( "SELECT * FROM daily_top_themes WHERE trade_date = ? ORDER BY rank ASC", (date,) ).fetchall() items = [dict_from_row(r) for r in rows] return JSONResponse({"date": date, "items": items}, headers=_NO_CACHE_HEADERS) finally: conn.close() @router.get("/{theme_code}/news", summary="题材相关新闻(分页)") async def theme_news( theme_code: str, page_num: int = Query(1, ge=1, description="页码"), page_size: int = Query(10, ge=1, le=50, description="每页条数"), max_eu_time: str = Query("", description="分页游标(上一页返回的 maxEuTime)"), ): result = await themes.fetch_theme_news(theme_code, page_num, max_eu_time, page_size) if result is None: return JSONResponse( {"data": None, "theme_code": theme_code}, headers=_NO_CACHE_HEADERS, ) return JSONResponse( {"data": result, "theme_code": theme_code}, headers=_NO_CACHE_HEADERS, ) @router.get("/{theme_code}/quote", summary="单题材实时行情(强度/热度/涨幅)") async def theme_quote(theme_code: str): result = await themes.fetch_theme_quote(theme_code) if result is None: return JSONResponse( {"data": None, "theme_code": theme_code}, headers=_NO_CACHE_HEADERS, ) return JSONResponse( {"data": result, "theme_code": theme_code}, headers=_NO_CACHE_HEADERS, ) @router.get("/{theme_code}/detail", summary="题材详情") async def theme_detail(theme_code: str): data = await themes.fetch_theme_detail(theme_code) if not data: return JSONResponse( {"data": None, "theme_code": theme_code}, headers=_NO_CACHE_HEADERS, ) return JSONResponse( {"data": data, "theme_code": theme_code}, headers=_NO_CACHE_HEADERS, ) @router.get("/{theme_code}/stocks", summary="题材相关股票(全部)") async def theme_stocks(theme_code: str): result = await themes.fetch_theme_stocks(theme_code) return JSONResponse( { "data": result.get("stockList", []), "statistic": result.get("statistic", {}), "total": result.get("total", 0), "theme_code": theme_code, }, headers=_NO_CACHE_HEADERS, )