From a448886f67f56c032e3432402a38323ef9c9a6e2 Mon Sep 17 00:00:00 2001 From: zhulinsen <42829555+ZhuLinsen@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:18:02 +0800 Subject: [PATCH] fix: restore stock bar summary badges (#1848) --- api/v1/endpoints/history.py | 64 ++++++++++++++++--- .../src/components/history/StockBarItem.tsx | 9 +-- .../history/__tests__/StockBarItem.test.tsx | 12 ++-- docs/CHANGELOG.md | 2 + tests/test_analysis_history.py | 39 +++++++++++ 5 files changed, 107 insertions(+), 19 deletions(-) diff --git a/api/v1/endpoints/history.py b/api/v1/endpoints/history.py index 3fb47d201..db02141b0 100644 --- a/api/v1/endpoints/history.py +++ b/api/v1/endpoints/history.py @@ -10,7 +10,7 @@ """ import logging -from typing import Optional +from typing import Any, Optional from fastapi import APIRouter, HTTPException, Query, Depends, Body @@ -71,6 +71,47 @@ def _normalize_code_for_grouping(code: str) -> str: return normalize_stock_code(code or "") +def _raw_result_value(raw_result: Any, key: str) -> Any: + if not isinstance(raw_result, dict): + return None + + value = raw_result.get(key) + if value is not None and value != "": + return value + + for container_key in ("summary", "dashboard"): + container = raw_result.get(container_key) + if isinstance(container, dict): + nested_value = container.get(key) + if nested_value is not None and nested_value != "": + return nested_value + + return None + + +def _coalesce_text(*values: Any) -> Optional[str]: + for value in values: + if value is None: + continue + text = str(value).strip() + if text: + return text + return None + + +def _coalesce_int(*values: Any) -> Optional[int]: + for value in values: + if value is None or isinstance(value, bool): + continue + if isinstance(value, str) and not value.strip(): + continue + try: + return int(float(value)) + except (TypeError, ValueError): + continue + return None + + @router.get( "", response_model=HistoryListResponse, @@ -285,15 +326,20 @@ def get_stock_bar( record = seen[norm_code] raw_result = parse_json_field(getattr(record, "raw_result", None)) model_used = raw_result.get("model_used") if isinstance(raw_result, dict) else None + sentiment_score = _coalesce_int( + record.sentiment_score, + _raw_result_value(raw_result, "sentiment_score"), + ) + operation_advice = _coalesce_text( + record.operation_advice, + _raw_result_value(raw_result, "operation_advice"), + ) action_fields = build_action_fields( - operation_advice=( - raw_result.get("operation_advice") if isinstance(raw_result, dict) else None - ) - or record.operation_advice, - explicit_action=raw_result.get("action") if isinstance(raw_result, dict) else None, + operation_advice=operation_advice, + explicit_action=_raw_result_value(raw_result, "action"), report_type=record.report_type, report_language=normalize_report_language( - raw_result.get("report_language") if isinstance(raw_result, dict) else None + _raw_result_value(raw_result, "report_language") ), ) @@ -308,8 +354,8 @@ def get_stock_bar( stock_code=display_stock_code, stock_name=record.name, report_type=record.report_type, - sentiment_score=record.sentiment_score, - operation_advice=record.operation_advice, + sentiment_score=sentiment_score, + operation_advice=operation_advice, action=action_fields["action"], action_label=action_fields["action_label"], analysis_count=analysis_count, diff --git a/apps/dsa-web/src/components/history/StockBarItem.tsx b/apps/dsa-web/src/components/history/StockBarItem.tsx index 7931f4986..00462ff37 100644 --- a/apps/dsa-web/src/components/history/StockBarItem.tsx +++ b/apps/dsa-web/src/components/history/StockBarItem.tsx @@ -26,14 +26,15 @@ export const StockBarItemComponent: React.FC = ({ isMarketReview = false, }) => { const { language, t } = useUiLanguage(); - const sentimentColor = item.sentimentScore !== undefined ? getSentimentColor(item.sentimentScore) : null; + const sentimentScore = typeof item.sentimentScore === 'number' ? item.sentimentScore : null; + const sentimentColor = sentimentScore !== null ? getSentimentColor(sentimentScore) : null; const stockName = item.stockName || item.stockCode; const actionLabels = buildDecisionActionLabelMap(t); const operationLabel = getDecisionActionLabel( item.action, item.actionLabel, item.operationAdvice, - null, + t('history.sentiment'), actionLabels, ); const phaseLabel = getMarketPhaseSummaryLabel(item.marketPhaseSummary, language) @@ -85,7 +86,7 @@ export const StockBarItemComponent: React.FC = ({ > {t('stockBar.market')} - ) : operationLabel && sentimentColor ? ( + ) : sentimentColor ? ( = ({ backgroundColor: `${sentimentColor}10`, }} > - {operationLabel} {item.sentimentScore} + {operationLabel} {sentimentScore} ) : null} {onDelete && ( diff --git a/apps/dsa-web/src/components/history/__tests__/StockBarItem.test.tsx b/apps/dsa-web/src/components/history/__tests__/StockBarItem.test.tsx index 3a61f62ae..9a98426ea 100644 --- a/apps/dsa-web/src/components/history/__tests__/StockBarItem.test.tsx +++ b/apps/dsa-web/src/components/history/__tests__/StockBarItem.test.tsx @@ -121,7 +121,7 @@ describe('StockBarItemComponent', () => { const actions = screen.getByTestId('history-card-actions'); expect(within(actions).queryByText('buy 28')).not.toBeInTheDocument(); - expect(within(actions).queryByText(/28/)).not.toBeInTheDocument(); + expect(within(actions).getByText(/28/)).toBeInTheDocument(); }); it('does not render financial compound English advice as an action badge', () => { @@ -141,7 +141,7 @@ describe('StockBarItemComponent', () => { let actions = screen.getByTestId('history-card-actions'); expect(within(actions).queryByText('持有 28')).not.toBeInTheDocument(); - expect(within(actions).queryByText(/28/)).not.toBeInTheDocument(); + expect(within(actions).getByText(/28/)).toBeInTheDocument(); rerender( { actions = screen.getByTestId('history-card-actions'); expect(within(actions).queryByText('卖出 31')).not.toBeInTheDocument(); - expect(within(actions).queryByText(/31/)).not.toBeInTheDocument(); + expect(within(actions).getByText(/31/)).toBeInTheDocument(); }); it('does not render Chinese financial context legacy advice as an action badge', () => { @@ -179,7 +179,7 @@ describe('StockBarItemComponent', () => { let actions = screen.getByTestId('history-card-actions'); expect(within(actions).queryByText('买入 32')).not.toBeInTheDocument(); - expect(within(actions).queryByText(/32/)).not.toBeInTheDocument(); + expect(within(actions).getByText(/32/)).toBeInTheDocument(); rerender( { actions = screen.getByTestId('history-card-actions'); expect(within(actions).queryByText('卖出 34')).not.toBeInTheDocument(); - expect(within(actions).queryByText(/34/)).not.toBeInTheDocument(); + expect(within(actions).getByText(/34/)).toBeInTheDocument(); }); it('does not render multi-guard legacy advice as an action badge', () => { @@ -218,6 +218,6 @@ describe('StockBarItemComponent', () => { const actions = screen.getByTestId('history-card-actions'); expect(within(actions).queryByText('回避 28')).not.toBeInTheDocument(); expect(within(actions).queryByText('预警 28')).not.toBeInTheDocument(); - expect(within(actions).queryByText(/28/)).not.toBeInTheDocument(); + expect(within(actions).getByText(/28/)).toBeInTheDocument(); }); }); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index da67aa2fb..ee6fecf9e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). +- [修复] 修复 Web 首页个股栏在 stock-bar 摘要字段缺失或动作建议无法归类时隐藏情绪分与建议标识的问题。 + ## [3.24.1] - 2026-06-28 ### 修复 diff --git a/tests/test_analysis_history.py b/tests/test_analysis_history.py index 68e501683..b4acdc588 100644 --- a/tests/test_analysis_history.py +++ b/tests/test_analysis_history.py @@ -755,6 +755,45 @@ class AnalysisHistoryTestCase(unittest.TestCase): self.assertEqual(response.items[0].action, "avoid") self.assertEqual(response.items[0].action_label, "回避") + def test_stock_bar_item_falls_back_to_raw_result_summary_fields(self) -> None: + if get_stock_bar is None: + self.skipTest("fastapi is not installed in this test environment") + + result = self._build_result() + result.operation_advice = "Hold" + result.report_language = "en" + + saved = self.db.save_analysis_history( + result=result, + query_id="query_stock_bar_raw_fallback", + report_type="detailed", + news_content="stock report", + context_snapshot=None, + save_snapshot=False, + ) + self.assertGreater(saved, 0) + + with self.db.session_scope() as session: + row = session.query(AnalysisHistory).filter( + AnalysisHistory.query_id == "query_stock_bar_raw_fallback" + ).first() + self.assertIsNotNone(row) + row.sentiment_score = None + row.operation_advice = None + + response = get_stock_bar( + start_date=None, + end_date=None, + limit=10, + db_manager=self.db, + ) + + self.assertEqual(len(response.items), 1) + self.assertEqual(response.items[0].sentiment_score, 78) + self.assertEqual(response.items[0].operation_advice, "Hold") + self.assertEqual(response.items[0].action, "hold") + self.assertEqual(response.items[0].action_label, "Hold") + def test_history_detail_uses_service_resolved_action_fields(self) -> None: if get_history_detail is None: self.skipTest("fastapi is not installed in this test environment")