mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
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:
@@ -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(
|
||||
|
||||
@@ -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 或最终解释字段。
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user