From 16a1f22bd8d04cb2bb8ecfa73314769cdb9f4a7e Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 19 May 2026 13:07:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E9=80=9F=E7=95=B6=E6=97=A5=E6=A5=AD?= =?UTF-8?q?=E7=B8=BE=20metadata=20=E6=9F=A5=E8=A9=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/daily_sales_routes.py | 40 +++++++++++++++++++++++++++-- tests/test_cache_manager.py | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/routes/daily_sales_routes.py b/routes/daily_sales_routes.py index 7c7125a..47654cf 100644 --- a/routes/daily_sales_routes.py +++ b/routes/daily_sales_routes.py @@ -202,6 +202,43 @@ def _get_available_daily_dates(engine, table_name='daily_sales_snapshot'): return dates +def _get_daily_sales_metadata(engine, table_name='daily_sales_snapshot'): + """一次取得日期選單與資料指紋,避免首屏每次掃兩輪 daily snapshot。""" + validate_table_name(table_name) + if engine.dialect.name != 'postgresql': + return ( + _get_available_daily_dates(engine, table_name), + _get_data_fingerprint(engine, table_name), + ) + + query = text( + f'SELECT MAX(snapshot_date)::text AS max_snapshot_date, ' + f'COUNT(*) AS row_count, ' + f'ARRAY_AGG(DISTINCT snapshot_date::date ORDER BY snapshot_date::date DESC) ' + f'FILTER (WHERE snapshot_date IS NOT NULL) AS available_dates ' + f'FROM "{table_name}"' + ) + try: + with engine.connect() as conn: + row = conn.execute(query).fetchone() + if not row: + return [], (None, 0) + raw_dates = row[2] or [] + dates = [] + for raw_date in raw_dates: + try: + dates.append(pd.to_datetime(raw_date).normalize()) + except Exception: + continue + return dates, (row[0], row[1] or 0) + except Exception as exc: + sys_log.warning(f"[DailySales] metadata 查詢失敗,改用相容查詢: {exc}") + return ( + _get_available_daily_dates(engine, table_name), + _get_data_fingerprint(engine, table_name), + ) + + def _read_daily_sales_window(engine, table_name, start_date, end_date): """只讀取畫面需要的日期窗口,降低冷 worker 首頁等待時間。""" return safe_read_sql( @@ -501,7 +538,7 @@ def daily_sales(): chart_data=None, categories=None, calendar_data=None, selected_month=None, datetime_now=datetime_now_str, active_page='daily_sales') - available_dates = _get_available_daily_dates(engine, table_name) + available_dates, current_fingerprint = _get_daily_sales_metadata(engine, table_name) if not available_dates: return render_template('daily_sales.html', error="資料表為空,請先匯入當日業績資料。", @@ -537,7 +574,6 @@ def daily_sales(): ) data_end = max(selected_date, month_end) - current_fingerprint = _get_data_fingerprint(engine, table_name) view_cache_key = "|".join([ table_name, 'view', diff --git a/tests/test_cache_manager.py b/tests/test_cache_manager.py index f12992e..41ed560 100644 --- a/tests/test_cache_manager.py +++ b/tests/test_cache_manager.py @@ -1,4 +1,5 @@ from pathlib import Path +from datetime import date import pickle from flask import Flask, session @@ -183,6 +184,54 @@ def test_clear_daily_sales_cache_removes_shared_view_cache_files(tmp_path, monke assert not cache_file.exists() +def test_daily_sales_metadata_uses_single_postgres_query(): + from routes import daily_sales_routes + + class FakeDialect: + name = "postgresql" + + class FakeResult: + def fetchone(self): + return ("2026-05-17", 85118, [date(2026, 5, 17), date(2026, 5, 16)]) + + class FakeConnection: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def execute(self, query): + self.query = str(query) + return FakeResult() + + class FakeEngine: + dialect = FakeDialect() + + def connect(self): + return FakeConnection() + + dates, fingerprint = daily_sales_routes._get_daily_sales_metadata(FakeEngine()) + + assert [d.strftime("%Y-%m-%d") for d in dates] == ["2026-05-17", "2026-05-16"] + assert fingerprint == ("2026-05-17", 85118) + + +def test_daily_sales_metadata_falls_back_for_sqlite(monkeypatch): + from routes import daily_sales_routes + + class FakeDialect: + name = "sqlite" + + class FakeEngine: + dialect = FakeDialect() + + monkeypatch.setattr(daily_sales_routes, "_get_available_daily_dates", lambda engine, table_name: ["date-a"]) + monkeypatch.setattr(daily_sales_routes, "_get_data_fingerprint", lambda engine, table_name: ("date-a", 1)) + + assert daily_sales_routes._get_daily_sales_metadata(FakeEngine()) == (["date-a"], ("date-a", 1)) + + def test_promo_dashboard_shared_cache_roundtrip(tmp_path, monkeypatch): from routes import edm_routes