fix: delete all history records by stock code (#1987)

* fix: delete all history records by stock code

* fix: reject blank history stock codes

---------

Co-authored-by: zhulinsen <42829555+ZhuLinsen@users.noreply.github.com>
This commit is contained in:
Kushida
2026-07-12 10:02:54 +03:00
committed by GitHub
parent 6fda28b4be
commit 44371c2961
3 changed files with 85 additions and 6 deletions

View File

@@ -60,6 +60,7 @@ from src.market_phase_summary import extract_market_phase_summary
logger = logging.getLogger(__name__)
router = APIRouter()
_DELETE_BY_CODE_BATCH_SIZE = 10_000
def _normalize_code_for_grouping(code: str) -> str:
@@ -233,6 +234,7 @@ def get_history_list(
response_model=DeleteHistoryResponse,
responses={
200: {"description": "删除成功"},
400: {"description": "股票代码不能为空", "model": ErrorResponse},
404: {"description": "未找到记录", "model": ErrorResponse},
500: {"description": "服务器错误", "model": ErrorResponse},
},
@@ -245,12 +247,33 @@ def delete_history_by_code(
) -> DeleteHistoryResponse:
try:
candidates = HistoryService._history_code_filter_candidates(stock_code)
records, _ = db_manager.get_analysis_history_paginated(code=candidates, limit=10000)
record_ids = [r.id for r in records if r.id is not None]
if not record_ids:
return DeleteHistoryResponse(deleted=0)
deleted = db_manager.delete_analysis_history_records(record_ids)
if not candidates:
raise HTTPException(
status_code=400,
detail={"error": "invalid_request", "message": "stock_code 不能为空"},
)
deleted = 0
while True:
records, _ = db_manager.get_analysis_history_paginated(
code=candidates,
limit=_DELETE_BY_CODE_BATCH_SIZE,
)
record_ids = [r.id for r in records if r.id is not None]
if not record_ids:
break
batch_deleted = db_manager.delete_analysis_history_records(record_ids)
if batch_deleted == 0:
raise RuntimeError("history deletion made no progress")
deleted += batch_deleted
if len(records) < _DELETE_BY_CODE_BATCH_SIZE:
break
return DeleteHistoryResponse(deleted=deleted)
except HTTPException:
raise
except Exception as e:
logger.error(f"按股票代码删除历史记录失败: {e}", exc_info=True)
raise HTTPException(

View File

@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
> For user-friendly release highlights, see the [GitHub Releases](https://github.com/ZhuLinsen/daily_stock_analysis/releases) page.
## [Unreleased]
- [修复] 按股票代码删除历史记录时分批清理全部匹配项,并拒绝空白代码,避免超过 10000 条后残留记录或无筛选删除。
- [修复] 市场结构概念排行为空或超时时复用本轮负结果,避免批量个股分析重复请求同一概念排行数据源。
- [修复] Windows/macOS 桌面后端打包显式收集并校验 AkShare `file_fold/calendar.json`,避免发行包因缺少交易日历 package data 导致热点题材和选股日线增强降级。
- [改进] 为 multi-agent DecisionAgent 增加内部低敏分歧摘要输入管线,作为 #1904 P1 解释输出的前置 plumbing不改变 public API、dashboard schema 或最终解释字段。

View File

@@ -16,6 +16,7 @@ import tempfile
import unittest
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
# Keep this test runnable when optional LLM runtime deps are not installed.
@@ -27,10 +28,11 @@ except ModuleNotFoundError:
try:
from fastapi.testclient import TestClient
from api.app import create_app
from api.v1.endpoints.history import get_history_detail, get_history_list, get_stock_bar
from api.v1.endpoints.history import delete_history_by_code, get_history_detail, get_history_list, get_stock_bar
except ModuleNotFoundError:
TestClient = None
create_app = None
delete_history_by_code = None
get_history_detail = None
get_history_list = None
get_stock_bar = None
@@ -184,6 +186,59 @@ class AnalysisHistoryTestCase(unittest.TestCase):
self.assertEqual(getattr(raised.exception, "status_code", None), 500)
def test_delete_history_by_code_deletes_more_than_one_lookup_batch(self) -> None:
if delete_history_by_code is None:
self.skipTest("fastapi is not installed in this test environment")
remaining = {record_id: SimpleNamespace(id=record_id) for record_id in range(1, 10_002)}
db = MagicMock()
def get_records(*, code, limit=20, offset=0, **_kwargs):
records = list(remaining.values())[offset:offset + limit]
return records, len(remaining)
def delete_records(record_ids):
deleted = 0
for record_id in record_ids:
if remaining.pop(record_id, None) is not None:
deleted += 1
return deleted
db.get_analysis_history_paginated.side_effect = get_records
db.delete_analysis_history_records.side_effect = delete_records
response = delete_history_by_code("600519", db_manager=db)
self.assertEqual(response.deleted, 10_001)
self.assertEqual(remaining, {})
self.assertEqual(db.get_analysis_history_paginated.call_count, 2)
def test_delete_history_by_code_rejects_blank_code_before_query(self) -> None:
if delete_history_by_code is None:
self.skipTest("fastapi is not installed in this test environment")
record_id = self._save_history("query_delete_blank_code")
with (
patch.object(
self.db,
"get_analysis_history_paginated",
wraps=self.db.get_analysis_history_paginated,
) as query,
patch.object(
self.db,
"delete_analysis_history_records",
wraps=self.db.delete_analysis_history_records,
) as delete,
):
with self.assertRaises(Exception) as raised:
delete_history_by_code(" ", db_manager=self.db)
self.assertEqual(getattr(raised.exception, "status_code", None), 400)
query.assert_not_called()
delete.assert_not_called()
with self.db.get_session() as session:
self.assertIsNotNone(session.query(AnalysisHistory).filter(AnalysisHistory.id == record_id).first())
def _build_result(self) -> AnalysisResult:
"""构造分析结果"""
return AnalysisResult(