This commit is contained in:
@@ -228,6 +228,12 @@ def import_excel():
|
||||
message = '匯入完成,但所有資料皆已存在 (重複),無新增數據。'
|
||||
|
||||
clear_sales_cache_for_table(table_name)
|
||||
if table_name == 'realtime_sales_monthly':
|
||||
try:
|
||||
from services.cache_service import clear_growth_cache
|
||||
clear_growth_cache()
|
||||
except Exception as cache_error:
|
||||
sys_log.warning(f"[Web] [Cache] 成長分析快取清除失敗: {cache_error}")
|
||||
sys_log.info(f"[Web] [Cache] 已清除業績分析快取: {table_name}")
|
||||
|
||||
return jsonify({'status': 'success', 'message': message, 'rows': rows_imported, 'table': table_name})
|
||||
@@ -241,6 +247,12 @@ def import_excel():
|
||||
df.to_sql(table_name, con=engine, if_exists='replace', index=False)
|
||||
|
||||
clear_sales_cache_for_table(table_name)
|
||||
if table_name == 'realtime_sales_monthly':
|
||||
try:
|
||||
from services.cache_service import clear_growth_cache
|
||||
clear_growth_cache()
|
||||
except Exception as cache_error:
|
||||
sys_log.warning(f"[Web] [Cache] 成長分析快取清除失敗: {cache_error}")
|
||||
sys_log.info(f"[Web] [Cache] 已清除業績分析快取: {table_name}")
|
||||
|
||||
return jsonify({
|
||||
|
||||
@@ -466,6 +466,18 @@ def _get_growth_source_fingerprint(engine, table_name='realtime_sales_monthly'):
|
||||
if not inspect(engine).has_table(table_name):
|
||||
return None
|
||||
table_name = validate_table_name(table_name)
|
||||
if engine.dialect.name == 'postgresql':
|
||||
from services.cache_service import (
|
||||
get_growth_source_fingerprint_cache,
|
||||
set_growth_source_fingerprint_cache,
|
||||
)
|
||||
cache_key = f"{engine.url}|{table_name}"
|
||||
cached_fingerprint = get_growth_source_fingerprint_cache(cache_key)
|
||||
if cached_fingerprint is not None:
|
||||
return cached_fingerprint
|
||||
else:
|
||||
cache_key = None
|
||||
|
||||
table_ref = f'"{table_name}"'
|
||||
if engine.dialect.name == 'postgresql':
|
||||
fingerprint_sql = text(f"""
|
||||
@@ -481,7 +493,10 @@ def _get_growth_source_fingerprint(engine, table_name='realtime_sales_monthly'):
|
||||
""")
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(fingerprint_sql).fetchone()
|
||||
return (row[0], row[1]) if row else None
|
||||
fingerprint = (row[0], row[1]) if row else None
|
||||
if cache_key is not None:
|
||||
set_growth_source_fingerprint_cache(cache_key, fingerprint)
|
||||
return fingerprint
|
||||
except Exception as exc:
|
||||
sys_log.warning(f"[GrowthAnalysis] 快取指紋查詢失敗,保守沿用 TTL: {exc}")
|
||||
return None
|
||||
|
||||
@@ -9,6 +9,7 @@ older imports working and only owns growth-analysis and slow-query helpers.
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
from services.cache_manager import (
|
||||
@@ -42,6 +43,8 @@ _GROWTH_ANALYSIS_CACHE = {
|
||||
_GROWTH_CACHE_TTL = 1800 # 成長分析快取: 30 分鐘
|
||||
_BASE_DIR = Path(__file__).resolve().parents[1]
|
||||
_GROWTH_SHARED_CACHE_FILE = _BASE_DIR / "data" / "growth_analysis_cache.pkl"
|
||||
_GROWTH_SOURCE_FINGERPRINT_CACHE = {}
|
||||
_GROWTH_SOURCE_FINGERPRINT_TTL = 60
|
||||
|
||||
# ==========================================
|
||||
# 慢查詢監控
|
||||
@@ -89,6 +92,7 @@ def clear_growth_cache():
|
||||
'timestamp': None,
|
||||
'source_fingerprint': None,
|
||||
}
|
||||
_GROWTH_SOURCE_FINGERPRINT_CACHE.clear()
|
||||
try:
|
||||
if os.path.exists(_GROWTH_SHARED_CACHE_FILE):
|
||||
os.remove(_GROWTH_SHARED_CACHE_FILE)
|
||||
@@ -128,6 +132,35 @@ def get_growth_cache():
|
||||
return _GROWTH_ANALYSIS_CACHE
|
||||
|
||||
|
||||
def get_growth_source_fingerprint_cache(cache_key):
|
||||
"""讀取短 TTL source fingerprint,避免每次命中頁面快取仍掃大表 COUNT。"""
|
||||
entry = _GROWTH_SOURCE_FINGERPRINT_CACHE.get(cache_key)
|
||||
if not entry:
|
||||
return None
|
||||
if time.time() - entry.get('time', 0) >= _GROWTH_SOURCE_FINGERPRINT_TTL:
|
||||
_GROWTH_SOURCE_FINGERPRINT_CACHE.pop(cache_key, None)
|
||||
return None
|
||||
return entry.get('fingerprint')
|
||||
|
||||
|
||||
def set_growth_source_fingerprint_cache(cache_key, fingerprint):
|
||||
"""寫入 source fingerprint 短快取;None 不快取,維持 fail-open。"""
|
||||
if fingerprint is None:
|
||||
return
|
||||
_GROWTH_SOURCE_FINGERPRINT_CACHE[cache_key] = {
|
||||
'fingerprint': fingerprint,
|
||||
'time': time.time(),
|
||||
}
|
||||
|
||||
|
||||
def clear_growth_source_fingerprint_cache(cache_key=None):
|
||||
"""清除 source fingerprint 快取;匯入流程會呼叫 clear_growth_cache 一併處理。"""
|
||||
if cache_key is None:
|
||||
_GROWTH_SOURCE_FINGERPRINT_CACHE.clear()
|
||||
else:
|
||||
_GROWTH_SOURCE_FINGERPRINT_CACHE.pop(cache_key, None)
|
||||
|
||||
|
||||
def _is_growth_payload_valid(payload, source_fingerprint=None):
|
||||
if not payload:
|
||||
return False
|
||||
|
||||
@@ -701,9 +701,13 @@ class ImportService:
|
||||
session.close()
|
||||
|
||||
logger.info(f"任務 {job_id} 匯入成功: {total_rows} 筆")
|
||||
# cache 失效改靠 _get_data_fingerprint(DB max(snapshot_date)+count(*)),
|
||||
# 寫入後指紋自動跳號,4 worker 下一次 request 時各自偵測失效,
|
||||
# 取代不可靠的 N-POST hack(命中率僅 9.4%,見 web-researcher 報告)。
|
||||
try:
|
||||
from services.cache_service import clear_growth_cache
|
||||
clear_growth_cache()
|
||||
except Exception as cache_error:
|
||||
logger.warning(f"任務 {job_id} 成長分析快取清除失敗: {cache_error}")
|
||||
# daily_sales cache 失效仍靠 _get_data_fingerprint(DB max(snapshot_date)+count(*));
|
||||
# growth cache 另有 shared file + source fingerprint,匯入後主動清掉避免短 TTL 內看到舊圖。
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -159,6 +159,22 @@ def test_clear_growth_cache_removes_shared_file(tmp_path, monkeypatch):
|
||||
assert not shared_cache.exists()
|
||||
|
||||
|
||||
def test_growth_source_fingerprint_short_cache_clears_with_growth_cache(tmp_path, monkeypatch):
|
||||
from services import cache_service
|
||||
|
||||
shared_cache = tmp_path / "growth_analysis_cache.pkl"
|
||||
monkeypatch.setattr(cache_service, "_GROWTH_SHARED_CACHE_FILE", shared_cache)
|
||||
cache_service.clear_growth_cache()
|
||||
|
||||
cache_service.set_growth_source_fingerprint_cache("growth:source", ("2026-05-17", 10))
|
||||
|
||||
assert cache_service.get_growth_source_fingerprint_cache("growth:source") == ("2026-05-17", 10)
|
||||
|
||||
cache_service.clear_growth_cache()
|
||||
|
||||
assert cache_service.get_growth_source_fingerprint_cache("growth:source") is None
|
||||
|
||||
|
||||
def test_daily_sales_shared_view_cache_roundtrip(tmp_path, monkeypatch):
|
||||
from routes import daily_sales_routes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user