Skip to content

Commit 843b553

Browse files
committed
fix: avoid stale pending update after empty sync
1 parent 006a268 commit 843b553

2 files changed

Lines changed: 86 additions & 5 deletions

File tree

quantclass_sync_internal/data_query.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .constants import (
1616
ENCODING_CANDIDATES, KNOWN_DATASETS, TIMESTAMP_FILE_NAME,
1717
BUSINESS_DAY_ONLY_PRODUCTS, FINANCIAL_PRODUCTS, NOTICE_PRODUCTS,
18-
INFRA_PRODUCTS,
18+
INFRA_PRODUCTS, REASON_NO_VALID_OUTPUT,
1919
)
2020
from .csv_engine import write_csv_payload
2121
from .models import CsvPayload, log_error, RULES
@@ -173,17 +173,35 @@ def get_products_overview(
173173
local_date = read_local_timestamp_date(data_root, product)
174174
last = last_results.get(product, {})
175175
last_status = last.get("status", "")
176+
last_reason = last.get("reason_code", "")
177+
last_source = last.get("source", "")
178+
last_date = _parse_date(last.get("date_time", ""))
179+
freshness_anchor = _parse_date(last.get("checked_at", "")) or last_date
180+
last_result_fresh = (
181+
freshness_anchor is not None
182+
and (today - freshness_anchor).days <= _STALE_GRACE_DAYS
183+
)
176184

177185
# 优先用传入的 API 实时日期(检查更新按钮场景)
178186
api_date = _parse_date((api_latest_dates or {}).get(product, ""))
179187
if api_date is not None:
180-
ref_date = api_date
188+
# 同一最新日期已在同步阶段确认无有效输出时,不再把它计为待更新。
189+
if (
190+
last_source == "sync"
191+
and last_reason == REASON_NO_VALID_OUTPUT
192+
and last_result_fresh
193+
and last_date == api_date
194+
and _parse_date(local_date) is not None
195+
):
196+
ref_date = _parse_date(local_date)
197+
else:
198+
ref_date = api_date
181199
else:
182200
# 用缓存的 API 日期作为参考,避免周末/假日误报落后;
183201
# 缓存超过宽限期或无缓存时降级回 today,提示可能有新数据。
184-
# 宽限期从"上次查询/同步时间"算起(checked_at 优先,降级到 date_time
185-
cached_api_date = _parse_date(last.get("date_time", ""))
186-
freshness_anchor = _parse_date(last.get("checked_at", "")) or cached_api_date
202+
# 仅排除明确来自同步结果(source="sync")的 date_time
203+
# 旧安装缺少 source 字段时保持兼容,仍沿用原缓存逻辑。
204+
cached_api_date = last_date if last_source != "sync" else None
187205
cache_fresh = (
188206
cached_api_date is not None
189207
and freshness_anchor is not None

tests/test_data_query.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ def _write_report(self, filename: str, products: list):
126126
json.dumps(existing, ensure_ascii=False), encoding="utf-8"
127127
)
128128

129+
def _write_product_last_status(self, payload: dict) -> None:
130+
"""直接写入 product_last_status.json,用于覆盖 source/checked_at 等字段。"""
131+
(self.log_dir / "product_last_status.json").write_text(
132+
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
133+
)
134+
129135
def test_basic_overview(self):
130136
"""有 timestamp + 有报告的基本场景。"""
131137
self._write_timestamp("stock-trading-data", "2026-03-13")
@@ -313,6 +319,63 @@ def test_overview_api_latest_dates_equal_local(self):
313319
self.assertEqual(overview[0]["days_behind"], 0)
314320
self.assertEqual(overview[0]["status_color"], "green")
315321

322+
def test_overview_sync_source_date_is_not_used_as_api_cache(self):
323+
"""sync 写入的 date_time 不能当作 API 缓存日期使用。"""
324+
self._write_timestamp("coin-cap", "2026-03-10")
325+
self._write_product_last_status({
326+
"coin-cap": {
327+
"status": "skipped",
328+
"reason_code": "no_valid_output",
329+
"error": "同步未产生可用输出,已跳过状态推进。",
330+
"date_time": "2026-03-11",
331+
"checked_at": "2026-03-13T09:00:00",
332+
"source": "sync",
333+
}
334+
})
335+
336+
import unittest.mock
337+
with unittest.mock.patch(
338+
"quantclass_sync_internal.data_query.report_dir_path",
339+
return_value=self.log_dir,
340+
):
341+
overview = get_products_overview(
342+
self.data_root,
343+
["coin-cap"],
344+
today=date(2026, 3, 13),
345+
)
346+
347+
self.assertEqual(overview[0]["days_behind"], 3)
348+
self.assertEqual(overview[0]["status_color"], "yellow")
349+
350+
def test_overview_same_api_date_after_no_valid_output_is_not_pending(self):
351+
"""同一 API 日期已确认 no_valid_output 时,不再标记为待更新。"""
352+
self._write_timestamp("coin-cap", "2026-03-10")
353+
self._write_product_last_status({
354+
"coin-cap": {
355+
"status": "skipped",
356+
"reason_code": "no_valid_output",
357+
"error": "同步未产生可用输出,已跳过状态推进。",
358+
"date_time": "2026-03-11",
359+
"checked_at": "2026-03-13T09:00:00",
360+
"source": "sync",
361+
}
362+
})
363+
364+
import unittest.mock
365+
with unittest.mock.patch(
366+
"quantclass_sync_internal.data_query.report_dir_path",
367+
return_value=self.log_dir,
368+
):
369+
overview = get_products_overview(
370+
self.data_root,
371+
["coin-cap"],
372+
today=date(2026, 3, 13),
373+
api_latest_dates={"coin-cap": "2026-03-11"},
374+
)
375+
376+
self.assertEqual(overview[0]["days_behind"], 0)
377+
self.assertEqual(overview[0]["status_color"], "green")
378+
316379
def test_overview_api_latest_dates_earlier_than_local(self):
317380
"""API 日期早于本地日期时 behind=0(max(0, diff) 截断)。"""
318381
self._write_timestamp("stock-trading-data", "2026-03-14")

0 commit comments

Comments
 (0)