Files
ewoooc/services/cache_manager.py
OoO 2ac7410d40
All checks were successful
CD Pipeline / deploy (push) Successful in 2m20s
fix(dashboard): prewarm cache and expose pick evidence
2026-05-01 16:34:13 +08:00

141 lines
4.0 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 time
from pathlib import Path
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:
pass
return entry['data']
def set(self, key, data):
try:
fp = self._fp_fn()
except Exception:
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"
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()
def clear_daily_sales_cache():
"""清除當日業績 cache。"""
_DAILY_SALES_PROCESSED_CACHE.clear()
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:
pass