From 2b1174a902a6448a0f9b7367312bcd0d3a6e5cfc Mon Sep 17 00:00:00 2001 From: OoO Date: Wed, 13 May 2026 10:31:52 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E5=87=BA=E8=AA=A4=E5=85=A5=E7=9A=84?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E8=AE=8A=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.py | 2 +- routes/daily_sales_routes.py | 114 +++++++++-------------------------- 2 files changed, 29 insertions(+), 87 deletions(-) diff --git a/config.py b/config.py index cc13afe..0385f65 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.99" +SYSTEM_VERSION = "V10.98" 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..c78d130 100644 --- a/routes/daily_sales_routes.py +++ b/routes/daily_sales_routes.py @@ -61,13 +61,10 @@ def _get_data_fingerprint(engine, table_name='daily_sales_snapshot'): """DB 指紋 (max_snapshot_date, row_count):資料一變動指紋就跳號, 讓 4 worker 的 in-memory cache 在下一次 request 自然失效。""" try: - validate_table_name(table_name) - if engine.dialect.name == 'postgresql': - fingerprint_sql = f'SELECT MAX(snapshot_date)::text, COUNT(*) FROM "{table_name}"' - else: - fingerprint_sql = f'SELECT CAST(MAX(snapshot_date) AS TEXT), COUNT(*) FROM "{table_name}"' with engine.connect() as conn: - row = conn.execute(text(fingerprint_sql)).fetchone() + row = conn.execute(text( + f'SELECT MAX(snapshot_date)::text, COUNT(*) FROM "{table_name}"' + )).fetchone() return (row[0], row[1]) if row else (None, 0) except Exception as e: sys_log.warning(f"[Cache] fingerprint 查詢失敗(保守維持現有 cache): {e}") @@ -92,45 +89,6 @@ def _is_cache_valid(cache_key, engine=None, table_name='daily_sales_snapshot'): return True -def _get_available_daily_dates(engine, table_name='daily_sales_snapshot'): - """取得可選日期清單,避免為了 date selector 載入整張業績表。""" - validate_table_name(table_name) - if engine.dialect.name == 'postgresql': - date_expr = 'snapshot_date::date' - else: - date_expr = 'date(snapshot_date)' - - query = text( - f'SELECT DISTINCT {date_expr} AS snapshot_date ' - f'FROM "{table_name}" ' - 'WHERE snapshot_date IS NOT NULL ' - 'ORDER BY snapshot_date DESC' - ) - with engine.connect() as conn: - rows = conn.execute(query).fetchall() - - dates = [] - for row in rows: - try: - dates.append(pd.to_datetime(row[0]).normalize()) - except Exception: - continue - return dates - - -def _read_daily_sales_window(engine, table_name, start_date, end_date): - """只讀取畫面需要的日期窗口,降低冷 worker 首頁等待時間。""" - return safe_read_sql( - table_name, - engine=engine, - where_clause='"snapshot_date" >= :start_date AND "snapshot_date" <= :end_date', - params={ - 'start_date': start_date.strftime('%Y-%m-%d'), - 'end_date': end_date.strftime('%Y-%m-%d'), - }, - ) - - # ========================================== # 輔助函數 # ========================================== @@ -415,21 +373,35 @@ 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) - if not available_dates: - return render_template('daily_sales.html', - error="資料表為空,請先匯入當日業績資料。", - selected_date=None, available_dates=[], current=None, dod=None, wow=None, - chart_data=None, categories=None, calendar_data=None, selected_month=None, - datetime_now=datetime_now_str, active_page='daily_sales') + cache_key = f'{table_name}_daily' + # TTL + DB fingerprint 雙閘檢查(資料變動即自動失效,跨 worker 強一致) + if _is_cache_valid(cache_key, engine, table_name): + 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: + df = safe_read_sql(table_name, engine=engine) + if df.empty: + return render_template('daily_sales.html', + error="資料表為空,請先匯入當日業績資料。", + selected_date=None, available_dates=[], current=None, dod=None, wow=None, + chart_data=None, categories=None, calendar_data=None, selected_month=None, + datetime_now=datetime_now_str, active_page='daily_sales') - available_dates_str = [d.strftime('%Y-%m-%d') for d in available_dates] + df = preprocess_daily_sales_data(df) + _SALES_PROCESSED_CACHE[cache_key] = { + 'df': df, 'timestamp': datetime.now(), + 'fingerprint': _get_data_fingerprint(engine, table_name), + } + sys_log.info(f"已重新載入數據並更新緩存,共 {len(df)} 筆記錄") + + available_dates = sorted(df['snapshot_date'].unique(), reverse=True) + available_dates_str = [d.strftime('%Y-%m-%d') if isinstance(d, pd.Timestamp) else str(d) for d in available_dates] selected_date_param = request.args.get('date') if selected_date_param: selected_date = pd.to_datetime(selected_date_param) else: - selected_date = available_dates[0] + selected_date = df['snapshot_date'].max() selected_month_param = request.args.get('month') if selected_month_param: @@ -441,42 +413,12 @@ def daily_sales(): if selected_month_param and not selected_date_param: is_month_view = True - month_start = selected_month.replace(day=1) - month_end = (month_start + pd.DateOffset(months=1)) - pd.Timedelta(days=1) - data_start = min( - selected_date - pd.Timedelta(days=30), - selected_date - pd.Timedelta(days=7), - month_start - pd.Timedelta(days=1), - ) - data_end = max(selected_date, month_end) - - 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): - 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: - df = _read_daily_sales_window(engine, table_name, data_start, data_end) - if df.empty: - return render_template('daily_sales.html', - error="所選日期區間沒有業績資料。", - selected_date=None, available_dates=available_dates_str, current=None, dod=None, wow=None, - chart_data=None, categories=None, calendar_data=None, selected_month=None, - datetime_now=datetime_now_str, active_page='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), - } - sys_log.info( - f"已載入當日業績窗口 {data_start.strftime('%Y-%m-%d')}~{data_end.strftime('%Y-%m-%d')},共 {len(df)} 筆記錄" - ) - current_kpi = calculate_daily_kpis(df, selected_date) dod_kpi = calculate_dod(df, selected_date) wow_kpi = calculate_wow(df, selected_date) + month_start = selected_month.replace(day=1) + month_end = (month_start + pd.DateOffset(months=1)) - pd.Timedelta(days=1) month_df = df[(df['snapshot_date'] >= month_start) & (df['snapshot_date'] <= month_end)] cols = month_df.columns.tolist()