Skip to content

Commit 8f90b3e

Browse files
committed
refactor: 代码简化 - TTL 共享函数 + 参数收窄 + source 常量化 + 去重复 UI
1 parent 38c5886 commit 8f90b3e

5 files changed

Lines changed: 60 additions & 45 deletions

File tree

quantclass_sync_internal/gui/assets/index.html

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,10 +338,7 @@ <h2 class="setup-title">QuantClass Sync 初始设置</h2>
338338
</div>
339339
<span class="qs-progress-text" x-text="total > 0 ? completed + ' / ' + total : '准备中...'"></span>
340340
</div>
341-
<!-- 进度指示行 -->
342-
<div x-show="total === 0" style="font-size:0.92em; color:#6b7280; margin:6px 0 2px;">
343-
正在准备同步计划...
344-
</div>
341+
<!-- 进度指示行(仅在有产品时显示详情,准备中状态由进度条文字承担) -->
345342
<div x-show="total > 0" style="font-size:0.92em; color:#6b7280; margin:6px 0 2px;">
346343
<span x-text="'已完成 ' + completed + '/' + total"></span>
347344
<span x-show="currentProduct"> · 最新: <span style="font-weight:600" x-text="currentProduct"></span></span>

quantclass_sync_internal/orchestrator.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@
9999
write_local_timestamp,
100100
)
101101

102+
103+
def _is_cache_fresh(checked_at_str: str) -> bool:
104+
"""检查缓存时间戳是否在 TTL 内。旧格式(无 T)视为过期。"""
105+
if "T" not in checked_at_str:
106+
return False
107+
try:
108+
checked_at = datetime.strptime(checked_at_str, "%Y-%m-%dT%H:%M:%S")
109+
return (datetime.now() - checked_at).total_seconds() < API_DATE_CACHE_TTL_SECONDS
110+
except ValueError:
111+
return False
112+
113+
102114
def process_product(
103115
plan: ProductPlan,
104116
date_time: Optional[str],
@@ -510,19 +522,19 @@ def _resolve_requested_dates_for_plan(
510522
cached = api_date_cache.get(product_name) or api_date_cache.get(plan.name)
511523
if cached:
512524
cached_date, checked_at_str = cached
513-
if "T" in checked_at_str: # 旧日期格式无法比较,视为过期
525+
if _is_cache_fresh(checked_at_str):
526+
# 计算缓存年龄用于日志
514527
try:
515528
checked_at = datetime.strptime(checked_at_str, "%Y-%m-%dT%H:%M:%S")
516529
age_seconds = (datetime.now() - checked_at).total_seconds()
517-
if age_seconds < API_DATE_CACHE_TTL_SECONDS:
518-
log_info(
519-
f"[{plan.name}] 使用缓存 API 日期 {cached_date}{int(age_seconds)}s 前查询)",
520-
event="PRODUCT_PLAN", decision="cache_hit",
521-
)
522-
api_latest_candidates = [cached_date]
523-
cache_hit = True
524530
except ValueError:
525-
pass # 解析失败,回退 HTTP
531+
age_seconds = 0.0
532+
log_info(
533+
f"[{plan.name}] 使用缓存 API 日期 {cached_date}{int(age_seconds)}s 前查询)",
534+
event="PRODUCT_PLAN", decision="cache_hit",
535+
)
536+
api_latest_candidates = [cached_date]
537+
cache_hit = True
526538

527539
if not cache_hit:
528540
try:
@@ -892,32 +904,27 @@ def _maybe_run_coin_preprocess(
892904

893905
def _prefetch_api_dates(
894906
products: List[str],
895-
api_base: str,
907+
command_ctx: "CommandContext",
896908
hid: str,
897909
headers: Dict[str, str],
898-
log_dir: Path,
899910
max_workers: int = 8,
900911
) -> Dict[str, Tuple[str, str]]:
901912
"""并发预取产品的 API 最新日期,写入缓存并返回。
902913
903914
已在缓存中且未过期的产品跳过。失败的产品静默跳过,
904915
Plan 阶段会回退到逐产品 HTTP 查询。
905916
"""
917+
api_base = command_ctx.api_base.rstrip("/")
918+
log_dir = report_dir_path(command_ctx.data_root)
906919
# 1. 读现有缓存,筛出需要查询的产品
907920
existing_cache = load_api_latest_dates(log_dir)
908-
now = datetime.now()
909921
uncached = []
910922
for product in products:
911923
cached = existing_cache.get(product)
912924
if cached:
913925
_, checked_at_str = cached
914-
if "T" in checked_at_str:
915-
try:
916-
checked_at = datetime.strptime(checked_at_str, "%Y-%m-%dT%H:%M:%S")
917-
if (now - checked_at).total_seconds() < API_DATE_CACHE_TTL_SECONDS:
918-
continue # 缓存新鲜,跳过
919-
except ValueError:
920-
pass
926+
if _is_cache_fresh(checked_at_str):
927+
continue # 缓存新鲜,跳过
921928
uncached.append(product)
922929

923930
if not uncached:
@@ -1027,10 +1034,9 @@ def _execute_plans(
10271034
product_names = [normalize_product_name(p.name) for p in plans]
10281035
_api_date_cache = _prefetch_api_dates(
10291036
products=product_names,
1030-
api_base=command_ctx.api_base.rstrip("/"),
1037+
command_ctx=command_ctx,
10311038
hid=hid,
10321039
headers=headers,
1033-
log_dir=report_dir_path(command_ctx.data_root),
10341040
)
10351041

10361042
# stop-on-error 要求严格顺序控制,强制串行

quantclass_sync_internal/status_store.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232

3333
_RUN_SCOPE_PATTERN = re.compile(r"^\d{8}-\d{6}(?:[-_].+)?$")
3434

35+
# 产品最后状态写入来源标识常量
36+
_SOURCE_API_CHECK = "api_check"
37+
_SOURCE_SYNC = "sync"
38+
3539

3640
def _status_db_has_rows(path: Path) -> bool:
3741
"""判断状态库是否可用(存在 product_status 且至少 1 行)。"""
@@ -472,7 +476,7 @@ def _update_product_last_status(log_dir: Path, report: RunReport) -> None:
472476
"error": item.error,
473477
"date_time": item.date_time,
474478
"checked_at": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
475-
"source": "sync",
479+
"source": _SOURCE_SYNC,
476480
}
477481
# 原子写入
478482
with atomic_temp_path(status_path, tag="last_status") as tmp:
@@ -505,12 +509,12 @@ def update_api_latest_dates(log_dir: Path, api_latest_dates: Dict[str, str]) ->
505509
if product in existing:
506510
existing[product]["date_time"] = date_str
507511
existing[product]["checked_at"] = checked_at
508-
existing[product]["source"] = "api_check"
512+
existing[product]["source"] = _SOURCE_API_CHECK
509513
else:
510514
existing[product] = {
511515
"status": "", "reason_code": "", "error": "",
512516
"date_time": date_str, "checked_at": checked_at,
513-
"source": "api_check",
517+
"source": _SOURCE_API_CHECK,
514518
}
515519
with atomic_temp_path(status_path, tag="last_status") as tmp:
516520
tmp.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
@@ -537,7 +541,7 @@ def load_api_latest_dates(log_dir: Path) -> Dict[str, Tuple[str, str]]:
537541
if not isinstance(info, dict):
538542
continue
539543
# 只读取 check_updates 写入的记录,排除同步结果
540-
if info.get("source") != "api_check":
544+
if info.get("source") != _SOURCE_API_CHECK:
541545
continue
542546
dt, ca = info.get("date_time"), info.get("checked_at")
543547
if isinstance(dt, str) and isinstance(ca, str):

tests/test_status_store.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from quantclass_sync_internal.models import ProductStatus
1010
from quantclass_sync_internal.reporting import _append_result, _new_report
1111
from quantclass_sync_internal.status_store import (
12+
_SOURCE_API_CHECK,
13+
_SOURCE_SYNC,
1214
_update_product_last_status,
1315
ensure_status_table,
1416
export_status_json,
@@ -167,7 +169,7 @@ def test_update_api_latest_dates_writes_iso_datetime_and_source(self):
167169
entry = status["product-a"]
168170
self.assertIn("T", entry["checked_at"])
169171
self.assertRegex(entry["checked_at"], r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$")
170-
self.assertEqual(entry["source"], "api_check")
172+
self.assertEqual(entry["source"], _SOURCE_API_CHECK)
171173

172174
def test_product_last_status_writes_iso_datetime_and_sync_source(self):
173175
"""_update_product_last_status 应写入 ISO datetime 和 source="sync"。"""
@@ -179,7 +181,7 @@ def test_product_last_status_writes_iso_datetime_and_sync_source(self):
179181
status = json.loads((log_dir / "product_last_status.json").read_text())
180182
entry = status["p1"]
181183
self.assertIn("T", entry["checked_at"])
182-
self.assertEqual(entry["source"], "sync")
184+
self.assertEqual(entry["source"], _SOURCE_SYNC)
183185

184186

185187
class TestLoadApiLatestDates(unittest.TestCase):
@@ -227,9 +229,9 @@ def test_sync_result_excluded(self):
227229
status_path = log_dir / "product_last_status.json"
228230
status_path.write_text(json.dumps({
229231
"prod-a": {"date_time": "2026-03-18", "checked_at": "2026-03-18T10:00:00",
230-
"source": "api_check", "status": ""},
232+
"source": _SOURCE_API_CHECK, "status": ""},
231233
"prod-b": {"date_time": "2026-03-17", "checked_at": "2026-03-18T10:00:00",
232-
"source": "sync", "status": "ok"},
234+
"source": _SOURCE_SYNC, "status": "ok"},
233235
}))
234236
cache = load_api_latest_dates(log_dir)
235237
self.assertIn("prod-a", cache)

tests/test_update_catchup.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from quantclass_sync_internal.models import CommandContext, EmptyDownloadLinkError, FatalRequestError, ProductPlan, ProductSyncError, RunReport, SyncStats
1111
from quantclass_sync_internal.orchestrator import _execute_plans, _resolve_requested_dates_for_plan
1212
from quantclass_sync_internal.reporting import _new_report
13-
from quantclass_sync_internal.status_store import connect_status_db
13+
from quantclass_sync_internal.status_store import _SOURCE_API_CHECK, connect_status_db
1414

1515

1616
class UpdateCatchUpTests(unittest.TestCase):
@@ -705,21 +705,31 @@ class TestPrefetchApiDates(unittest.TestCase):
705705
def setUp(self):
706706
self._tmpdir = tempfile.TemporaryDirectory()
707707
self.root = Path(self._tmpdir.name)
708+
# log_dir 路径与 report_dir_path(data_root) 保持一致:data_root/.quantclass_sync/log
708709
self.log_dir = self.root / ".quantclass_sync" / "log"
709710
self.log_dir.mkdir(parents=True)
710711

711712
def tearDown(self):
712713
self._tmpdir.cleanup()
713714

715+
def _ctx(self) -> CommandContext:
716+
"""构造测试用 CommandContext,api_base 设为 http://fake。"""
717+
return CommandContext(
718+
run_id="test",
719+
data_root=self.root,
720+
dry_run=False,
721+
stop_on_error=False,
722+
api_base="http://fake",
723+
)
724+
714725
@patch("quantclass_sync_internal.orchestrator.get_latest_time")
715726
def test_fetches_uncached_products(self, mock_get):
716727
"""无缓存时并发调用 get_latest_time。"""
717728
mock_get.return_value = "2026-03-18"
718729
from quantclass_sync_internal.orchestrator import _prefetch_api_dates
719730
cache = _prefetch_api_dates(
720731
products=["prod-a", "prod-b"],
721-
api_base="http://fake", hid="hid", headers={},
722-
log_dir=self.log_dir,
732+
command_ctx=self._ctx(), hid="hid", headers={},
723733
)
724734
self.assertEqual(mock_get.call_count, 2)
725735
self.assertIn("prod-a", cache)
@@ -734,8 +744,7 @@ def test_skips_fresh_cache(self, mock_get):
734744
update_api_latest_dates(self.log_dir, {"prod-a": "2026-03-18"})
735745
cache = _prefetch_api_dates(
736746
products=["prod-a"],
737-
api_base="http://fake", hid="hid", headers={},
738-
log_dir=self.log_dir,
747+
command_ctx=self._ctx(), hid="hid", headers={},
739748
)
740749
mock_get.assert_not_called()
741750
self.assertIn("prod-a", cache)
@@ -749,8 +758,7 @@ def test_partial_cache_only_fetches_missing(self, mock_get):
749758
update_api_latest_dates(self.log_dir, {"prod-a": "2026-03-18"})
750759
cache = _prefetch_api_dates(
751760
products=["prod-a", "prod-b"],
752-
api_base="http://fake", hid="hid", headers={},
753-
log_dir=self.log_dir,
761+
command_ctx=self._ctx(), hid="hid", headers={},
754762
)
755763
# 只查了 prod-b
756764
self.assertEqual(mock_get.call_count, 1)
@@ -763,8 +771,7 @@ def test_failure_returns_partial_cache(self, mock_get):
763771
from quantclass_sync_internal.orchestrator import _prefetch_api_dates
764772
cache = _prefetch_api_dates(
765773
products=["prod-a"],
766-
api_base="http://fake", hid="hid", headers={},
767-
log_dir=self.log_dir,
774+
command_ctx=self._ctx(), hid="hid", headers={},
768775
)
769776
# 失败但不抛异常,返回空缓存
770777
self.assertEqual(cache, {})
@@ -783,14 +790,13 @@ def test_expired_cache_refetches(self, mock_get):
783790
"prod-a": {
784791
"date_time": "2026-03-17",
785792
"checked_at": expired_time.strftime("%Y-%m-%dT%H:%M:%S"),
786-
"source": "api_check",
793+
"source": _SOURCE_API_CHECK,
787794
}
788795
}))
789796
mock_get.return_value = "2026-03-18"
790797
cache = _prefetch_api_dates(
791798
products=["prod-a"],
792-
api_base="http://fake", hid="hid", headers={},
793-
log_dir=self.log_dir,
799+
command_ctx=self._ctx(), hid="hid", headers={},
794800
)
795801
mock_get.assert_called_once() # 过期缓存触发重查
796802
self.assertEqual(cache["prod-a"][0], "2026-03-18") # 返回新日期

0 commit comments

Comments
 (0)