From f8b9b1abf7f45e3fc4b1459ee4016a87a3d01fc5 Mon Sep 17 00:00:00 2001 From: OoO Date: Wed, 13 May 2026 12:15:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B1=E7=94=A8=E6=A5=AD=E7=B8=BE=E5=88=86?= =?UTF-8?q?=E6=9E=90=E9=A0=81=E9=9D=A2=E5=BF=AB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.py | 2 +- routes/sales_routes.py | 69 ++++++++++++++++++++++++++++++++------- services/cache_manager.py | 10 ++++++ 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/config.py b/config.py index 9850c90..b06deb0 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.109" +SYSTEM_VERSION = "V10.110" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/routes/sales_routes.py b/routes/sales_routes.py index 756f587..fcff7d0 100644 --- a/routes/sales_routes.py +++ b/routes/sales_routes.py @@ -11,6 +11,8 @@ import hashlib import io import math +import os +import pickle import time import traceback from datetime import datetime, timezone, timedelta @@ -20,7 +22,7 @@ from sqlalchemy import inspect, text import pandas as pd import numpy as np -from config import BASE_DIR, DATABASE_TYPE +from config import BASE_DIR, DATABASE_TYPE, SYSTEM_VERSION from database.manager import DatabaseManager from services.logger_manager import SystemLogger from services.daily_sales_service import prepare_marketing_summary @@ -28,6 +30,7 @@ from services.cache_manager import ( _SALES_PROCESSED_CACHE, _SALES_OPTIONS_CACHE, _SALES_ANALYSIS_RESULT_CACHE, + _SALES_ANALYSIS_PAGE_CACHE_DIR, set_sales_processed_cache, ) from utils.text_helpers import get_color_for_string @@ -47,6 +50,7 @@ _SALES_PREVIEW_CACHE_TTL = 600 _SALES_OPTIONS_CACHE_TTL = 1800 _SALES_PAGE_CONTEXT_CACHE_TTL = 180 _SALES_PAGE_CONTEXT_CACHE_MAX = 24 +_SALES_SHARED_PAGE_CONTEXT_CACHE_TTL = 1800 # ========================================== @@ -101,6 +105,45 @@ def _set_sales_page_context_cache(cache_key, context): ) +def _sales_page_context_cache_file(cache_key): + return _SALES_ANALYSIS_PAGE_CACHE_DIR / f"{cache_key}.pkl" + + +def _get_sales_shared_page_context_cache(cache_key): + path = _sales_page_context_cache_file(cache_key) + if not path.exists(): + return None + try: + if time.time() - path.stat().st_mtime >= _SALES_SHARED_PAGE_CONTEXT_CACHE_TTL: + path.unlink(missing_ok=True) + return None + with open(path, 'rb') as f: + payload = pickle.load(f) + if payload.get('version') != SYSTEM_VERSION: + return None + return payload.get('context') + except Exception as exc: + sys_log.warning(f"[Sales Analysis] 共享頁面快取讀取失敗: {exc}") + return None + + +def _set_sales_shared_page_context_cache(cache_key, context): + path = _sales_page_context_cache_file(cache_key) + tmp_path = path.with_suffix(f".{os.getpid()}.tmp") + try: + os.makedirs(path.parent, exist_ok=True) + with open(tmp_path, 'wb') as f: + pickle.dump({'version': SYSTEM_VERSION, 'context': context}, f, protocol=pickle.HIGHEST_PROTOCOL) + os.replace(tmp_path, path) + except Exception as exc: + sys_log.warning(f"[Sales Analysis] 共享頁面快取寫入失敗: {exc}") + try: + if tmp_path.exists(): + tmp_path.unlink() + except OSError: + pass + + def _format_sales_data_range(min_date, max_date): if not min_date or not max_date: return '' @@ -854,6 +897,19 @@ def sales_analysis(): else: cache_key = f"{table_name}_{data_range_months}m" + page_cache_key = "sales_analysis:page_context:" + _sales_analysis_args_fingerprint( + table_name, + cache_key, + SYSTEM_VERSION, + ) + cached_context = ( + _get_sales_page_context_cache(page_cache_key) + or _get_sales_shared_page_context_cache(page_cache_key) + ) + if cached_context: + _set_sales_page_context_cache(page_cache_key, cached_context) + return render_template('sales_analysis.html', **cached_context) + # 2. 讀取與處理資料 (V-Opt: 使用二級快取機制 Raw -> Processed) df = None cols_map = {} @@ -1161,16 +1217,6 @@ def sales_analysis(): } set_sales_processed_cache(cache_key, cache_entry, aliases=(table_name,)) - processed_entry = _SALES_PROCESSED_CACHE.get(cache_key, {}) - page_cache_key = "sales_analysis:page_context:" + _sales_analysis_args_fingerprint( - table_name, - cache_key, - processed_entry.get('time'), - ) - cached_context = _get_sales_page_context_cache(page_cache_key) - if cached_context: - return render_template('sales_analysis.html', **cached_context) - # 🚩 V-Opt: 使用共用篩選函式 target_df, cols_map, err = _get_filtered_sales_data(cache_key) if err: @@ -1705,6 +1751,7 @@ def sales_analysis(): 'db_data_range': db_data_range, } _set_sales_page_context_cache(page_cache_key, context) + _set_sales_shared_page_context_cache(page_cache_key, context) return render_template('sales_analysis.html', **context) except Exception as e: diff --git a/services/cache_manager.py b/services/cache_manager.py index ba280e8..0a28901 100644 --- a/services/cache_manager.py +++ b/services/cache_manager.py @@ -69,6 +69,7 @@ _DASHBOARD_CACHE_TTL = 1800 _BASE_DIR = Path(__file__).resolve().parents[1] _DASHBOARD_SHARED_CACHE_FILE = _BASE_DIR / "data" / "dashboard_full_cache.pkl" _DASHBOARD_STALE_CACHE_FILE = _BASE_DIR / "data" / "dashboard_full_cache_stale.pkl" +_SALES_ANALYSIS_PAGE_CACHE_DIR = _BASE_DIR / "data" / "sales_analysis_page_cache" def cleanup_sales_cache(): @@ -116,6 +117,15 @@ def clear_sales_cache(): _SALES_PROCESSED_CACHE.clear() _SALES_OPTIONS_CACHE.clear() _SALES_ANALYSIS_RESULT_CACHE.clear() + try: + if _SALES_ANALYSIS_PAGE_CACHE_DIR.exists(): + for cache_file in _SALES_ANALYSIS_PAGE_CACHE_DIR.glob("*.pkl"): + try: + cache_file.unlink() + except OSError: + logger.debug("sales analysis page cache file cleanup failed: %s", cache_file, exc_info=True) + except OSError: + logger.debug("sales analysis page cache cleanup failed", exc_info=True) def clear_daily_sales_cache():