From 6c236ebad0035e8dbb096b5f2b9841e9c26aaac6 Mon Sep 17 00:00:00 2001 From: OoO Date: Wed, 13 May 2026 11:35:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BF=AB=E5=8F=96=E7=95=B6=E6=97=A5=E6=A5=AD?= =?UTF-8?q?=E7=B8=BE=E9=A0=81=E9=9D=A2=E5=85=A7=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.py | 2 +- routes/daily_sales_routes.py | 81 ++++++++++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/config.py b/config.py index 398c6f4..24bf7f8 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.105" +SYSTEM_VERSION = "V10.106" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/routes/daily_sales_routes.py b/routes/daily_sales_routes.py index beb6c8c..40ba7d2 100644 --- a/routes/daily_sales_routes.py +++ b/routes/daily_sales_routes.py @@ -38,11 +38,39 @@ sys_log = SystemLogger("DailySalesRoutes").get_logger() daily_sales_bp = Blueprint('daily_sales', __name__) _CACHE_EXPIRY_SECONDS = 300 # 5 分鐘緩存過期 +_VIEW_CACHE_EXPIRY_SECONDS = 120 +_DAILY_SALES_VIEW_CACHE = {} +_DAILY_SALES_VIEW_CACHE_MAX = 24 + + +def _get_daily_view_cache(cache_key): + cache_data = _DAILY_SALES_VIEW_CACHE.get(cache_key) + if not cache_data: + return None + elapsed = (datetime.now() - cache_data['timestamp']).total_seconds() + if elapsed >= _VIEW_CACHE_EXPIRY_SECONDS: + _DAILY_SALES_VIEW_CACHE.pop(cache_key, None) + return None + return cache_data['context'] + + +def _set_daily_view_cache(cache_key, context): + if len(_DAILY_SALES_VIEW_CACHE) >= _DAILY_SALES_VIEW_CACHE_MAX: + oldest_key = min( + _DAILY_SALES_VIEW_CACHE, + key=lambda key: _DAILY_SALES_VIEW_CACHE[key]['timestamp'] + ) + _DAILY_SALES_VIEW_CACHE.pop(oldest_key, None) + _DAILY_SALES_VIEW_CACHE[cache_key] = { + 'context': context, + 'timestamp': datetime.now() + } def clear_daily_sales_cache(): """清除當日業績緩存(供匯入服務調用)""" _clear_daily_sales_cache() + _DAILY_SALES_VIEW_CACHE.clear() sys_log.info("已清除當日業績緩存") @@ -51,8 +79,9 @@ def clear_daily_sales_cache(): @login_required def api_clear_daily_sales_cache(): """手動清除快取(保留為 ops escape hatch;正常失效靠 DB fingerprint)""" - cache_size = len(_SALES_PROCESSED_CACHE) + cache_size = len(_SALES_PROCESSED_CACHE) + len(_DAILY_SALES_VIEW_CACHE) _clear_daily_sales_cache() + _DAILY_SALES_VIEW_CACHE.clear() sys_log.info(f"[API] 已清除當日業績緩存 (原有 {cache_size} 個緩存項目)") return {'success': True, 'message': f'已清除 {cache_size} 個緩存項目'} @@ -450,9 +479,22 @@ 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', + selected_date.strftime('%Y-%m-%d'), + selected_month.strftime('%Y-%m'), + 'month' if is_month_view else 'day', + str(current_fingerprint), + ]) + cached_context = _get_daily_view_cache(view_cache_key) + if cached_context: + return render_template('daily_sales.html', **cached_context) + cache_key = f"{table_name}_daily_{data_start.strftime('%Y%m%d')}_{data_end.strftime('%Y%m%d')}" # TTL + DB fingerprint 雙閘檢查(資料變動即自動失效,跨 worker 強一致) - if _is_cache_valid(cache_key, engine, table_name): + if _is_cache_valid(cache_key) and _SALES_PROCESSED_CACHE[cache_key].get('fingerprint') == current_fingerprint: df = _SALES_PROCESSED_CACHE[cache_key]['df'] sys_log.debug(f"使用緩存數據,剩餘有效時間: {_CACHE_EXPIRY_SECONDS - (datetime.now() - _SALES_PROCESSED_CACHE[cache_key]['timestamp']).total_seconds():.0f}秒") else: @@ -467,7 +509,7 @@ def daily_sales(): df = preprocess_daily_sales_data(df) _SALES_PROCESSED_CACHE[cache_key] = { 'df': df, 'timestamp': datetime.now(), - 'fingerprint': _get_data_fingerprint(engine, table_name), + 'fingerprint': current_fingerprint, } sys_log.info( f"已載入當日業績窗口 {data_start.strftime('%Y-%m-%d')}~{data_end.strftime('%Y-%m-%d')},共 {len(df)} 筆記錄" @@ -522,21 +564,24 @@ def daily_sales(): month_end=month_end if is_month_view else None ) - return render_template('daily_sales.html', - selected_date=selected_date.strftime('%Y-%m-%d') if isinstance(selected_date, pd.Timestamp) else selected_date, - available_dates=available_dates_str, - current=current_kpi, - dod=dod_kpi, - wow=wow_kpi, - month_kpi=month_kpi, - is_month_view=is_month_view, - chart_data=chart_data, - categories=category_list, - calendar_data=calendar_data, - marketing_data=marketing_data, - selected_month=selected_month.strftime('%Y-%m') if isinstance(selected_month, pd.Timestamp) else selected_month, - datetime_now=datetime_now_str, - active_page='daily_sales') + context = { + 'selected_date': selected_date.strftime('%Y-%m-%d') if isinstance(selected_date, pd.Timestamp) else selected_date, + 'available_dates': available_dates_str, + 'current': current_kpi, + 'dod': dod_kpi, + 'wow': wow_kpi, + 'month_kpi': month_kpi, + 'is_month_view': is_month_view, + 'chart_data': chart_data, + 'categories': category_list, + 'calendar_data': calendar_data, + 'marketing_data': marketing_data, + 'selected_month': selected_month.strftime('%Y-%m') if isinstance(selected_month, pd.Timestamp) else selected_month, + 'datetime_now': datetime_now_str, + 'active_page': 'daily_sales', + } + _set_daily_view_cache(view_cache_key, context) + return render_template('daily_sales.html', **context) except Exception as e: sys_log.error(f"[Web] [DailySales] Error: {e}")