Files
ewoooc/services/cache_manager.py
OoO d8c7f6f19c
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
快取當日業績頁面 context
2026-05-13 12:19:11 +08:00

166 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快取單一來源模組。
ADR-017 Phase 3f-2: 將 sales/import/export/daily 會共同碰到的
module-level cache 收斂到這裡,避免各 route 各自持有一份 dict。
"""
import os
import logging
import time
from pathlib import Path
logger = logging.getLogger(__name__)
class FingerprintCache:
"""TTL + fingerprint 的小型 in-memory cache。"""
def __init__(self, name, fingerprint_fn):
self.name = name
self._store = {}
self._fp_fn = fingerprint_fn
def get(self, key, ttl=300):
entry = self._store.get(key)
if not entry:
return None
if time.time() - entry['ts'] > ttl:
return None
try:
if self._fp_fn() != entry['fp']:
return None
except Exception:
logger.debug("cache fingerprint check failed for %s", self.name, exc_info=True)
return entry['data']
def set(self, key, data):
try:
fp = self._fp_fn()
except Exception:
logger.debug("cache fingerprint write failed for %s", self.name, exc_info=True)
fp = None
self._store[key] = {'data': data, 'ts': time.time(), 'fp': fp}
def clear(self):
self._store.clear()
_SALES_DF_CACHE = {}
_SALES_PROCESSED_CACHE = {}
_SALES_OPTIONS_CACHE = {}
_SALES_ANALYSIS_RESULT_CACHE = {}
_SALES_CACHE_MAX_ENTRIES = 10
_SALES_CACHE_TTL = 600
_DAILY_SALES_PROCESSED_CACHE = {}
_DASHBOARD_DATA_CACHE = {
'consolidated_data': None,
'consolidated_timestamp': None,
'today_start': None,
'full_data': None,
'full_timestamp': None,
}
_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"
_DAILY_SALES_VIEW_CACHE_DIR = _BASE_DIR / "data" / "daily_sales_view_cache"
def cleanup_sales_cache():
"""清理 sales 處理後快取的過期與超額條目。"""
current_time = time.time()
expired_keys = [
key for key, value in _SALES_PROCESSED_CACHE.items()
if value.get('time') and current_time - value['time'] > _SALES_CACHE_TTL
]
for key in expired_keys:
_SALES_PROCESSED_CACHE.pop(key, None)
if len(_SALES_PROCESSED_CACHE) > _SALES_CACHE_MAX_ENTRIES:
sorted_items = sorted(
[(key, value.get('time', 0)) for key, value in _SALES_PROCESSED_CACHE.items()],
key=lambda item: item[1],
)
for key, _ in sorted_items[:-_SALES_CACHE_MAX_ENTRIES]:
_SALES_PROCESSED_CACHE.pop(key, None)
def set_sales_processed_cache(key, entry, aliases=()):
"""寫入 sales cache並可同步寫入別名 key。"""
entry.setdefault('time', time.time())
_SALES_PROCESSED_CACHE[key] = entry
for alias in aliases:
_SALES_PROCESSED_CACHE[alias] = entry
cleanup_sales_cache()
def clear_sales_cache_for_table(table_name):
"""匯入資料後清除指定表對應的所有 sales cache key。"""
_SALES_DF_CACHE.pop(table_name, None)
keys_to_delete = [
key for key in list(_SALES_PROCESSED_CACHE.keys())
if key == table_name or key.startswith(f"{table_name}_")
]
for key in keys_to_delete:
_SALES_PROCESSED_CACHE.pop(key, None)
def clear_sales_cache():
"""清除所有 sales cache。"""
_SALES_DF_CACHE.clear()
_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():
"""清除當日業績 cache。"""
_DAILY_SALES_PROCESSED_CACHE.clear()
try:
if _DAILY_SALES_VIEW_CACHE_DIR.exists():
for cache_file in _DAILY_SALES_VIEW_CACHE_DIR.glob("*.pkl"):
try:
cache_file.unlink()
except OSError:
logger.debug("daily sales view cache file cleanup failed: %s", cache_file, exc_info=True)
except OSError:
logger.debug("daily sales view cache cleanup failed", exc_info=True)
def clear_dashboard_cache():
"""清除商品看板 cache。"""
_DASHBOARD_DATA_CACHE.clear()
_DASHBOARD_DATA_CACHE.update({
'consolidated_data': None,
'consolidated_timestamp': None,
'today_start': None,
'full_data': None,
'full_timestamp': None,
})
try:
if os.path.exists(_DASHBOARD_SHARED_CACHE_FILE):
try:
os.replace(_DASHBOARD_SHARED_CACHE_FILE, _DASHBOARD_STALE_CACHE_FILE)
except OSError:
os.remove(_DASHBOARD_SHARED_CACHE_FILE)
except FileNotFoundError:
pass
except OSError:
logger.debug("dashboard shared cache cleanup failed", exc_info=True)