perf(dashboard): speed up competitor overview
All checks were successful
CD Pipeline / deploy (push) Successful in 2m12s
All checks were successful
CD Pipeline / deploy (push) Successful in 2m12s
This commit is contained in:
4
app.py
4
app.py
@@ -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 防護函數
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user