perf(dashboard): speed up competitor overview
All checks were successful
CD Pipeline / deploy (push) Successful in 2m12s

This commit is contained in:
OoO
2026-05-01 20:36:25 +08:00
parent b9d6186d68
commit 4e853a233f
4 changed files with 136 additions and 6 deletions

4
app.py
View File

@@ -95,8 +95,8 @@ except Exception as e:
sys_log.error(f"無法檢測磁碟空間: {e}")
# 🚩 系統版本定義 (備份與顯示用)
# 🚩 2026-05-01 V10.68: Sync latest MOMO Pro prototype styling to production frontend
SYSTEM_VERSION = "V10.68"
# 🚩 2026-05-01 V10.69: Speed up dashboard competitor overview with real cached product data
SYSTEM_VERSION = "V10.69"
# ==========================================
# 🔒 SQL Injection 防護函數

View File

@@ -254,7 +254,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.68"
SYSTEM_VERSION = "V10.69"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -209,7 +209,7 @@ def _dashboard_decision_row(row, tone):
}
def _load_competitor_decision_overview(session):
def _load_competitor_decision_overview(session, latest_items=None):
"""讀取商品看板第一屏使用的 PChome 比價決策摘要。全部來自正式 DB。"""
cache_key = 'competitor_decision_overview'
cache_ts_key = 'competitor_decision_overview_timestamp'
@@ -237,6 +237,133 @@ def _load_competitor_decision_overview(session):
'pending_priority': [],
}
if latest_items:
try:
item_map = {}
for item in latest_items:
record = item.get('record')
product = getattr(record, 'product', None)
sku = str(getattr(product, 'i_code', '') or '')
if not sku:
continue
item_map[sku] = {
'sku': sku,
'name': getattr(product, 'name', '') or '',
'category': getattr(product, 'category', '') or '',
'momo_url': getattr(product, 'url', None) or _build_momo_product_url(sku),
'momo_price': _to_float(getattr(record, 'price', None)) or 0,
}
competitor_map = _load_pchome_competitor_map(session, item_map.keys())
compared = []
for sku, item in item_map.items():
competitor = competitor_map.get(sku)
pchome_price = _to_float(competitor.get('price')) if competitor else None
if not pchome_price:
continue
gap_amount = item['momo_price'] - pchome_price
gap_pct = gap_amount / pchome_price * 100 if pchome_price else 0
compared.append({
**item,
'pchome_price': pchome_price,
'competitor_product_id': competitor.get('product_id'),
'competitor_product_name': competitor.get('product_name'),
'match_score': competitor.get('match_score'),
'crawled_at': competitor.get('crawled_at'),
'gap_amount': gap_amount,
'gap_pct': gap_pct,
})
picks_rows = session.execute(text("""
SELECT
sku,
name,
momo_price,
pchome_price,
gap_pct,
confidence,
reason,
model_footprint,
created_at
FROM ai_price_recommendations
WHERE strategy = 'product_pick'
AND status = 'pending'
ORDER BY confidence DESC NULLS LAST, gap_pct DESC NULLS LAST, created_at DESC
LIMIT 3
""")).mappings().all()
ai_pick_count = session.execute(text("""
SELECT COUNT(*)
FROM ai_price_recommendations
WHERE strategy = 'product_pick'
AND status = 'pending'
""")).scalar() or 0
total_active = len(item_map)
matched_count = len(compared)
pchome_advantages = [row for row in compared if row['gap_pct'] >= 5]
momo_threats = [row for row in compared if row['gap_pct'] <= -5]
near_items = [row for row in compared if -5 < row['gap_pct'] < 5]
pending_items = [
row for sku, row in item_map.items()
if sku not in competitor_map
]
last_crawled = max(
(row.get('crawled_at') for row in compared if row.get('crawled_at')),
default=None,
)
overview = dict(default)
overview.update({
'total_active': total_active,
'matched_count': matched_count,
'match_rate': round(matched_count / max(total_active, 1) * 100, 1),
'pchome_advantage_count': len(pchome_advantages),
'momo_threat_count': len(momo_threats),
'near_count': len(near_items),
'pending_match_count': max(total_active - matched_count, 0),
'ai_pick_count': int(ai_pick_count),
'avg_advantage_gap': round(
sum(row['gap_pct'] for row in pchome_advantages) / len(pchome_advantages),
1,
) if pchome_advantages else 0,
'last_pchome_crawled': _format_dashboard_dt(last_crawled),
})
overview['top_pchome_advantages'] = [
_dashboard_decision_row(row, 'win')
for row in sorted(pchome_advantages, key=lambda row: row['gap_pct'], reverse=True)[:3]
]
overview['top_momo_threats'] = [
_dashboard_decision_row(row, 'risk')
for row in sorted(momo_threats, key=lambda row: row['gap_pct'])[:3]
]
overview['top_picks'] = []
for row in picks_rows:
pick = dict(row)
competitor = competitor_map.get(str(pick.get('sku') or '')) or {}
pick['competitor_product_id'] = competitor.get('product_id')
pick['competitor_product_name'] = competitor.get('product_name')
pick['crawled_at'] = competitor.get('crawled_at')
overview['top_picks'].append(_dashboard_decision_row(pick, 'pick'))
overview['pending_priority'] = [
{
'sku': row['sku'],
'name': row['name'],
'category': row['category'],
'momo_price': row['momo_price'],
'momo_url': row['momo_url'],
}
for row in sorted(pending_items, key=lambda row: row['momo_price'], reverse=True)[:3]
]
_DASHBOARD_DATA_CACHE[cache_key] = overview
_DASHBOARD_DATA_CACHE[cache_ts_key] = time.time()
return overview
except Exception as exc:
sys_log.warning(f"[Dashboard] PChome 比價快取摘要讀取略過,改用 SQL 後備: {exc}")
try:
session.rollback()
except Exception:
pass
latest_compared_cte = """
WITH latest_momo AS (
SELECT
@@ -1276,7 +1403,7 @@ def index():
competitor.get('price') if competitor else None
)
competitor_overview = _load_competitor_decision_overview(session)
competitor_overview = _load_competitor_decision_overview(session, unique_items)
template_name = 'dashboard.html' if request.args.get('ui') == 'legacy' else 'dashboard_v2.html'
return render_template(template_name,

View File

@@ -87,7 +87,10 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
assert "_write_shared_full_dashboard_cache(full_data)" in route_source
assert "warm_full_dashboard_cache" in route_source
assert "force_rebuild=False" in route_source
assert "_load_competitor_decision_overview(session)" in route_source
assert "def _load_competitor_decision_overview(session, latest_items=None)" in route_source
assert "_load_competitor_decision_overview(session, unique_items)" in route_source
assert "item_map = {}" in route_source
assert "competitor_map = _load_pchome_competitor_map(session, item_map.keys())" in route_source
assert "ai_price_recommendations" in route_source
assert "pending_match_count" in route_source
assert "MockRecord" not in route_source