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