This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
|
||||
import io
|
||||
import calendar
|
||||
import hashlib
|
||||
import os
|
||||
import pickle
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from urllib.parse import quote
|
||||
from flask import Blueprint, request, render_template, send_file
|
||||
@@ -14,7 +17,7 @@ from auth import login_required
|
||||
from sqlalchemy import inspect, text
|
||||
import pandas as pd
|
||||
|
||||
from config import BASE_DIR
|
||||
from config import BASE_DIR, SYSTEM_VERSION
|
||||
from database.manager import DatabaseManager
|
||||
from services.logger_manager import SystemLogger
|
||||
from utils.df_helpers import find_col
|
||||
@@ -25,6 +28,7 @@ from services.daily_sales_service import (
|
||||
)
|
||||
from services.cache_manager import (
|
||||
_DAILY_SALES_PROCESSED_CACHE as _SALES_PROCESSED_CACHE,
|
||||
_DAILY_SALES_VIEW_CACHE_DIR,
|
||||
clear_daily_sales_cache as _clear_daily_sales_cache,
|
||||
)
|
||||
|
||||
@@ -39,6 +43,7 @@ daily_sales_bp = Blueprint('daily_sales', __name__)
|
||||
|
||||
_CACHE_EXPIRY_SECONDS = 300 # 5 分鐘緩存過期
|
||||
_VIEW_CACHE_EXPIRY_SECONDS = 120
|
||||
_SHARED_VIEW_CACHE_EXPIRY_SECONDS = 1800
|
||||
_DAILY_SALES_VIEW_CACHE = {}
|
||||
_DAILY_SALES_VIEW_CACHE_MAX = 24
|
||||
|
||||
@@ -67,6 +72,46 @@ def _set_daily_view_cache(cache_key, context):
|
||||
}
|
||||
|
||||
|
||||
def _daily_view_cache_file(cache_key):
|
||||
digest = hashlib.md5(cache_key.encode('utf-8'), usedforsecurity=False).hexdigest()
|
||||
return _DAILY_SALES_VIEW_CACHE_DIR / f"{digest}.pkl"
|
||||
|
||||
|
||||
def _get_shared_daily_view_cache(cache_key):
|
||||
path = _daily_view_cache_file(cache_key)
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
if (datetime.now().timestamp() - path.stat().st_mtime) >= _SHARED_VIEW_CACHE_EXPIRY_SECONDS:
|
||||
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"[DailySales] 共享 view cache 讀取失敗: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def _set_shared_daily_view_cache(cache_key, context):
|
||||
path = _daily_view_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"[DailySales] 共享 view cache 寫入失敗: {exc}")
|
||||
try:
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def clear_daily_sales_cache():
|
||||
"""清除當日業績緩存(供匯入服務調用)"""
|
||||
_clear_daily_sales_cache()
|
||||
@@ -488,8 +533,9 @@ def daily_sales():
|
||||
'month' if is_month_view else 'day',
|
||||
str(current_fingerprint),
|
||||
])
|
||||
cached_context = _get_daily_view_cache(view_cache_key)
|
||||
cached_context = _get_daily_view_cache(view_cache_key) or _get_shared_daily_view_cache(view_cache_key)
|
||||
if cached_context:
|
||||
_set_daily_view_cache(view_cache_key, 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')}"
|
||||
@@ -581,6 +627,7 @@ def daily_sales():
|
||||
'active_page': 'daily_sales',
|
||||
}
|
||||
_set_daily_view_cache(view_cache_key, context)
|
||||
_set_shared_daily_view_cache(view_cache_key, context)
|
||||
return render_template('daily_sales.html', **context)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -70,6 +70,7 @@ _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():
|
||||
@@ -131,6 +132,15 @@ def clear_sales_cache():
|
||||
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():
|
||||
|
||||
@@ -119,6 +119,31 @@ def test_clear_sales_cache_removes_shared_page_cache_files(tmp_path, monkeypatch
|
||||
assert not cache_file.exists()
|
||||
|
||||
|
||||
def test_daily_sales_shared_view_cache_roundtrip(tmp_path, monkeypatch):
|
||||
from routes import daily_sales_routes
|
||||
|
||||
monkeypatch.setattr(daily_sales_routes, "_DAILY_SALES_VIEW_CACHE_DIR", tmp_path)
|
||||
cache_key = "daily_sales:view:test"
|
||||
context = {"summary": {"revenue": 456}, "active_page": "daily_sales"}
|
||||
|
||||
daily_sales_routes._set_shared_daily_view_cache(cache_key, context)
|
||||
|
||||
assert daily_sales_routes._get_shared_daily_view_cache(cache_key) == context
|
||||
|
||||
|
||||
def test_clear_daily_sales_cache_removes_shared_view_cache_files(tmp_path, monkeypatch):
|
||||
from services import cache_manager
|
||||
|
||||
monkeypatch.setattr(cache_manager, "_DAILY_SALES_VIEW_CACHE_DIR", tmp_path)
|
||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
||||
cache_file = tmp_path / "daily_sales_view.pkl"
|
||||
cache_file.write_bytes(b"stale")
|
||||
|
||||
cache_manager.clear_daily_sales_cache()
|
||||
|
||||
assert not cache_file.exists()
|
||||
|
||||
|
||||
def test_cache_dicts_are_only_defined_in_cache_manager():
|
||||
assignments = []
|
||||
for path in [ROOT / "app.py", *ROOT.glob("routes/*.py"), *ROOT.glob("services/*.py")]:
|
||||
|
||||
Reference in New Issue
Block a user