This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user