# -*- coding: utf-8 -*- """ =================================== 历史记录接口 =================================== 职责: 1. 提供 GET /api/v1/history 历史列表查询接口 2. 提供 GET /api/v1/history/{query_id} 历史详情查询接口 """ import logging from typing import Optional from fastapi import APIRouter, HTTPException, Query, Depends from api.deps import get_database_manager from api.v1.schemas.history import ( HistoryListResponse, HistoryItem, NewsIntelItem, NewsIntelResponse, AnalysisReport, ReportMeta, ReportSummary, ReportStrategy, ReportDetails, ) from api.v1.schemas.common import ErrorResponse from src.storage import DatabaseManager from src.services.history_service import HistoryService logger = logging.getLogger(__name__) router = APIRouter() @router.get( "", response_model=HistoryListResponse, responses={ 200: {"description": "历史记录列表"}, 500: {"description": "服务器错误", "model": ErrorResponse}, }, summary="获取历史分析列表", description="分页获取历史分析记录摘要,支持按股票代码和日期范围筛选" ) def get_history_list( stock_code: Optional[str] = Query(None, description="股票代码筛选"), start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD)"), end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD)"), page: int = Query(1, ge=1, description="页码(从 1 开始)"), limit: int = Query(20, ge=1, le=100, description="每页数量"), db_manager: DatabaseManager = Depends(get_database_manager) ) -> HistoryListResponse: """ 获取历史分析列表 分页获取历史分析记录摘要,支持按股票代码和日期范围筛选 Args: stock_code: 股票代码筛选 start_date: 开始日期 end_date: 结束日期 page: 页码 limit: 每页数量 db_manager: 数据库管理器依赖 Returns: HistoryListResponse: 历史记录列表 """ try: service = HistoryService(db_manager) # 使用 def 而非 async def,FastAPI 自动在线程池中执行 result = service.get_history_list( stock_code=stock_code, start_date=start_date, end_date=end_date, page=page, limit=limit ) # 转换为响应模型 items = [ HistoryItem( query_id=item.get("query_id", ""), stock_code=item.get("stock_code", ""), stock_name=item.get("stock_name"), report_type=item.get("report_type"), sentiment_score=item.get("sentiment_score"), operation_advice=item.get("operation_advice"), created_at=item.get("created_at") ) for item in result.get("items", []) ] return HistoryListResponse( total=result.get("total", 0), page=page, limit=limit, items=items ) except Exception as e: logger.error(f"查询历史列表失败: {e}", exc_info=True) raise HTTPException( status_code=500, detail={ "error": "internal_error", "message": f"查询历史列表失败: {str(e)}" } ) @router.get( "/{query_id}", response_model=AnalysisReport, responses={ 200: {"description": "报告详情"}, 404: {"description": "报告不存在", "model": ErrorResponse}, 500: {"description": "服务器错误", "model": ErrorResponse}, }, summary="获取历史报告详情", description="根据 query_id 获取完整的历史分析报告" ) def get_history_detail( query_id: str, db_manager: DatabaseManager = Depends(get_database_manager) ) -> AnalysisReport: """ 获取历史报告详情 根据 query_id 获取完整的历史分析报告 Args: query_id: 分析记录唯一标识 db_manager: 数据库管理器依赖 Returns: AnalysisReport: 完整分析报告 Raises: HTTPException: 404 - 报告不存在 """ try: service = HistoryService(db_manager) # 使用 def 而非 async def,FastAPI 自动在线程池中执行 result = service.get_history_detail(query_id) if result is None: raise HTTPException( status_code=404, detail={ "error": "not_found", "message": f"未找到 query_id={query_id} 的分析记录" } ) # 从 context_snapshot 中提取价格信息 current_price = None change_pct = None context_snapshot = result.get("context_snapshot") if context_snapshot and isinstance(context_snapshot, dict): # 尝试从 enhanced_context.realtime 获取 enhanced_context = context_snapshot.get("enhanced_context") or {} realtime = enhanced_context.get("realtime") or {} current_price = realtime.get("price") change_pct = realtime.get("change_pct") or realtime.get("change_60d") # 也尝试从 realtime_quote_raw 获取 if current_price is None: realtime_quote_raw = context_snapshot.get("realtime_quote_raw") or {} current_price = realtime_quote_raw.get("price") change_pct = change_pct or realtime_quote_raw.get("change_pct") or realtime_quote_raw.get("pct_chg") # 构建响应模型 meta = ReportMeta( query_id=result.get("query_id", query_id), stock_code=result.get("stock_code", ""), stock_name=result.get("stock_name"), report_type=result.get("report_type"), created_at=result.get("created_at"), current_price=current_price, change_pct=change_pct ) summary = ReportSummary( analysis_summary=result.get("analysis_summary"), operation_advice=result.get("operation_advice"), trend_prediction=result.get("trend_prediction"), sentiment_score=result.get("sentiment_score"), sentiment_label=result.get("sentiment_label") ) strategy = ReportStrategy( ideal_buy=result.get("ideal_buy"), secondary_buy=result.get("secondary_buy"), stop_loss=result.get("stop_loss"), take_profit=result.get("take_profit") ) details = ReportDetails( news_content=result.get("news_content"), raw_result=result.get("raw_result"), context_snapshot=result.get("context_snapshot") ) return AnalysisReport( meta=meta, summary=summary, strategy=strategy, details=details ) except HTTPException: raise except Exception as e: logger.error(f"查询历史详情失败: {e}", exc_info=True) raise HTTPException( status_code=500, detail={ "error": "internal_error", "message": f"查询历史详情失败: {str(e)}" } ) @router.get( "/{query_id}/news", response_model=NewsIntelResponse, responses={ 200: {"description": "新闻情报列表"}, 500: {"description": "服务器错误", "model": ErrorResponse}, }, summary="获取历史报告关联新闻", description="根据 query_id 获取关联的新闻情报列表(为空也返回 200)" ) def get_history_news( query_id: str, limit: int = Query(20, ge=1, le=100, description="返回数量限制"), db_manager: DatabaseManager = Depends(get_database_manager) ) -> NewsIntelResponse: """ 获取历史报告关联新闻 Args: query_id: 分析记录唯一标识 limit: 返回数量限制 db_manager: 数据库管理器依赖 Returns: NewsIntelResponse: 新闻情报列表 """ try: service = HistoryService(db_manager) items = service.get_news_intel(query_id=query_id, limit=limit) response_items = [ NewsIntelItem( title=item.get("title", ""), snippet=item.get("snippet"), url=item.get("url", "") ) for item in items ] return NewsIntelResponse( total=len(response_items), items=response_items ) except Exception as e: logger.error(f"查询新闻情报失败: {e}", exc_info=True) raise HTTPException( status_code=500, detail={ "error": "internal_error", "message": f"查询新闻情报失败: {str(e)}" } )